Оптимизация необъяснимо медленного запроса MySQL - PullRequest
3 голосов
/ 10 февраля 2012

Я теряю волосы по глупому запросу.Сначала я бы объяснил, какова его цель.У меня есть набор значений, выбираемых каждый час и хранящихся в БД.Эти значения могут увеличиваться или оставаться равными со временем.Этот запрос извлекает последнее значение день за днем ​​ в течение последних 60 дней (у меня есть запрос двойников для извлечения последнего значения по неделям и месяцам, они похожи).Запрос не требует пояснений:

SELECT l.value AS value
FROM atable AS l
WHERE l.time = (
                  SELECT MAX(m.time)
                  FROM atable AS m
                  WHERE DATE(l.time) = DATE(m.time) 
                  LIMIT 1
               )
ORDER BY l.time DESC 
LIMIT 60

Не выглядит особенным.Но это очень медленно (> 30 секунд), учитывая, что time является индексом, а таблица содержит менее 5000 строк.И я уверен, что проблема с подзапросом.

Где ошибка нуба?


Обновление 1 : та же ситуация, если я избегаю MAX() с использованием SELECT m.time ... ORDER BY m.time DESC.

Обновление 2 : Кажется, это не проблема с функцией DATE(), вызываемой много раз.Я пытался создать вычисляемое поле day DATE.UPDATE atable SET day = DATE(time) работает менее чем за 2 сек.Измененный запрос с l.day = m.day (без функций!) Выполняется точно в то же время, что и раньше.


Ответы [ 6 ]

2 голосов
/ 10 февраля 2012

Основная проблема, которую я вижу, - использование DATE() слева от выражения в предложении WHERE.Использование функции DATE() с обеих сторон выражения WHERE явно запрещает MySQL использовать индекс в поле даты.Вместо этого он должен просканировать все строки, чтобы применить функцию к каждой строке.

Вместо этого:

WHERE DATE(l.time) = DATE(m.time) 

Попробуйте что-то вроде этого:

WHERE l.time BETWEEN
  DATE_SUB(m.date, INTERVAL TIME_TO_SEC(m.date) SECOND)
  AND DATE_ADD(DATE_SUB(m.date, INTERVAL TIME_TO_SEC(m.date) SECOND), INTERVAL 86399 SECOND)

Может быть, вызнаю лучший способ превратить m.date в диапазон, подобный 2012-02-09 00:00:00 и 2012-02-09 23:59:59, чем в приведенном выше примере, но идея в том, что вы хотите оставить левую часть выражения в качестве необработанного имени столбца, l.timeв этом случае и задайте ему диапазон в виде двух констант (или двух выражений, которые можно преобразовать в константы) с правой стороны.

РЕДАКТИРОВАТЬ

Я использую ваше предварительно вычисленное поле day:

SELECT *
FROM atable a
WHERE a.time IN
(SELECT MAX(time)
FROM atable
GROUP BY day
ORDER BY day DESC
LIMIT 60)

По крайней мере, здесь внутренний запрос выполняется только один раз, а затем выполняется бинарный поиск с ключом IN.Вы по-прежнему сканируете таблицу, но только один раз, и преимущество внутреннего запроса, выполняемого только один раз, вероятно, приведет к огромным потерям.

Если вы знаете, что у вас есть значения на каждый день, вы можете улучшитьэтот внутренний запрос, добавив предложение WHERE, ограничив его последними 60 календарными днями и потеряв LIMIT 60.Убедитесь, что day и time проиндексированы.

1 голос
/ 10 февраля 2012

На основании ответа обратной связи, если записи последовательно добавляются через дату / время, напрямую соотносятся с идентификатором автоинкремента, который заботится о ВРЕМЕНИ ... получите число автоинклюзии для точного, не однозначного объединения

select
      A1.AutoID,
      A1.time,
      A1.Value
   from
      ( select date( A2.time ) as SingleDate,
               max( A2.AutoID ) as MaxAutoID
           from aTable A2
           where date( A2.Time ) >= date( date_sub( now(), interval 60 day ))
           group by date( A2.time ) ) into MaxPerDate
      JOIN aTable A1
         on MaxPerDate.MaxAutoID = A1.AutoID
   order by
      A1.AutoID DESC
1 голос
/ 10 февраля 2012

Вместо использования MAX (m.time) в подвыборе сделайте следующее

SELECT m.time
FROM table AS m
WHERE DATE(l.time) = DATE(m.time)
ORDER BY m.time DESC
LIMIT 1

Это может помочь ускорить запрос, поскольку он предоставляет парсеру запросов альтернативу

Однако еще одна вещь, которую я заметил, это то, что вы используете DATE (l.time) и DATE (m.time), которые, если ваш индекс не создан для DATE (m.time), то вы не будете использовать индекс и следовательно может вызвать медлительность.

0 голосов
/ 10 февраля 2012

Ваш внешний запрос использует файловую сортировку без индексов. Попробуйте переключиться на движок InnoDB, чтобы посмотреть, улучшится ли он.

Проведение быстрого теста:

mysql> show create table atable\G
*************************** 1. row ***************************
       Table: atable
Create Table: CREATE TABLE `atable` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `t` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `t` (`t`)
) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

mysql> explain SELECT id FROM atable AS l WHERE l.t = (                   SELECT MAX(m.t)                   FROM atable AS m                   WHERE DATE(l.t) = DATE(m.t)                    LIMIT 1                ) ORDER BY l.t DESC  LIMIT 50;
+----+--------------------+-------+-------+---------------+------+---------+------+------+--------------------------+
| id | select_type        | table | type  | possible_keys | key  | key_len | ref  | rows | Extra                    |
+----+--------------------+-------+-------+---------------+------+---------+------+------+--------------------------+
|  1 | PRIMARY            | l     | index | NULL          | t    | 4       | NULL |   50 | Using where; Using index |
|  2 | DEPENDENT SUBQUERY | m     | index | NULL          | t    | 4       | NULL |   50 | Using where; Using index |
+----+--------------------+-------+-------+---------------+------+---------+------+------+--------------------------+
2 rows in set (0.00 sec)

After changing to MyISAM:

mysql> explain SELECT id FROM atable AS l WHERE l.t = (                   SELECT MAX(m.t)                   FROM atable AS m                   WHERE DATE(l.t) = DATE(m.t)                    LIMIT 1                ) ORDER BY l.t DESC  LIMIT 50;
+----+--------------------+-------+-------+---------------+------+---------+------+------+-----------------------------+
| id | select_type        | table | type  | possible_keys | key  | key_len | ref  | rows | Extra                       |
+----+--------------------+-------+-------+---------------+------+---------+------+------+-----------------------------+
|  1 | PRIMARY            | l     | ALL   | NULL          | NULL | NULL    | NULL |   50 | Using where; Using filesort |
|  2 | DEPENDENT SUBQUERY | m     | index | NULL          | t    | 4       | NULL |   50 | Using where; Using index    |
+----+--------------------+-------+-------+---------------+------+---------+------+------+-----------------------------+
2 rows in set (0.00 sec)
0 голосов
/ 10 февраля 2012

Если у вас есть индекс на time, я бы предложил получить TOP 1 вместо MAX следующим образом:

SELECT  l.value AS value
FROM    table AS l
WHERE   l.time = (
               SELECT TOP 1 m.time
               FROM   table AS m
               ORDER BY m.time DESC LIMIT 1
             )
ORDER BY l.time DESC LIMIT 60
0 голосов
/ 10 февраля 2012

Вы можете использовать оператор объяснения, чтобы mysql сообщал вам, что он делает.

EXPLAIN SELECT  l.value AS value
        FROM    table AS l
        WHERE   l.time = (
                   SELECT MAX(m.time)
                   FROM   table AS m
                   WHERE  DATE(l.time) = DATE(m.time) LIMIT 1
                )
        ORDER BY l.time DESC LIMIT 60

Это должно, по крайней мере, дать вам представление о том, что делать дальше.

...