Оптимизировать выбор всех строк из таблицы на основе результатов из одной и той же таблицы?

#mysql #query-optimization

Вопрос:

Я буду первым, кто признает, что я не силен в SQL (и, вероятно, мне не следует относиться к нему как к текущему файлу журнала), но мне было интересно, могу ли я получить несколько советов для улучшения некоторых медленных запросов…

У меня есть большая таблица mysql с 2 строками, в которой я выполняю два полных поиска таблиц на основе подмножества самых последних данных. Когда я загружаю страницу, содержащую эти запросы, я часто обнаруживаю, что их выполнение занимает несколько секунд, но запросы внутри довольно быстрые.

Советник PMA (предположительно ужасный) в значительной степени бросает на меня всю кухонную раковину, временные таблицы, слишком много видов, объединения без индексов (у меня даже нет объединений?), Чтение с фиксированной позиции, чтение следующей позиции, временные таблицы, записанные на диск… последнее особенно заставляет меня задуматься, не проблема ли это с настройкой, но я играл со всеми ручками и даже заплатил за управляемую услугу, которая, похоже, не помогла.

 CREATE TABLE `archive` (  `id` bigint UNSIGNED NOT NULL,  `ip` varchar(15) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,  `service` enum('ssh','telnet','ftp','pop3','imap','rdp','vnc','sql','http','smb','smtp','dns','sip','ldap') CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,  `hostid` bigint UNSIGNED NOT NULL,  `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;  ALTER TABLE `archive`  ADD PRIMARY KEY (`id`),  ADD KEY `service` (`service`),  ADD KEY `date` (`date`),  ADD KEY `ip` (`ip`),  ADD KEY `date-ip` (`date`,`ip`),  ADD KEY `date-service` (`date`,`service`),  ADD KEY `ip-date` (`ip`,`date`),  ADD KEY `ip-service` (`ip`,`service`),  ADD KEY `service-date` (`service`,`date`),  ADD KEY `service-ip` (`service`,`ip`);  

Добавление индексов определенно помогло (хотя они в 4 раза превышают размер фактических данных), но я не знаю, где я могу оптимизировать дальше. Первоначально я думал о кэшировании результатов подзапроса в php и использовании его дважды для основных запросов, но я не думаю, что у меня будет доступ к результату, как только я закрою подзапрос. Я рассматривал возможность объединения, но они выглядят так, как будто предназначены для 2 или более отдельных таблиц, но подзапрос из одной и той же таблицы, поэтому я не уверен, что это тоже сработает. Предполагается, что запросы должны находить наиболее активные ip/службы на основе наличия у меня данных с ip-адреса за последние 24 часа…

 SELECT service, COUNT(service) AS total FROM `archive` WHERE ip IN (SELECT DISTINCT ip FROM `archive` WHERE date gt; DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR)) GROUP BY service HAVING total gt; 1 ORDER BY total DESC, service ASC LIMIT 10   ---- -------------- ----------------- ------------ ------- ---------------------------------------------------------------------------- ------------ --------- ------------------------ ------- ---------- ---------------------------------  | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |  ---- -------------- ----------------- ------------ ------- ---------------------------------------------------------------------------- ------------ --------- ------------------------ ------- ---------- ---------------------------------  | 1 | SIMPLE | lt;subquery2gt; | NULL | ALL | NULL | NULL | NULL | NULL | NULL | 100.00 | Using temporary; Using filesort | | 1 | SIMPLE | archive | NULL | ref | service,ip,date-service,ip-date,ip-service,service-date,service-ip | ip-service | 47 | lt;subquery2gt;.ip | 5 | 100.00 | Using index | | 2 | MATERIALIZED | archive | NULL | range | date,ip,date-ip,date-service,ip-date,ip-service | date-ip | 5 | NULL | 44246 | 100.00 | Using where; Using index |  ---- -------------- ----------------- ------------ ------- ---------------------------------------------------------------------------- ------------ --------- ------------------------ ------- ---------- ---------------------------------    
 SELECT ip, COUNT(ip) AS total FROM `archive` WHERE ip IN (SELECT DISTINCT ip FROM `archive` WHERE date gt; DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR)) GROUP BY ip HAVING total gt; 1 ORDER BY total DESC, INET_ATON(ip) ASC LIMIT 10   ---- -------------- ----------------- ------------ ------- --------------------------------------------------------------- --------- --------- ------------------------ ------- ---------- ---------------------------------  | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |  ---- -------------- ----------------- ------------ ------- --------------------------------------------------------------- --------- --------- ------------------------ ------- ---------- ---------------------------------  | 1 | SIMPLE | lt;subquery2gt; | NULL | ALL | NULL | NULL | NULL | NULL | NULL | 100.00 | Using temporary; Using filesort | | 1 | SIMPLE | archive | NULL | ref | ip,date-ip,ip-date,ip-service,service-ip | ip-date | 47 | lt;subquery2gt;.ip | 5 | 100.00 | Using index | | 2 | MATERIALIZED | archive | NULL | range | date,ip,date-ip,date-service,ip-date,ip-service | date-ip | 5 | NULL | 44168 | 100.00 | Using where; Using index |  ---- -------------- ----------------- ------------ ------- --------------------------------------------------------------- --------- --------- ------------------------ ------- ---------- ---------------------------------    

общий подзапрос: 0,0351 с

весь запрос 1: 1.4270 с

весь запрос 2: 1.5601 с

общая загрузка страницы: 3,050 с (всего 7 запросов)

Неужели я просто обречен на ужасную производительность с этим столом?

Надеюсь, здесь достаточно информации, чтобы понять, что происходит, но если кто-нибудь может помочь, я, безусловно, буду признателен. Я не возражаю против того, чтобы использовать больше оборудования для решения этой проблемы, но когда сервер 8c/16t с 16 ГБ не может обрабатывать 150 МБ данных, я не уверен, что это произойдет. Заранее спасибо, что прочитали мой длинный вопрос.

Комментарии:

1. Что explain показывает ан? Какова цель запроса? Это не имеет никакого отношения к PHP, кроме, возможно, phpmyadmin, предполагающего, что это и есть PMA? Даже в этом случае это просто пользовательский интерфейс для доступа к базе данных mysql.

2. Также не уверен в цели INET_ATON(ip) пребывания в группе by. Это не позволит использовать индекс, так как все записи будут нуждаться в преобразовании.

3. Привет, пользователь3783243, я добавил дополнительную информацию по запросу. Чтобы ответить на ваш вопрос о INET_ATON, он «упорядочивает» адреса по алфавиту. Спасибо

4. Использование IN с подзапросом, как известно, является чем-то вроде повышения производительности. Вы можете JOIN вернуть таблицу к самой себе, или, более конкретно, здесь вы можете JOIN вернуть свой подзапрос к таблице, из которой он был получен.

5. Кстати, вы уверены, что у вас есть пункт » ГДЕ » в нужном месте? В настоящее время ваш запрос будет искать IP-адрес за последние 24 часа, а затем вернет все данные в таблице для этих адресов. Если вам просто нужна сводка за последние 24 часа, вы можете удалить подзапрос и выбрать по дате только в основном запросе.

Ответ №1:

У вас есть правильные индексы (а также многие другие индексы), и ваш запрос соответствует вашим спецификациям и работает почти оптимально. Маловероятно, что вы сможете сделать это намного быстрее: он должен полностью вернуться к началу вашего стола.

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

Некоторые возможные незначительные изменения.

  • используйте параметры latin1_bin сортировки для своей ip колонки. Он использует 8-битные символы и сопоставляет их без учета регистра. Этого достаточно для адресов с точечными квадратами IPv4 (и адресов IPv6). Вы избавитесь от некоторых накладных расходов при сопоставлении и группировке. Или, что еще лучше,
  • Если вы знаете, что у вас не будет ничего, кроме адресов IPv4, переработайте свой ip столбец, чтобы сохранить их двоичные представления ( то есть INET_ATON() сгенерированное значение каждого IPv4). Вы можете поместить их в UNSIGNED INT 32-разрядный целочисленный тип данных, что сделает поиск, группировку и упорядочение еще быстрее.

Можно переработать способ сбора этих данных. Например, вы можете организовать сбор не более одной строки на службу в день. Это уменьшит разрешение временных рядов ваших данных, но также значительно ускорит выполнение ваших запросов. Определите свою таблицу примерно так:

 CREATE TABLE archive2 (  ip VARCHAR(15) COLLATE latin1_bin NOT NULL,  service ENUM ('ssh','telnet','ftp',  'pop3','imap','rdp',  'vnc','sql','http','smb',  'smtp','dns','sip','ldap') COLLATE NOT NULL,  `date` DATE NOT NULL,  `count` INT NOT NULL,  hostid bigint UNSIGNED NOT NULL,  PRIMARY KEY (`date`, ip, service) ) ENGINE=InnoDB;  

Затем, когда вы вставляете строку, используйте этот запрос:

 INSERT INTO archive2 (`date`, ip, service, `count`, hostid)  VALUES (CURDATE(), ?ip, ?service, 1, ?hostid) ON DUPLICATE KEY UPDATE  SET count = count   1;  

Это автоматически увеличит ваш count столбец, если строка для ip , service , и date уже существует.

Тогда ваш второй запрос будет выглядеть так:

 SELECT ip, SUM(`count`) AS total  FROM archive   WHERE ip IN (  SELECT ip FROM archive   WHERE `date` gt; CURDATE() - INTERVAL 1 DAY  GROUP BY ip  HAVING total gt; 1  ) ORDER BY total DESC, INET_ATON(ip) ASC LIMIT 10;  

Индекс первичного ключа удовлетворит этому запросу.

Комментарии:

1. Привет, к сожалению, сокращение данных таким образом на самом деле не было бы возможным, так как я потерял бы много информации, не имея строки для каждого инцидента и полной отметки времени. У меня была аналогичная настройка в тестировании, где у меня было значение счетчика, и я бы ударил по метке времени вместо вставки новых строк. Спасибо, что взглянули, хотя я мог бы попробовать изменить типы данных в ближайшем будущем, так как слышал, что VARCHAR(15) плохо подходит для хранения IP-адресов.

2. Изменение кодировки/параметров сортировки перечисления не влияет на то, что хранится (небольшое число), но может привести к ошибкам сортировки при выполнении запроса (параметры сортировки соединений по сравнению с параметрами сортировки столбцов).

3. Понял, @RickJames. Отредактировал мой ответ.

Ответ №2:

Первый запрос

(Я не уверен, что это можно сделать намного быстрее.)

(в настоящее время)

 SELECT service, COUNT(service) AS total  FROM `archive`  WHERE ip IN (  SELECT DISTINCT ip  FROM `archive`  WHERE date gt; DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR)  )  GROUP BY service  HAVING total gt; 1  ORDER BY total DESC, service ASC  LIMIT 10  

Примечания:

  • COUNT(service) —gt; COUNT(*)
  • DISTINCT не требуется в IN (SELECT DISTINCT ...)
  • IN ( SELECT ... ) часто работает медленно; перепишите с помощью EXISTS ( SELECT 1 ... ) или JOIN (см. Ниже)
  • INDEX(date, IP) — для подзапроса
  • INDEX(service, IP) — для вашего внешнего запроса
  • INDEX(IP, service) — для моего внешнего запроса
  • Выбросьте избыточные индексы; они могут помешать. (См. ниже)
  • Ему придется собрать все возможные результаты, прежде чем приступить к ORDER BY и LIMIT . (То есть LIMIT очень мало влияет на производительность этого запроса.)
  • CHARACTER SET utf8 COLLATE utf8_unicode_ci является грубым перебором для IP-адресов; переключитесь на CHARACTER SET ascii COLLATE ascii_bin .
  • Если вы используете MySQL 8.0 (или MariaDB 10.2), a WITH для вычисления подзапроса один раз вместе с a UNION для вычисления двух внешних запросов может обеспечить некоторую дополнительную скорость.
  • В MariaDB есть «кэш подзапросов», который может привести к пропуску оценки второго подзапроса.
  • Используя DATETIME вместо TIMESTAMP этого , вы столкнетесь с двумя незначительными сбоями в год, когда начнется/закончится летнее время.
  • Я сомневаюсь, что hostid это должно быть BIGINT (8 байт).

Чтобы переключиться на a JOIN , подумайте о том, чтобы сначала выбрать строки-кандидаты:

 SELECT service, COUNT(*) AS total  FROM ( SELECT DISTINCT IP  FROM archive  WHERE `date` gt; NOW() - INTERVAL 24 HOUR  ) AS x  JOIN archive USING(IP)  GROUP BY service  HAVING total gt; 1  ORDER BY total DESC, service ASC  LIMIT 10  

Для дальнейшего обсуждения любого медленного (но рабочего) запроса, пожалуйста, предоставьте оба варианта EXPLAIN :

 EXPLAIN SELECT ... EXPLAIN FORMAT=JSON SELECT ...  

Отбросьте эти индексы:

 ADD KEY `service` (`service`),  ADD KEY `date` (`date`),  ADD KEY `ip` (`ip`),  

Рекомендую только

 ADD PRIMARY KEY (`id`),  -- as discussed:  ADD KEY `date-ip` (`date`,`ip`),  ADD KEY `ip-service` (`ip`,`service`),  ADD KEY `service-ip` (`service`,`ip`),  -- maybe other queries need these:  ADD KEY `date-service` (`date`,`service`),  ADD KEY `ip-date` (`ip`,`date`),  ADD KEY `service-date` (`service`,`date`),  

Общее правило здесь заключается в том, что вам не нужно INDEX(a) , когда у вас тоже есть INDEX(a,b) . В частности, они могут препятствовать использованию лучших индексов; см. EXPLAINs .

Второй запрос

Перепишите его

 SELECT ip, COUNT(DISTINCT ip) AS total  FROM `archive`  WHERE date gt; DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR)  GROUP BY ip  HAVING total gt; 1  ORDER BY total DESC, INET_ATON(ip) ASC  LIMIT 10   

Он будет использоваться только INDEX(date, ip) .

Комментарии:

1. Привет, Рик, к сожалению, это соединение в два раза медленнее, и ваш второй запрос даже не возвращает никаких строк вообще. Извините.

2. Похоже, что ваш второй запрос также не может создать никаких строк.