open atlas
↑ К треку
SQL и PostgreSQL вглубь SQL · 01 · 02

Фильтрация строк: WHERE, приоритет операторов и sargability

WHERE решает, какие строки выживут — и, незаметно, сможет ли планировщик использовать индекс. Обёртка колонки в функцию или ведущий wildcard убивают индекс и превращают поиск за 2 мс в полный скан.

SQL Middle ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

Два запроса для ревьюера выглядят одинаково. 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';         -- полуоткрытый диапазон, без ловушки BETWEEN

Sargability: слово, объясняющее зазор в 900×

Предикат sargable (Search-ARGument-able), если планировщик может использовать его для работы с индексом вместо скана каждой строки. Правило большого пальца: индексируемая колонка должна стоять голой с одной стороны, сравниваясь с константой. Как только ты оборачиваешь колонку в функцию или преобразуешь её, B-tree — отсортированный по сырым значениям колонки — становится бесполезен, потому что индекс понятия не имеет, как сортируется lower(email).

Два не-sargable паттерна доминируют в продакшен-инцидентах:

  1. Функция на колонке. 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; здесь нужно лишь следствие формы предиката.)
  2. Ведущий 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?

Закончи аналогию

Заполни пропуск: предикат, которым планировщик может вести индекс — голая колонка, сравниваемая с константой — называется _______. Обёртка колонки в функцию разрушает это свойство.

Вспомните перед уходом
  1. 01
    Что значит 'sargable' и какое однострочное правило держит предикат sargable?
  2. 02
    Почему WHERE lower(email) = 'a@b.com' игнорирует индекс на email и как это починить?
  3. 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.