Составной индекс SQLite: как увидеть, что он помогает фильтру клиента и даты
Отчёт считает сумму заказов клиента за месяц. После добавления индекса по created_at и client_id время почти не изменилось. Само наличие обоих полей в индексе ещё не говорит, какую часть таблицы SQLite сможет быстро найти. Для запроса client_id = 7 и диапазона дат полезен порядок, при котором сначала идут заказы клиента, а внутри них — даты.
Почему равенство идёт перед диапазоном
В индексе (client_id, created_at) записи сначала сгруппированы по клиенту, затем упорядочены по дате. SQLite находит группу клиента 7 и читает нужный отрезок дат. При обратном порядке поиск по датам сначала охватит заказы всех клиентов за месяц, а client_id придётся проверять внутри этого отрезка. Это рассуждение о конкретном запросе; другой запрос может потребовать другого порядка полей.
Проверять выбор оптимизатора удобно через EXPLAIN QUERY PLAN. В описании плана SCAN означает проход по таблице или индексу, а SEARCH — отбор части строк. После создания подходящего индекса ищите имя индекса и оба использованных условия: равенство клиента и границы даты. Не привязывайте скрипты к точной формулировке строки плана — она меняется между версиями SQLite.
Сравнение на отдельной учебной таблице
Следующий SQL создаёт 10 000 учебных заказов. Запустите его в пустой SQLite-базе: он сначала покажет план без индекса, затем создаст индекс и покажет план того же запроса. Последний SELECT возвращает результат отчёта, чтобы мы проверяли и скорость поиска, и смысл запроса.
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
client_id INTEGER NOT NULL,
created_at TEXT NOT NULL,
amount INTEGER NOT NULL
);
WITH RECURSIVE seq(n) AS (
VALUES (0)
UNION ALL
SELECT n + 1 FROM seq WHERE n < 9999
)
INSERT INTO orders(client_id, created_at, amount)
SELECT n % 100, date('2026-09-01', printf('+%d days', n % 90)), 100
FROM seq;
EXPLAIN QUERY PLAN
SELECT SUM(amount) FROM orders
WHERE client_id = 7
AND created_at >= '2026-10-01'
AND created_at < '2026-11-01';
CREATE INDEX idx_orders_client_date ON orders(client_id, created_at);
EXPLAIN QUERY PLAN
SELECT SUM(amount) FROM orders
WHERE client_id = 7
AND created_at >= '2026-10-01'
AND created_at < '2026-11-01';
SELECT SUM(amount) AS october_total FROM orders
WHERE client_id = 7
AND created_at >= '2026-10-01'
AND created_at < '2026-11-01';
В локальной проверке до индекса план содержит SCAN orders, после — SEARCH orders USING INDEX idx_orders_client_date с условиями по клиенту и дате. Итоговая сумма равна 3300; её вычисляет последний запрос, а не план. План изменился. Время запроса сравните на своей нагрузке: на 10 000 строках этой демонстрации разница может быть мала.
Что проверить на своей таблице
Сначала сохраните исходный план и результат отчёта для одинаковых параметров. Затем проверьте индекс на копии данных с похожим распределением клиентов и дат. Сравните новый план, результат и время нескольких прогонов. Если после создания индекса результат изменился, проблема не в выборе доступа: найдите различие в запросе или данных. Если план стал лучше, а время нет, возможно, отбор возвращает слишком большую долю таблицы или основное время уходит на чтение amount и агрегацию.
В нашем индексе amount отсутствует, поэтому для суммы SQLite читает значение из таблицы. Добавление третьей колонки иногда позволяет обойтись индексом, но увеличивает размер индекса и стоимость вставок. Такой вариант имеет смысл проверять измерением на рабочем типе нагрузки. Запрос только по дате также не использует левый край индекса (client_id, created_at) так же, как запрос по клиенту; для него нужен отдельный анализ плана.
Фильтр месяца задан как >= 1 октября и < 1 ноября, чтобы не терять вечер последнего дня при хранении времени. Здесь даты в учебной таблице текстовые и нормализованы. Правило соседних границ отдельно разобрано в статье о SQL-фильтре за месяц.
Продолжение практики
В платном курсе «Продвинутый SQL» вы разберёте планы выполнения SQLite, составные и покрывающие индексы, а затем будете переписывать условия с проверкой того, что результат не изменился. Эти навыки понадобятся и в итоговой аналитической витрине интернет-магазина. Откройте задачу «Закрытые заявки за период» и посмотрите программу курса.
Другие разборы запросов собраны в разделе аналитического SQL; если важен подсчёт периодов, пригодится пример загрузки гостиницы.
