Django медленное внутреннее соединение в таблице с более чем 10 миллионами записей

Я использую mysql с Django. Я пытаюсь подсчитать количество visitor_pages для определенного дилера за определенный промежуток времени. Я хотел бы поделиться необработанным sql-запросом, который я получил из панели инструментов отладки django.

SELECT COUNT(*) AS `__count`
  FROM `visitor_page`
INNER JOIN `dealer_visitors`
    ON (`visitor_page`.`dealer_visitor_id` = `dealer_visitors`.`id`)
WHERE (`visitor_page`.`date_time` BETWEEN '2021-02-01 05:51:00' AND '2021-03-21 05:50:00' AND `dealer_visitors`.`dealer_id` = 15)

Проблема в том, что у меня более 13 миллионов записей в таблице visitor_pages и около 1,5 миллиона записей в таблице dealer_visitor. Я уже проиндексировал date_time. Я думаю использовать материализованное представление, но прежде чем приступить к этому, я был бы очень признателен за предложения о том, как я могу улучшить этот запрос.

visitor_pages schema:

CREATE TABLE `visitor_page` (
  `id` int NOT NULL AUTO_INCREMENT,
  `date_time` datetime(6) DEFAULT NULL,
  `added_at` datetime(6) DEFAULT NULL,
  `updated_at` datetime(6) DEFAULT NULL,
  `page_id` int NOT NULL,
  `dealer_visitor_id` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `visitor_page_page_id_246babdf_fk_web_page_id` (`page_id`),
  KEY `visitor_page_dealer_visitor_id_e2dddea2_fk_dealer_visitors_id` (`dealer_visitor_id`),
  KEY `visitor_page_date_time_06e9e9f5` (`date_time`),
  CONSTRAINT `visitor_page_dealer_visitor_id_e2dddea2_fk_dealer_visitors_id` FOREIGN KEY (`dealer_visitor_id`) REFERENCES `dealer_visitors` (`id`),
  CONSTRAINT `visitor_page_page_id_246babdf_fk_web_page_id` FOREIGN KEY (`page_id`) REFERENCES `web_page` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13626649 DEFAULT CHARSET=latin1;

dealer_visitors schema:

CREATE TABLE `dealer_visitors` (
  `id` int NOT NULL AUTO_INCREMENT,
  `visit_date` datetime(6) DEFAULT NULL,
  `added_at` datetime(6) DEFAULT NULL,
  `updated_at` datetime(6) DEFAULT NULL,
  `dealer_id` int NOT NULL,
  `visitor_id` int NOT NULL,
  `type` int DEFAULT NULL,
  `notes` longtext,
  `location` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `dealer_visitors_dealer_id_306e2202_fk_dealer_id` (`dealer_id`),
  KEY `dealer_visitors_visitor_id_27ae498e_fk_visitor_id` (`visitor_id`),
  KEY `dealer_visitors_type_af0f7d79` (`type`),
  KEY `dealer_visitors_visit_date_f2b138c9` (`visit_date`),
  CONSTRAINT `dealer_visitors_dealer_id_306e2202_fk_dealer_id` FOREIGN KEY (`dealer_id`) REFERENCES `dealer` (`id`),
  CONSTRAINT `dealer_visitors_visitor_id_27ae498e_fk_visitor_id` FOREIGN KEY (`visitor_id`) REFERENCES `visitor` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1524478 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

EXPLAIN ANALYZE запрос дает мне следующее: EXPLAIN ANALYZE

ОБЪЯСНИТЕ: enter image description here

Для этого запроса:

SELECT COUNT(*) AS `__count`
FROM visitor_page vp JOIN
     dealer_visitors dv
     ON vp.dealer_visitor_id = dv.id
WHERE vp.date_time BETWEEN '2021-02-01 05:51:00' AND '2021-03-21 05:50:00' AND
     dv.dealer_id = 15;

Лучшие индексы находятся на dealer_visitors(dealer_id, date_time, id) и visitor_page(dealer_visitor_id).

Индекс только на date немного помогает. Но вы извлекаете данные за месяц, и это может быть очень много данных для обработки. Наличие dealer_id в качестве первого столбца в индексе ограничит данные только строками для этого дилера за этот период времени.

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

   ON `visitor_page`.`dealer_visitor_id` = `dealer_visitors`.`id`
WHERE `visitor_page`.`date_time` BETWEEN ...
  AND `dealer_visitors`.`dealer_id` = 15

Начиная с visitor_page:

 visitor_page:  INDEX(date_time)   -- (already exists)
 dealer_visitors:  (already has PRIMARY KEY(id))

Начиная с dealer_visitors:

 dealer_visitors:  INDEX(dealer_id)   -- (already exists)
 visitor_page:  INDEX(dealer_visitor_id, date_time)  -- in this order

и исключить dealer_visitors_visitor_id_27ae498e_fk_visitor_id как излишний.

В чистом виде добавляется один индекс и отбрасывается один индекс.

Материализованное представление -- Часто для отчетов Хранилища данных лучше всего создавать и постепенно поддерживать "сводную таблицу" ("материализованное представление"). Очень странный диапазон дат (1 месяц + 20 дней - 61 секунда) делает это неудобным. Обычно удобно делать таблицу на основе целых дней. Если вы можете перейти на ежедневный (или ежечасный), то см. http://mysql.rjweb.org/doc.php/summarytables

Еще кое-что нужно проверить: Сколько у вас оперативной памяти? Что говорит SHOW VARIABLES LIKE 'innodb_buffer_pool_size';?

Я вижу, что таблицы имеют разную кодировку/колляцию. Это не является проблемой для данного запроса, но если у вас есть другие запросы, которые JOIN на VARCHARs, проверьте, что они используют одинаковый collation.

Вернуться на верх