Как изменить сам запрос или DDL, чтобы ускорить выборку по диапазону дат?

Рейтинг: 2Ответов: 1Опубликовано: 11.07.2023

MySQL 8.0.33. Имеется таблица (убрал незначащие поля в контексте вопроса):

CREATE TABLE `data` (
  `id` int NOT NULL AUTO_INCREMENT,
  `period` date DEFAULT NULL,
  `h_sum` double DEFAULT '0',
  `hw_sum` double DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `period` (`period`),
) ENGINE=InnoDB;

куда каждый месяц (поле period хранит дату = 1 число этого месяца) записываются несколько десятков тысяч записей. Сейчас в ней данные за 62 месяца, 2016426 записей. Один из запросов должен подсчитывать сумму h_sum+hw_sum за несколько месяцев (помесячно). Если так:

SELECT period, SUM(h_sum+hw_sum) paid
FROM data
WHERE period BETWEEN '2023-01-01' AND '2023-07-01'
GROUP BY period
ORDER BY period;

запрос выполняется 19 сек! Его EXPLAIN:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE data \N index period period 4 \N 1934197 33,18 Using where

Если переписать его с использованием оконных функций:

SELECT DISTINCT period, SUM(h_sum+hw_sum) OVER (PARTITION BY period ORDER BY period) paid
FROM data
WHERE period BETWEEN '2023-01-01' AND '2023-07-01'

то время выполнения уменьшается до 8 сек, но перестает использоваться индекс, EXPLAIN:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE data \N ALL period \N \N \N 1934197 33,18 Using where; Using filesort

Если принудить использовать индекс:

SELECT DISTINCT period, SUM(h_sum+hw_sum) OVER (PARTITION BY period ORDER BY period) paid
FROM data FORCE INDEX (period)
WHERE period BETWEEN '2023-01-01' AND '2023-07-01'

то время уменьшается до 4 сек, но EXPLAIN (поле extra) становится совсем плохим:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE data \N range period period 4 \N 641720 100,00 Using index condition; Using MRR; Using filesort

Вопрос: можно ли еще как-то оптимизировать/изменить запрос для ускорения времени выполнения (чем больше месяцев в выборке, тем дольше считается)? Может изменить тип поля? На что? Индекс? Структуру таблицы? Еще что-то?

Доп. вопрос: так ли нужен ORDER BY period, если данные записываются всегда последовательно по месяцам (т.е., к примеру, данные января 2023 не могут быть записаны ранее декабря 2022)? Если убрать его из запросов, то ничего не меняется: ни порядок месяцев в выборке, ни скорость выполнения...

Ответы

▲ 0Принят

Селективность обычного индекса period

Индекс в запросе не особенно помогает, так как 641720 строк - это треть всех данных таблицы (EXPLAIN: 641720/2016426 ~ 1/3. Или колонка filtered 33,18 - там где с индексом). То есть селективность 1/3 - маленькая.

Для получения значений h_sum и hw_sum все равно берутся данные собственно из файла данных, а не из файла индекса - то есть происходит дополнительное чтение с диска.

Оптимизатор думает:

  1. прочитать малоселективный индекс, а затем по ссылкам из него дополнительно прочитать h_sum + hw_sum из файла данных

или

  1. прочитать только файл данных, по ходу фильтруя данные

И выбирает второй вариант. И не особенно ошибается со своим выбором - разница всего 19/8 ~ 2 раза. Обычно индексы работают многократно быстрее.

Как можно улучшить? Сделать покрывающий индекс или делать предварительные расчеты. Поправить тип period.

Покрывающий индекс

Можете сделать один индекс сразу по нескольким полям: period, h_sum, hw_sum - он станет покрывающим в данном конкретном запросе. Порядок полей важен. Тогда будет использоваться исключительно индекс.

Мне трудно сказать, насколько это увеличит скорость, так как все равно много данных. А должно стать быстрее банально из-за того, что индекс компактнее чем все данные.

Попробую посчитать.

Получение данных из покрывающего индекса. Допустим 2016426 записей. Размер одной записи: date + 2 * double + PK. Я не стал искать точные размеры, думаю в 100 байт укладывается. Получилось 192 Мб. Чтение с диска займет доли секунды.

Также БД придется сделать суммирование 641720 * 2 = 1283440 раз. Если одна операция сложения double это 1-3 такта, то на 2 ГГц процессоре потратится 1283440÷2000000000 = 0,0006 сек. CPU делает и другие операции типа записи в кеш L1, я их не стал тут учитывать. Если я правильно рассчитываю, этим временем вообще можно пренебречь.

В итоге покрывающий индекс должен существенно помочь. Но это не точно.

Предварительная агрегация

OLTP - обработка транзакций в реальном времени. Типичный веб-сайт, где важно время отклика. OLAP - аналитика. Она всегда долгая из-за агрегации.

Хотите OLAP в реальном времени? Делайте предварительные расчеты и используйте простой SELECT по результатам.

Варианты:

  • Сделать агрегацию средствами своего языка программирования и хранить в отдельной таблице. Или вообще в Redis кешировать.
  • Настроить триггеры: при любой вставке/обновлении/удалении данных в таблицу data сама БД будет триггериться и обновлять таблицу аналитики типа data_by_period.

NOT NULL

period date DEFAULT NULL - я везде читал, что NOT NULL быстрее. Но разница может быть ничтожная. Пробуйте.