$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогБыстрый старт в SQL: как писать WHERE так, чтобы индексы действительно использовались

Быстрый старт в SQL: как писать WHERE так, чтобы индексы действительно использовались

$ sudo teach IT
·26 июля 2026 г.·11 мин·67
Быстрый старт в SQL: как писать WHERE так, чтобы индексы действительно использовались

Поймём, почему запрос может быть медленным даже с индексом: не sargable условия, функции на колонках, неправильный порядок фильтров. Разберём типовые паттерны и дадим рецепты.

Содержание
Как оптимизатор “думает” про индексы: sargable против non-sargableМинимальный тест: есть ли индексный поиск?Антипаттерн №1: функции на колонках в WHEREПлохой паттерн: вы меняете значение колонкиИсправление: привести константу, а не колонкуЕсли нужны “функциональные” индексыАнтипаттерн №2: приведение типов (implicit/explicit casts)Пример: строка вместо числаРекомендация: приводите константу к типу колонкиБолее тонкий случай: даты и строкиАнтипаттерн №3: выражения в обеих сторонах сравненияПлохой паттернРецепт: переносите вычисления в константуАнтипаттерн №4: диапазоны “ломающие” условия и неаккуратные сравненияПлохой паттерн: функция по дате/времениПравильный паттерн: диапазон по исходной колонкеАнтипаттерн №5: OR-условия без понимания планаПроблемный запросРецепт: иногда помогает разнести OR на UNION ALLАнтипаттерн №6: неправильный порядок условий — миф и реальностьКак писать WHERE, чтобы условия были sargable: базовые шаблоны1) Равенство по колонке2) Диапазон по “сырой” колонке3) Предикаты с вычислениями — перенос в константы4) Избегайте функций на колонках5) LIKE: индекс может работать, но зависит от префиксаNULL и тройная логика: как не потерять индексный поискПравильно для NULLПлохоLIKE, ILIKE и регистр: что реально с индексамиПроверяем на практике: EXPLAIN как инструмент, а не формальностьИндексы ≠ гарантия: кардинальность, статистика и “не тот” индексСоставные индексы и “порядок” колонок: почему иногда индекс не используетсяПрактические рецепты: “переписать WHERE” без магииЧеклист sargabilityКак подойти к теме системно: от типичных ошибок к устойчивым навыкамВыводы

Индекс — это одно из самых эффективных средств ускорения выборки в реляционных базах. Но на практике часто случается обратное: вы видите подходящий индекс в схеме, а запрос всё равно выполняется медленно. Причина обычно не в “плохих настройках сервера” и не в “мало данных”, а в том, что условия в WHERE написаны так, что оптимизатор не может использовать индекс.

В этой статье разберём, почему запрос может быть медленным даже при наличии индекса, и как писать WHERE, чтобы условия были sargable (Search ARGument ABLE) — то есть такими, которые позволяют СУБД превратить ваш фильтр в эффективный индексный поиск. Покажем типовые анти-паттерны: функции на колонках, приведение типов, “неявные” преобразования, неправильные шаблоны сравнения и условия, которые ломают план выполнения. Затем дадим практические рецепты и короткие примеры кода.


Как оптимизатор “думает” про индексы: sargable против non-sargable

Чтобы индекс реально ускорял запрос, оптимизатор должен иметь возможность:

  1. Применить условие к колонке в неизменном виде (или хотя бы так, чтобы выражение можно было разложить на диапазоны/равенства).
  2. Использовать индекс для ограничения набора строк до выполнения остальной части запроса.

Когда условие в WHERE содержит выражение, результат которого зависит от преобразования самой колонки (например, WHERE LOWER(name) = 'john'), оптимизатор обычно не может “обратным ходом” определить, какие значения в индексе дадут нужный результат. В этом случае часто выбирается полный перебор или фильтрация после чтения данных из таблицы — индекс теряет смысл.

Термин sargable не стандартен везде одинаково, но смысл одинаков: условие должно быть таким, чтобы поиск по индексу можно было сформулировать как поиск по значениям или диапазонам.

Минимальный тест: есть ли индексный поиск?

Даже без глубокого понимания планов полезно знать, как выглядит “правильный” план: вы увидите Index Seek/Index Scan (в зависимости от СУБД и ситуации), а не Table Scan на десятки миллионов строк. В SQL это диагностируется через EXPLAIN/EXPLAIN ANALYZE.


Антипаттерн №1: функции на колонках в WHERE

Плохой паттерн: вы меняете значение колонки

Самый распространённый пример:

code
SELECT *
FROM users
WHERE LOWER(email) = 'test@example.com';

Идея понятная: сравниваем адрес без учёта регистра. Проблема в том, что выражение применяет функцию к колонке email. Для индекса по email оптимизатор не сможет использовать его напрямую: индекс хранит исходные значения, а вы сравниваете преобразованные.

Чем это заканчивается

Часто СУБД вынуждена прочитать много строк и применить LOWER уже на них.

Исправление: привести константу, а не колонку

Вместо функции на колонке попробуйте нормализовать константу:

code
SELECT *
FROM users
WHERE email = 'test@example.com';

Звучит банально, но вопрос в реальности: у вас действительно хранится email в одном регистре? Если да — это лучший вариант.

Если нужны “функциональные” индексы

Если требуется именно сравнение без регистра и данные могут храниться по-разному, корректный путь зависит от СУБД:

  • В PostgreSQL есть функциональные индексы: CREATE INDEX ... ON users (lower(email));
  • В SQL Server — индекс по вычисляемому (computed) столбцу или колlation/case-insensitive настройки.
  • В MySQL — варианты зависят от collations: иногда правильная сортировка/сравнение решает задачу без функций.

В PostgreSQL типичный подход:

code
CREATE INDEX users_email_lower_idx ON users (lower(email));

SELECT *
FROM users
WHERE lower(email) = lower('test@example.com');

Теперь условие стало sargable относительно функционального индекса.


Антипаттерн №2: приведение типов (implicit/explicit casts)

Если тип константы и тип колонки различаются, СУБД может выполнить преобразование. Иногда преобразование применится к колонке — и индекс снова “ломается”.

Пример: строка вместо числа

code
SELECT *
FROM orders
WHERE customer_id = '42';

Если customer_id — INT, то '42' будет преобразован, и иногда это безопасно. Но на практике важно проверять: в некоторых случаях оптимизатор может предпочесть план без индекса.

Рекомендация: приводите константу к типу колонки

code
SELECT *
FROM orders
WHERE customer_id = 42;

Более тонкий случай: даты и строки

code
SELECT *
FROM events
WHERE event_date >= '2026-01-01';

Если event_date — DATE или TIMESTAMP, то строка должна корректно парситься. Но если формат строк “неочевидный”, или СУБД вынуждена применять преобразование, индекс может перестать работать.

Лучший стиль — использовать литералы подходящего типа:

code
-- PostgreSQL:
SELECT *
FROM events
WHERE event_date >= DATE '2026-01-01';

Антипаттерн №3: выражения в обеих сторонах сравнения

Когда и колонка, и константа преобразуются, оптимизатору часто сложнее.

Плохой паттерн

code
SELECT *
FROM products
WHERE (price * 1.2) > 1000;

Если у вас есть индекс на price, он не поможет: вы ищете по выражению от колонки.

Рецепт: переносите вычисления в константу

code
SELECT *
FROM products
WHERE price > 1000 / 1.2;

Да, это арифметически то же самое (если нет нюансов с округлением/типами), но SQL становится sargable относительно price.


Антипаттерн №4: диапазоны “ломающие” условия и неаккуратные сравнения

Индекс особенно полезен на диапазонах (>=, <=) и на точных равенствах. Проблемы возникают, когда сравнение не позволяет оптимизатору выделить простой диапазон.

Плохой паттерн: функция по дате/времени

Например, поиск по “дню”:

code
SELECT *
FROM logs
WHERE DATE(created_at) = '2026-07-01';

Если created_at — TIMESTAMP, то DATE(created_at) применяет функцию к колонке — индекс обычно не используется.

Правильный паттерн: диапазон по исходной колонке

code
SELECT *
FROM logs
WHERE created_at >= TIMESTAMP '2026-07-01 00:00:00'
  AND created_at <  TIMESTAMP '2026-07-02 00:00:00';

Это классический рецепт. Он:

  • сохраняет возможность применить индекс по created_at;
  • корректно работает для таймзон и временной составляющей (при правильном типе литералов);
  • не требует вычисления DATE() на каждой строке.

Антипаттерн №5: OR-условия без понимания плана

OR сам по себе не запрещён, но он часто усложняет оптимизацию. Если OR “широкий” или условия на разных колонках, оптимизатор может выбрать неиндексный план.

Проблемный запрос

code
SELECT *
FROM users
WHERE status = 'active' OR email = 'test@example.com';

Если есть индексы и на status, и на email, теоретически возможно использование “bitmap/index union” (в зависимости от СУБД). Но на практике многое зависит от кардинальности, статистики, распределения данных.

Рецепт: иногда помогает разнести OR на UNION ALL

code
SELECT *
FROM users
WHERE status = 'active'
UNION ALL
SELECT *
FROM users
WHERE email = 'test@example.com';

Это может дать индексный путь по каждой ветке. Но важно помнить:

  • если данные могут пересекаться, UNION ALL даст дубликаты. Иногда нужен UNION (но он дороже);
  • план может измениться в зависимости от размеров выборки.

Поэтому правило такое: сначала проверьте EXPLAIN, затем принимайте решение.


Антипаттерн №6: неправильный порядок условий — миф и реальность

В SQL логика WHERE декларативная: оптимизатор сам решает, что и как исполнять. “Порядок фильтров” в тексте запроса обычно не влияет на то, какой индекс используется.

Но есть нюанс: порядок может влиять косвенно, если выражения содержат:

  • недетерминированные функции;
  • ошибки вычисления;
  • дорогие предикаты, которые могут оцениваться до/после (в зависимости от СУБД и оптимизации);
  • коррелированные подзапросы.

Однако в большинстве современных движков при чистых предикатах “переставьте условия — и всё станет быстро” не работает.

Практически полезный вывод: думайте не про порядок, а про форму предикатов (sargable vs non-sargable). Именно она определяет, сможет ли оптимизатор построить индексный план.


Как писать WHERE, чтобы условия были sargable: базовые шаблоны

Ниже набор рецептов, которые в реальной разработке чаще всего спасают от “почему индекс не используется”.

1) Равенство по колонке

Ок:

code
WHERE customer_id = 42

Плохо:

code
WHERE CAST(customer_id AS VARCHAR) = '42'

2) Диапазон по “сырой” колонке

Ок:

code
WHERE created_at >= :start
  AND created_at <  :end

Плохо:

code
WHERE DATE(created_at) = :day

3) Предикаты с вычислениями — перенос в константы

Плохо:

code
WHERE (price - discount) > 100

Ок (если корректно):

code
WHERE price > 100 + discount

(или вынести вычисление в параметр/подготовить заранее в приложении)

4) Избегайте функций на колонках

Плохо:

code
WHERE LOWER(name) LIKE 'ann%'

Ок (если возможно): либо функциональный индекс, либо хранение в нормализованном виде, либо подходящая collation.

5) LIKE: индекс может работать, но зависит от префикса

Индекс по строке обычно используется для LIKE 'prefix%', но часто не для LIKE '%suffix' или LIKE '%middle%'.

Пример:

code
WHERE email LIKE 'test@domain.com%'

Если у вас шаблоны с ведущим %, рассмотрите альтернативы: full-text/триграммы/отдельные индексы под pattern.


NULL и тройная логика: как не потерять индексный поиск

NULL — отдельная ловушка. В SQL нельзя писать “как в математике”: col = NULL не сработает.

Правильно для NULL

code
WHERE deleted_at IS NULL

Это часто sargable и обычно использует индекс (если он есть). А вот:

Плохо

code
WHERE deleted_at = NULL

В результате оптимизатор может выбрать неверные стратегии или вы получите пустую выборку.


LIKE, ILIKE и регистр: что реально с индексами

Если вы используете сравнение без регистра, то “функции на колонках” часто всплывают снова:

  • LOWER(col) = 'x'
  • ILIKE в PostgreSQL
  • case-insensitive сравнения в других СУБД

Решение опять же в функциональных индексах или в правильной схеме хранения/сравнения:

  • привести данные к единому регистру при записи;
  • использовать collation, где сравнение case-insensitive;
  • создать индекс по выражению lower(col).

Без одного из этих шагов запрос почти всегда превращается в сканирование.


Проверяем на практике: EXPLAIN как инструмент, а не формальность

Чтобы понять, что именно “ломает” индекс, смотрите планы выполнения. Подход:

  1. Возьмите запрос-минимум (один WHERE + простые SELECT).
  2. Смотрите план без лишних JOIN’ов.
  3. Поменяйте одно условие за раз и повторите EXPLAIN.
  4. Сверьте, исчез ли Index Seek/Bitmap Index Scan.

Ниже универсальная идея на примере PostgreSQL:

code
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM logs
WHERE DATE(created_at) = DATE '2026-07-01';

Потом сравните с диапазоном:

code
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM logs
WHERE created_at >= TIMESTAMP '2026-07-01 00:00:00'
  AND created_at <  TIMESTAMP '2026-07-02 00:00:00';

Разница часто видна сразу по типу сканирования.


Индексы ≠ гарантия: кардинальность, статистика и “не тот” индекс

Важно честно признать: даже sargable условие не всегда даст выигрыш.

Причины:

  • индекс есть, но он не подходит под предикат (например, вы фильтруете по B, а индекс по A);
  • индекс частично подходит, но оценка оптимизатора считает сканирование дешевле (неверная статистика);
  • условия слишком “широкие”, и почти все строки попадают под фильтр — индекс становится менее выгодным;
  • вы фильтруете по неравномерной колонке без актуальной статистики.

Поэтому дисциплина такая: сначала приведите WHERE к sargable форме, затем убедитесь, что индекс действительно соответствует фильтру (включая порядок колонок в составных индексах), и только потом обсуждайте настройки/статистику.


Составные индексы и “порядок” колонок: почему иногда индекс не используется

В составных индексах ((a, b, c)) критичен префикс. Часто кажется: “я фильтрую по b, значит индекс должен помочь”. Но если a в предикате отсутствует, индекс может работать хуже или не использоваться.

Типовой принцип B-tree индексов:

  • Если запрос имеет WHERE a = ... и далее AND b = ..., индекс по (a, b) используется эффективно.
  • Если запрос имеет только WHERE b = ... без условия на a, СУБД обычно не сможет нормально воспользоваться индексом (зависит от реализации, но чаще — нет).

Решение — менять индекс под запросы или переписывать запрос так, чтобы условия соответствовали индексной структуре.


Практические рецепты: “переписать WHERE” без магии

Соберём всё в короткий чеклист, который полезно держать рядом при ревью запросов.

Чеклист sargability

  • Нет ли функции на колонке в WHERE (LOWER, DATE, арифметика, каст)?
  • Нет ли явного/неявного приведения типа колонки или значения?
  • Диапазоны по датам заданы как [>= start, < end)?
  • Для LIKE используется префикс prefix%, а не '%suffix'?
  • OR не превращает предикаты в “всё сразу” без альтернативы (иногда UNION ALL лучше)?
  • Предикаты соответствуют составным индексам (есть ли условие по префиксным колонкам)?
  • Есть ли корректные индексы, включая функциональные там, где без функций не обойтись?

Как подойти к теме системно: от типичных ошибок к устойчивым навыкам

Многие разработчики учатся индексации “методом проб”: добавили индекс — запрос стал быстрее/не стал. Это полезно, но не даёт устойчивого понимания, почему именно.

Гораздо надёжнее — сначала освоить форму предикатов и принципы sargability. Когда вы понимаете, какие конструкции ломают поиск по индексу, вы быстрее находите причину медленности и меньше зависите от “угадывания”.

Если вы новичок и вам нужно привести базу по SQL в порядок (включая конструкции WHERE, логические операторы, сравнение с NULL, базовые типы и выражения), стартовать можно с курса формата «SQL – для начинающих!» — он как раз помогает закрыть многие фундаментальные пробелы, которые потом проявляются в производительности запросов. Например, через него удобнее пройти правила построения условий и избежать типичных ошибок, которые мы разобрали выше.


Выводы

Когда запрос медленный “несмотря на индекс”, почти всегда виновата форма WHERE, а не наличие самого индекса. Ключевая идея — sargable условия: предикаты должны позволять оптимизатору превратить фильтр в индексный поиск или индексный диапазон.

Главные источники проблем:

  • функции на колонках (LOWER(col), DATE(created_at)) — превращают запрос в сканирование;
  • приведение типов и неявные преобразования — ломают соответствие типам индекса;
  • вычисления по колонке в предикате — лучше переносить в константы/параметры;
  • неаккуратная фильтрация по датам и времени — заменяйте DATE(col)=... на диапазон;
  • OR и шаблоны LIKE — требуют внимательного рассмотрения, а иногда переписы

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

SQL – для начинающих!

Курс по теме

SQL – для начинающих!

С нуля до уверенных запросов на SQL. Никакой теории без дела — только живые запросы, настоящие данные и готовность к работе с любой базой. Хватит гуглить «как написать JOIN» — разберись один раз и забудь.

Бесплатно
Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Solidity DLC - поймай, если сможешь

Solidity DLC - поймай, если сможешь

Аудит смарт-контрактов на реальных взломах. The DAO, Parity, Wormhole, Ronin, Euler и ещё 7 разборов: что пошло не так, сколько потеряли и что бы спасло. Атаки и защиты на Foundry, план входа в профессию аудитора.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Solidity pro - всё или ничего

Solidity pro - всё или ничего

Как пишут контракты, которые работают в основной сети. Наследование и OpenZeppelin, паттерны продакшена, оптимизация газа, ABI, delegatecall и подписи. 7 модулей, 33 урока, а в финале собственное dApp: мини-DAO с токеном, голосованием и казной.

Перейти к курсу

Другие статьи

SQL: диаграмма жизненного цикла индекса — от EXPLAIN до теста на регрессию производительности
диаграмма

SQL: диаграмма жизненного цикла индекса — от EXPLAIN до теста на регрессию производительности

Покажем практический процесс: как определить, какой индекс реально используется, когда он бесполезен, как проверить влияние после изменений схемы и как не словить деградацию запросов в будущем.

16 сентября 2026 г.
1570
Как читать и писать REST API контракты: статусы, ошибки и схемы ответов
читать

Как читать и писать REST API контракты: статусы, ошибки и схемы ответов

Разберём типовые ошибки в дизайне API и научимся фиксировать контракт: коды HTTP, формат ошибок, версионирование и примеры запросов/ответов.

20 июля 2026 г.
1380
Как выучить программирование самому и бесплатно: с чего начать и в каком порядке
выучить

Как выучить программирование самому и бесплатно: с чего начать и в каком порядке

Разберём понятный маршрут для новичка: как выбрать первый язык, что учить в первую очередь, какие мини-проекты делать и как не забросить занятия. Дам бесплатные ориентиры и план, чтобы знания складывались в портфолио.

21 сентября 2026 г.
1960
Как начать с HTML и сделать первую страницу: пошагово и без сложных слов
html

Как начать с HTML и сделать первую страницу: пошагово и без сложных слов

Разберём, что нужно новичку, чтобы за 1–2 вечера собрать свою первую веб-страницу: структура HTML, базовые теги, ссылки и картинки, плюс простой чек-лист, как понять, что страница работает правильно.

2 ноября 2026 г.
20
Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей
cargo

Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей

Разберём, как организовать Rust workspace, управлять optional features, фиксировать версии библиотек и уменьшать “дрейф” зависимостей при командной разработке и CI.

18 сентября 2026 г.
1960
Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах
такое

Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах

Разберём, из чего состоит веб-страница: что делает HTML (смысл и структура) и что делает CSS (оформление). В конце соберёте мини-страницу и поймёте, что нужно учить в каком порядке.

30 октября 2026 г.
130

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Как оптимизатор “думает” про индексы: sargable против non-sargableМинимальный тест: есть ли индексный поиск?Антипаттерн №1: функции на колонках в WHEREПлохой паттерн: вы меняете значение колонкиИсправление: привести константу, а не колонкуЕсли нужны “функциональные” индексыАнтипаттерн №2: приведение типов (implicit/explicit casts)Пример: строка вместо числаРекомендация: приводите константу к типу колонкиБолее тонкий случай: даты и строкиАнтипаттерн №3: выражения в обеих сторонах сравненияПлохой паттернРецепт: переносите вычисления в константуАнтипаттерн №4: диапазоны “ломающие” условия и неаккуратные сравненияПлохой паттерн: функция по дате/времениПравильный паттерн: диапазон по исходной колонкеАнтипаттерн №5: OR-условия без понимания планаПроблемный запросРецепт: иногда помогает разнести OR на UNION ALLАнтипаттерн №6: неправильный порядок условий — миф и реальностьКак писать WHERE, чтобы условия были sargable: базовые шаблоны1) Равенство по колонке2) Диапазон по “сырой” колонке3) Предикаты с вычислениями — перенос в константы4) Избегайте функций на колонках5) LIKE: индекс может работать, но зависит от префиксаNULL и тройная логика: как не потерять индексный поискПравильно для NULLПлохоLIKE, ILIKE и регистр: что реально с индексамиПроверяем на практике: EXPLAIN как инструмент, а не формальностьИндексы ≠ гарантия: кардинальность, статистика и “не тот” индексСоставные индексы и “порядок” колонок: почему иногда индекс не используетсяПрактические рецепты: “переписать WHERE” без магииЧеклист sargabilityКак подойти к теме системно: от типичных ошибок к устойчивым навыкамВыводы