Фильтрация строк: WHERE, приоритет операторов и sargability
WHERE решает, какие строки выживут — и, незаметно, сможет ли планировщик использовать индекс. Обёртка колонки в функцию или ведущий wildcard убивают индекс и превращают поиск за 2 мс в полный скан.
Два запроса для ревьюера выглядят одинаково. WHERE email = 'a@b.com' бежит за 2 мс на 20 миллионах пользователей. WHERE lower(email) = 'a@b.com' бежит 1.9 секунды на той же таблице — регрессия в 900×, прячущаяся за одним безобидным вызовом функции. Схема в порядке, индекс есть, данные те же. Разница в том, sargable ли предикат: устроен ли он так, что планировщик реально может использовать индекс. WHERE — не просто фильтр; это контракт, решающий, какие планы вообще доступны.
Приоритет: AND связывает крепче, чем OR
WHERE вычисляет булево выражение на строку. Ловушка — в приоритете: AND связывает крепче, чем OR, ровно как * крепче +. Поэтому это:
SELECT * FROM orders
WHERE status = 'paid' OR status = 'shipped' AND total > 100;не значит «(paid или shipped) и total больше 100». Это значит status = 'paid' OR (status = 'shipped' AND total > 100) — каждый оплаченный заказ возвращается независимо от total, чего почти никогда не имели в виду. Фикс механический и неоспоримый на ревью: скобки на любой смешанный AND/OR.
SELECT * FROM orders
WHERE (status = 'paid' OR status = 'shipped') AND total > 100;NOT связывает крепче всех троих. Когда мешаешь все три, безопасная привычка: никогда не доверяй приоритету, всегда группируй скобками. Баг приоритета не падает с ошибкой — он молча возвращает неверные строки, а это худший вид багов.
Удобные предикаты: BETWEEN, IN, LIKE/ILIKE
- BETWEEN a AND b включает оба конца:
total BETWEEN 100 AND 200— этоtotal >= 100 AND total <= 200. Классическая ошибка —BETWEENна timestamp:created_at BETWEEN '2026-01-01' AND '2026-01-31'исключает почти весь 31 января, потому что верхняя граница — полночь. Для диапазонов времени предпочитай полуоткрытый интервал:created_at >= '2026-01-01' AND created_at < '2026-02-01'. - IN (…) — аккуратная цепочка
OR:status IN ('paid','shipped'). (INс подзапросом иNULLимеет знаменитую ловушку — это следующий урок.) - LIKE делает сопоставление по шаблону:
%= любая последовательность символов,_= один символ. ILIKE — версия без учёта регистра (расширение Postgres).
-- users(id, email, display_name, created_at)
SELECT id, email FROM users
WHERE email LIKE 'support@%' -- закреплённый префикс: дружелюбен к индексу
AND created_at >= '2026-01-01'
AND created_at < '2026-02-01'; -- полуоткрытый диапазон, без ловушки BETWEENSargability: слово, объясняющее зазор в 900×
Предикат sargable (Search-ARGument-able), если планировщик может использовать его для работы с индексом вместо скана каждой строки. Правило большого пальца: индексируемая колонка должна стоять голой с одной стороны, сравниваясь с константой. Как только ты оборачиваешь колонку в функцию или преобразуешь её, B-tree — отсортированный по сырым значениям колонки — становится бесполезен, потому что индекс понятия не имеет, как сортируется lower(email).
Два не-sargable паттерна доминируют в продакшен-инцидентах:
- Функция на колонке.
WHERE lower(email) = 'a@b.com'не может использовать индекс наemail, потому что индекс хранитemail, а неlower(email). Фикс — выражательный индекс (expression index) — индекс, покрывающий ровно то выражение, которое ты запрашиваешь:CREATE INDEX ON users (lower(email));. Теперьlower(email) = …снова sargable. (Внутренности индексов — B-tree, правило ведущей колонки, выражательные индексы — глубоко преподаёт глава indexes трекаdatabases; здесь нужно лишь следствие формы предиката.) - Ведущий wildcard в LIKE.
WHERE email LIKE 'abc%'sargable — префиксabcзакрепляет непрерывный диапазон в отсортированном B-tree, и Postgres идёт прямо к нему.WHERE email LIKE '%abc'— нет: нет фиксированного префикса для поиска, так что каждое значение нужно проверить. Поиск с ведущим wildcard на масштабе требует другого инструмента (trigram GIN-индекс или full-text search, т.е. полнотекстовый поиск), а не B-tree.
Эти два паттерна вместе покрывают большинство багов «индекс есть, но не используется». Когда видишь медленный запрос, который должен быть быстрым, — первым делом проверь, стоит ли колонка голо в WHERE, прежде чем добавлять новые индексы.
▸Частая ошибка
Самая коварная не-sargable форма — неявная функция. WHERE created_at::date = '2026-01-15' кастует колонку на каждой строке, так что индекс на created_at обходится стороной. Перепиши как sargable диапазон: created_at >= '2026-01-15' AND created_at < '2026-01-16'. Та же логика, но теперь голая колонка сравнивается с константами, и индекс ведёт скан. Всегда подозревай, когда в WHERE вокруг колонки что-то навёрнуто.
Параметризуй — ради безопасности И ради переиспользования плана
Никогда не собирай значение WHERE склейкой пользовательского ввода. WHERE email = '" + input + "' — учебниковая дыра SQL-инъекции: ввод ' OR '1'='1 превращает твой фильтр в тавтологию, возвращающую всю таблицу. Фикс — параметризованные запросы (bind-параметры / prepared statements): значение едет отдельно от текста SQL, поэтому никогда не может изменить структуру запроса.
-- Параметризовано: плейсхолдер $1 привязывается к значению драйвером,
-- никогда не интерполируется в SQL-строку. Инъекция становится невозможной.
SELECT id, email FROM users WHERE email = $1;Параметризация также позволяет Postgres кешировать и переиспользовать распарсенный/спланированный оператор между вызовами, убирая накладные расходы parse и plan на горячих путях. Безопасность и производительность указывают в одну сторону: привязывай, не склеивай.
WHERE status = 'paid' OR status = 'shipped' AND total > 100 — какие строки вернутся?
Есть B-tree индекс на users(email). Какой предикат может использовать его для index scan?
Заполни пропуск: предикат, которым планировщик может вести индекс — голая колонка, сравниваемая с константой — называется _______. Обёртка колонки в функцию разрушает это свойство.
- 01Что значит 'sargable' и какое однострочное правило держит предикат sargable?
- 02Почему WHERE lower(email) = 'a@b.com' игнорирует индекс на email и как это починить?
- 03Дай приоритет NOT, AND, OR и практическое правило их смешивания.
WHERE — это стадия отбора из прошлого урока, и она делает две работы сразу: решает, какие строки выживут, и — через свою форму — решает, какие планы планировщик может выбрать. Ставь скобки на любой смешанный AND/OR, потому что AND связывает крепче OR, и промах приоритета молча возвращает неверные строки. Используй BETWEEN/IN/LIKE/ILIKE ради читаемости, но бери полуоткрытые диапазоны (>= … < …) на timestamp, чтобы обойти баг включающей границы BETWEEN. Сеньорская идея — sargability: держи индексируемую колонку голой и сравниваемой с константой, чтобы B-tree вёл скан; обёртка в lower(), каст или ведущий % в LIKE делают предикат не-sargable и форсят полный скан — чинится выражательным индексом или trigram/full-text индексом (глава indexes трека databases копает глубоко). Наконец, всегда параметризуй: привязывай значения вместо склейки, что закрывает дверь SQL-инъекции и даёт Postgres переиспользовать план. Теперь, когда видишь медленный запрос, первый вопрос: голо ли стоит индексируемая колонка в WHERE, или вокруг неё что-то навёрнуто? Этот единственный взгляд ловит большинство index-убивающих багов раньше, чем ты открываешь EXPLAIN.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.