← все задачи

SQL · задача 8 из 10

Почему запрос медленный

Продвинутый 20 минут индексыEXPLAINsargable-условия

Условие

Дан запрос, который на проде выполняется двадцать секунд. Найдите проблемы и предложите исправления: SELECT * FROM orders WHERE DATE(created_at) = 2026-09-01 AND LOWER(status) = 'new' ORDER BY created_at DESC LIMIT 20;

Что требуется

  • Назвать все причины медленной работы
  • Переписать запрос
  • Предложить индекс и объяснить порядок колонок в нём

Пример

-- было: 20 секунд, seq scan по 50 млн строк
-- стало: 3 мс, index scan по 20 строкам

Сначала уточните

Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.

  • Какова кардинальность status — много ли строк со значением new?
  • Насколько велика таблица и как быстро растёт?
  • Нужны ли действительно все колонки или хватит нескольких?
Показать решение Скрыть решение

Решение

-- Переписанный запрос: колонки «голые», условие по диапазону
SELECT id, customer_id, status, created_at, total
FROM orders
WHERE created_at >= DATE '2026-09-01'
  AND created_at <  DATE '2026-09-02'
  AND status = 'new'
ORDER BY created_at DESC
LIMIT 20;

-- Индекс: сначала равенство, потом диапазон и сортировка
CREATE INDEX CONCURRENTLY orders_status_created_at_idx
    ON orders (status, created_at DESC);

-- Проверяем, что планировщик им пользуется
EXPLAIN (ANALYZE, BUFFERS)
SELECT ...;

Почему так

Почему функция над колонкой убивает индекс

  • DATE(created_at) и LOWER(status) — это вычисления над каждой строкой, обычный индекс к ним неприменим
  • Планировщику приходится читать всю таблицу, чтобы посчитать функцию, — отсюда seq scan на 50 млн строк
  • Лечится переписыванием на диапазон либо функциональным индексом по тому же выражению

Почему в индексе сначала status, потом created_at

  • В составном индексе сначала идут колонки с равенством, потом та, по которой диапазон и сортировка
  • При обратном порядке диапазон по дате «съест» индекс, и фильтр по статусу придётся применять к каждой найденной строке
  • С порядком (status, created_at DESC) запрос берёт из индекса сразу двадцать нужных строк и не сортирует ничего

Чем плох SELECT *

  • Лишние колонки — это лишние страницы с диска и лишний трафик, особенно если в таблице есть text или jsonb
  • С узким списком колонок возможен index-only scan: ответ берётся из индекса, без похода в таблицу
  • И отдельно: SELECT * ломается при изменении схемы, потому что клиент полагается на порядок колонок

Что смотреть в EXPLAIN

  • ANALYZE даёт реальное время и число строк: разница между ожидаемым и фактическим показывает, что статистика устарела
  • BUFFERS показывает, сколько страниц прочитано — именно чтение обычно и есть время запроса
  • Ищем Seq Scan на большой таблице, Sort с внешней сортировкой на диск и вложенные циклы с большим числом итераций

Что спросят дальше

  • Спросят, когда индекс не поможет: если условию удовлетворяет половина таблицы, seq scan честно быстрее
  • Спросят про цену индексов: каждый замедляет вставку и занимает место, поэтому «добавить индекс на всё» — не ответ
  • Спросят про CONCURRENTLY: обычный CREATE INDEX блокирует запись в таблицу на всё время построения

Следующая задача

Вставить или обновить без гонок — Счётчик, который увеличивают два процесса одновременно: классическая потерянная запись.