Как оптимизировать поиск по части данных с отсутствующими значениями в поле возраст
Что проверяют
Понимаете ли вы, как база обращается с NULL, и умеете ли подобрать индекс под конкретную выборку, а не «повесить индекс на возраст». Для Go-разработчика следом почти всегда идёт вопрос, как прочитать такое поле в структуру и не упасть на сканировании.
Как отвечать
Начните с уточнения: какую часть данных ищем. Это определяет решение.
- ищем строки без возраста (например, чтобы дозаполнить профиль): их обычно немного, и лучше всего работает частичный индекс
CREATE INDEX ON users (id) WHERE age IS NULL. Он маленький и обновляется только для этих строк; - ищем по диапазону возраста, а
NULLу большинства: частичный индексWHERE age IS NOT NULLменьше полного и быстрее обслуживается; - поведение PostgreSQL: B-tree хранит
NULL, и условиеage IS NULLиндекс использовать умеет. Утверждение «индекс не видит NULL» верно для Oracle, но не для Postgres; - проверка:
EXPLAIN (ANALYZE, BUFFERS)на реальном объёме данных, а не догадки.
Со стороны Go: rows.Scan в обычный int на NULL вернёт ошибку. Нужен sql.NullInt32, обобщённый sql.Null[int32] (появился в Go 1.22) или указатель *int32; в pgx то же самое через pgtype.Int4. Указатель удобен, если модель сразу уходит в JSON: nil сериализуется в null.
Условие частичного индекса пишите в тексте запроса литералом. Если признак передаётся параметром $1, обобщённый план подготовленного запроса, который pgx кеширует, такой индекс не возьмёт.
Где спотыкаются
Пишут WHERE age = NULL и удивляются пустому результату. Сравнение с NULL даёт NULL, нужен IS NULL.
Добавляют age IS NOT NULL AND age > 18 ради скорости. Условие age > 18 уже отсекает NULL, лишняя проверка ничего не ускоряет.
Предлагают заменить NULL на -1 или 0. Такой «возраст» попадает в AVG и в диапазонные условия, портит статистику планировщика, и каждый запрос приходится помнить об исключении.