Разберите SQL-кейс: почему запрос SELECT * FROM orders WHERE order_date > '2020-01-01' не использует индекс по order_date и как это исправить на уровне схемы и запроса?
Коротко — причины и способы исправления. Почему индекс по order_date\text{order\_date}order_date может не использоваться - На колонке применяется функция/выражение, например DATE(order_date)>′2020−01−01′\text{DATE(order\_date)} > '2020-01-01'DATE(order_date)>′2020−01−01′. Индексы на исходную колонку тогда не работают. - Типы не совпадают / происходит приведение (implicit cast), что может помешать использованию индекса. - Индекс отсутствует или он составной, где order_date\text{order\_date}order_date не стоит в левом (leading) положении — тогда индекс не годится для данного предиката. - Низкая селективность: если большая часть строк удовлетворяет order_date>′2020−01−01′\text{order\_date} > '2020-01-01'order_date>′2020−01−01′, оптимизатор предпочитает полный скан. - Отсутствие покрытия: вы пишете SELECT *\text{SELECT *}SELECT *, чтение по вторичному индексу приведёт к множественным random I/O, и оптимизатор может выбрать full table scan. - Статистика устарела — оптимизатор неверно оценивает стоимость. Как исправить на уровне схемы - Добавить простой индекс: CREATE INDEX idx_orders_order_date ON orders(order_date); - Если часто выбираете только несколько полей, сделать покрывающий индекс (DB-specific): - MySQL/Postgres: CREATE INDEX idx ON orders(order_date, other_col); - SQL Server: CREATE NONCLUSTERED INDEX idx ON orders(order_date) INCLUDE (other_col1, other_col2); - Если запрос использует функцию над датой, создать expression/functional индекс или генерируемую колонку и индексировать её: - Postgres: CREATE INDEX idx_date_trunc ON orders ((date(order_date))); - MySQL (если нет expression index): ALTER TABLE orders ADD COLUMN order_date_only DATE GENERATED ALWAYS AS (DATE(order_date)) STORED; CREATE INDEX idx_order_date_only ON orders(order_date_only); - Убедиться, что тип колонки — DATE/DATETIME/TIMESTAMP, а не текстовый, чтобы избежать неявных преобразований. - Обновить статистику: ANALYZE / VACUUM ANALYZE / UPDATE STATISTICS. Как исправить на уровне запроса - Не оборачивать колонку в функции; писать прямо: order_date>′2020−01−01′\text{order\_date} > '2020-01-01'order_date>′2020−01−01′ (или явно привести литерал к типу даты: order_date>DATE′2020−01−01′\text{order\_date} > \text{DATE} '2020-01-01'order_date>DATE′2020−01−01′ в Postgres). - Если нужно сравнивать по дате без времени, используйте либо границы (например order_date≥′2020−01−01′∧order_date<′2020−01−02′\text{order\_date} \ge '2020-01-01' \land \text{order\_date} < '2020-01-02'order_date≥′2020−01−01′∧order_date<′2020−01−02′), либо индексированную генерируемую колонку/функциональный индекс. - Ограничить вывод колонок, чтобы можно было использовать covering index вместо SELECT *\text{SELECT *}SELECT *. - При сомнениях — посмотреть план: EXPLAIN SELECT ... и при необходимости проанализировать/обновить статистику. Примеры - Создать индекс: CREATE INDEX idx_orders_order_date ON orders(order_date); - Переписать запрос (не оборачивая колонку): SELECT * FROM orders WHERE order_date>′2020−01−01′\text{order\_date} > '2020-01-01'order_date>′2020−01−01′. - Если раньше был WHERE DATE(order_date) > ’2020-01-01’\text{WHERE DATE(order\_date) > '2020-01-01'}WHERE DATE(order_date) > ’2020-01-01’, сделать генерируемую колонку + индекс и писать по ней. Рекомендация: сначала проверить EXPLAIN, затем добавить/перестроить индекс и обновить статистику; если селективность всё ещё низкая — индекс может и не помочь.
Почему индекс по order_date\text{order\_date}order_date может не использоваться
- На колонке применяется функция/выражение, например DATE(order_date)>′2020−01−01′\text{DATE(order\_date)} > '2020-01-01'DATE(order_date)>′2020−01−01′. Индексы на исходную колонку тогда не работают.
- Типы не совпадают / происходит приведение (implicit cast), что может помешать использованию индекса.
- Индекс отсутствует или он составной, где order_date\text{order\_date}order_date не стоит в левом (leading) положении — тогда индекс не годится для данного предиката.
- Низкая селективность: если большая часть строк удовлетворяет order_date>′2020−01−01′\text{order\_date} > '2020-01-01'order_date>′2020−01−01′, оптимизатор предпочитает полный скан.
- Отсутствие покрытия: вы пишете SELECT *\text{SELECT *}SELECT *, чтение по вторичному индексу приведёт к множественным random I/O, и оптимизатор может выбрать full table scan.
- Статистика устарела — оптимизатор неверно оценивает стоимость.
Как исправить на уровне схемы
- Добавить простой индекс:
CREATE INDEX idx_orders_order_date ON orders(order_date);
- Если часто выбираете только несколько полей, сделать покрывающий индекс (DB-specific):
- MySQL/Postgres: CREATE INDEX idx ON orders(order_date, other_col);
- SQL Server: CREATE NONCLUSTERED INDEX idx ON orders(order_date) INCLUDE (other_col1, other_col2);
- Если запрос использует функцию над датой, создать expression/functional индекс или генерируемую колонку и индексировать её:
- Postgres: CREATE INDEX idx_date_trunc ON orders ((date(order_date)));
- MySQL (если нет expression index): ALTER TABLE orders ADD COLUMN order_date_only DATE GENERATED ALWAYS AS (DATE(order_date)) STORED; CREATE INDEX idx_order_date_only ON orders(order_date_only);
- Убедиться, что тип колонки — DATE/DATETIME/TIMESTAMP, а не текстовый, чтобы избежать неявных преобразований.
- Обновить статистику: ANALYZE / VACUUM ANALYZE / UPDATE STATISTICS.
Как исправить на уровне запроса
- Не оборачивать колонку в функции; писать прямо: order_date>′2020−01−01′\text{order\_date} > '2020-01-01'order_date>′2020−01−01′ (или явно привести литерал к типу даты: order_date>DATE′2020−01−01′\text{order\_date} > \text{DATE} '2020-01-01'order_date>DATE′2020−01−01′ в Postgres).
- Если нужно сравнивать по дате без времени, используйте либо границы (например order_date≥′2020−01−01′∧order_date<′2020−01−02′\text{order\_date} \ge '2020-01-01' \land \text{order\_date} < '2020-01-02'order_date≥′2020−01−01′∧order_date<′2020−01−02′), либо индексированную генерируемую колонку/функциональный индекс.
- Ограничить вывод колонок, чтобы можно было использовать covering index вместо SELECT *\text{SELECT *}SELECT *.
- При сомнениях — посмотреть план: EXPLAIN SELECT ... и при необходимости проанализировать/обновить статистику.
Примеры
- Создать индекс: CREATE INDEX idx_orders_order_date ON orders(order_date);
- Переписать запрос (не оборачивая колонку): SELECT * FROM orders WHERE order_date>′2020−01−01′\text{order\_date} > '2020-01-01'order_date>′2020−01−01′.
- Если раньше был WHERE DATE(order_date) > ’2020-01-01’\text{WHERE DATE(order\_date) > '2020-01-01'}WHERE DATE(order_date) > ’2020-01-01’, сделать генерируемую колонку + индекс и писать по ней.
Рекомендация: сначала проверить EXPLAIN, затем добавить/перестроить индекс и обновить статистику; если селективность всё ещё низкая — индекс может и не помочь.