Оптимизация медленности подсчета (DISTINCT) даже с индексами покрытия
у нас есть таблица в MySql с примерно 30 миллионами записей, следующая структура таблицы
CREATE TABLE `campaign_logs` (
`domain` varchar(50) DEFAULT NULL,
`campaign_id` varchar(50) DEFAULT NULL,
`subscriber_id` varchar(50) DEFAULT NULL,
`message` varchar(21000) DEFAULT NULL,
`log_time` datetime DEFAULT NULL,
`log_type` varchar(50) DEFAULT NULL,
`level` varchar(50) DEFAULT NULL,
`campaign_name` varchar(500) DEFAULT NULL,
KEY `subscriber_id_index` (`subscriber_id`),
KEY `log_type_index` (`log_type`),
KEY `log_time_index` (`log_time`),
KEY `campid_domain_logtype_logtime_subid_index` (`campaign_id`,`domain`,`log_type`,`log_time`,`subscriber_id`),
KEY `domain_logtype_logtime_index` (`domain`,`log_type`,`log_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
ниже мой запрос
Я делаю UNION ALL вместо использования в операции
SELECT log_type,
DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
count(DISTINCT subscriber_id) AS COUNT,
COUNT(subscriber_id) AS total
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN='xxx'
AND campaign_id='123'
AND log_type = 'EMAIL_OPENED'
AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY log_date
UNION ALL
SELECT log_type,
DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
COUNT(DISTINCT subscriber_id) AS COUNT,
COUNT(subscriber_id) AS total
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN='xxx'
AND campaign_id='123'
AND log_type = 'EMAIL_SENT'
AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY log_date
UNION ALL
SELECT log_type,
DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
COUNT(DISTINCT subscriber_id) AS COUNT,
COUNT(subscriber_id) AS total
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN='xxx'
AND campaign_id='123'
AND log_type = 'EMAIL_CLICKED'
AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY log_date,
ниже мое объяснение заявление
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
| 1 | PRIMARY | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468 | NULL | 55074 | Using where; Using index; Using filesort |
| 2 | UNION | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468 | NULL | 330578 | Using where; Using index; Using filesort |
| 3 | UNION | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468 | NULL | 1589 | Using where; Using index; Using filesort |
| NULL | UNION RESULT | <union1,2,3> | ALL | NULL | NULL | NULL | NULL | NULL | |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
- я изменил COUNT (subscriber_id) на COUNT (*) и не заметил увеличения производительности.
2.Я удалил COUNT (DISTINCT subscriber_id) из запроса , затем я получил огромный увеличение производительности, я получаю результаты примерно за 1.5 сек, ранее это брала 50 сек - 1 минуту. Но мне нужно отличное количество subscriber_id от запроса
ниже объясняется, когда я удаляю COUNT (DISTINCT subscriber_id) из запроса
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
| 1 | PRIMARY | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468 | NULL | 55074 | Using where; Using index; Using temporary; Using filesort |
| 2 | UNION | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468 | NULL | 330578 | Using where; Using index; Using temporary; Using filesort |
| 3 | UNION | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468 | NULL | 1589 | Using where; Using index; Using temporary; Using filesort |
| NULL | UNION RESULT | <union1,2,3> | ALL | NULL | NULL | NULL | NULL | NULL | |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
- я выполнил три запроса по отдельности, удалив UNION ALL. Один запрос занимает 32seconds , другие принимают за 1,5 секунды каждый, но первый запрос имеет дело с около 350к записей и другие имеют дело только с 2К строк
я мог бы решить свою проблему производительности, оставив COUNT(DISTINCT...)
но мне нужны эти ценности. Есть ли способ, чтобы выполнить рефакторинг мой запрос, или добавить индекс, или что-то, чтобы получить COUNT(DISTINCT...)
значения, но гораздо быстрее?
обновление следующая информация о распределении данных в таблице
для 1 домен Кампания 1 20 log_types 1к-200к подписчиков
запрос выше я баллотируюсь домен имея 180к+ подписчиков.
6 ответов
если запрос без count(distinct)
идет намного быстрее, возможно, вы можете сделать вложенную агрегацию:
SELECT log_type, log_date,
count(*) AS COUNT, sum(cnt) AS total
FROM (SELECT log_type,
DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
subscriber_id, count(*) as cnt
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN = 'xxx' AND
campaign_id = '123' AND
log_type IN ('EMAIL_SENT', 'EMAIL_OPENED', 'EMAIL_CLICKED') AND
log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND
CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY logtype, log_date, subscriber_id
) l
GROUP BY logtype, log_date;
если повезет, это займет 2-3 секунды, а не 50. Однако вам может потребоваться разбить это на подзапросы, чтобы получить полную производительность. Итак, если это не имеет значительного прироста производительности, измените in
на =
один из видов. Если это работает, то union all
может быть необходимым.
EDIT:
другой попытка использовать переменные для перечисления значений перед group by
:
SELECT log_type, log_date, count(*) as cnt,
SUM(rn = 1) as sub_cnt
FROM (SELECT log_type,
DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
subscriber_id,
(@rn := if(@clt = concat_ws(':', campaign_id, log_type, log_time), @rn + 1,
if(@clt := concat_ws(':', campaign_id, log_type, log_time), 1, 1)
)
) as rn
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index) CROSS JOIN
(select @rn := 0)
WHERE DOMAIN = 'xxx' AND
campaign_id = '123' AND
log_type IN ('EMAIL_SENT', 'EMAIL_OPENED', 'EMAIL_CLICKED') AND
log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00', '+00:00', '+05:30') AND
CONVERT_TZ('2015-03-01 23:59:58', '+00:00', '+05:30')
ORDER BY logtype, log_date, subscriber_id
) t
GROUP BY log_type, log_date;
это все еще требует другого вида данных, но это может помочь.
чтобы ответить на ваш вопрос:
есть ли способ, чтобы выполнить рефакторинг мой запрос, или добавить индекс, или что-то, чтобы получить счет (DISTINCT...) значения, но гораздо быстрее?
Да, не группировать по вычисляемому полю (не группировать по результату функции). Вместо этого предварительно вычислите его, сохраните в постоянном столбце и включите этот постоянный столбец в индекс.
я бы попытался сделать следующее и посмотреть, изменится ли он производительность значительно.
1) упростите запрос и сосредоточьтесь на одной части.
Оставьте только один длинный ход SELECT
из трех, избавиться от UNION
для настройки периода. Когда-то самый длинный SELECT
оптимизирован, добавьте больше и проверьте, как работает полный запрос.
2) группировка по результату функции не позволяет движку эффективно использовать индекс.
Добавьте еще один столбец в таблицу (сначала временно, просто чтобы проверить идею) с результатом этой функции. Насколько я вижу, вы хотите сгруппироваться на 1 час, поэтому добавьте столбец log_time_hour datetime
и установить его в log_time
округлено / усечено до ближайшего часа (сохранить компонент даты).
добавить индекс с помощью нового столбца:(domain, campaign_id, log_type, log_time_hour, subscriber_id)
. Порядок первых трех столбцов в индексе не должен иметь значения (поскольку вы используете равенство сравнить с некоторой константой в запросе, а не диапазон), но сделайте их в том же порядке, что и в запросе. Или, лучше, сделайте их в определении индекса и в запросе в порядке селективность. Если у вас есть 100,000
кампании, 1000
домены и 3
типы журналов, затем поместите их в следующем порядке:campaign_id, domain, log_type
. Это не должно иметь большого значения, но стоит проверить. log_time_hour
должен занять четвертое место в определении индекса и subscriber_id
последние.
в запросе используйте новый столбец в WHERE
и GROUP BY
. Убедитесь, что в : как log_type
и log_time_hour
.
вам нужны оба COUNT
и COUNT(DISTINCT)
? Оставить только COUNT
сначала измерьте производительность. Оставь только COUNT(DISTINCT)
и измерить производительность. Оставьте оба и измерьте производительность. Посмотрите, как они сравниваются.
SELECT log_type,
log_time_hour,
count(DISTINCT subscriber_id) AS distinct_total,
COUNT(subscriber_id) AS total
FROM stats.campaign_logs
WHERE DOMAIN='xxx'
AND campaign_id='123'
AND log_type = 'EMAIL_OPENED'
AND log_time_hour >= '2015-02-01 00:00:00'
AND log_time_hour < '2015-03-02 00:00:00'
GROUP BY log_type, log_time_hour
SELECT log_type,
DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
count(DISTINCT subscriber_id) AS COUNT,
COUNT(subscriber_id) AS total
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN='xxx'
AND campaign_id='123'
AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY log_type, log_date
добавить AND log_type IN ('EMAIL_OPENED', 'EMAIL_SENT', 'EMAIL_CLICKED')
при необходимости.
Я бы попробовал другие порядки индекса, который вы используете, перемещая subscriber_id, и посмотреть, какой эффект. Возможно, вы можете получить лучшие результаты, перемещая столбцы с более высокой мощностью.
сначала я подумал, что он может использовать только часть индекса (не получая subscriber_id вообще). Если он не может использовать subscriber_id, то перемещение его вверх по дереву индекса заставит его работать медленнее, что, по крайней мере, скажет вам, что он не может его использовать.
Я не могу думаю, сколько угодно можно играть.
subscriber_id
бесполезно в вашем ключе, потому что вы группируете по вычисляемому полю вне ключа (log_date) перед подсчетом различных подписчиков. Это объясняет, почему это так медленно, потому что MySQL должен сортировать и фильтровать дубликаты абонентов без использования ключа.может быть ошибка с вашим условием log_time : у вас должно быть противоположное преобразование часового пояса вашего выбора (т. е.
'+05:30','+00:00'
), но у него не будет никаких основных инцидент во время запроса.вы можете избежать "Союза всех", сделав
log_type IN (...)
иlog_type, log_date
лучшим эффективным решением было бы добавить поле mid-hour в схему базы данных и установить там один из 48 mid-hour дня (и позаботиться о часовом поясе mid-hour). Таким образом, вы можете использовать индекс на campaign_id
,domain
,log_type
,log_mid_hour
,subscriber_id
это будет довольно избыточно, но улучшит скорость.
это привело к некоторым инициализации в таблице: будьте осторожны : не проверить это на рабочем столе
ALTER TABLE campaign_logs
ADD COLUMN log_mid_hour TINYINT AFTER log_time;
UPDATE campaign_logs SET log_mid_hour=2*HOUR(log_time)+IF(MINUTE(log_time)>29,1,0);
ALTER TABLE campaign_logs
ADD INDEX(`campaign_id`,`domain`,`log_time`,`log_type`,`log_mid_hour`,`subscriber_id`);
Вам также придется установить log_mid_hour в вашем скрипте для будущих записей.
ваш запрос станет (для 11 середине Фидо):
SELECT log_type,
MOD(log_mid_hour+11, 48) tz_log_mid_hour,
COUNT(DISTINCT subscriber_id) AS COUNT,
COUNT(subscriber_id) AS total
FROM stats.campaign_logs
WHERE DOMAIN='xxx'
AND campaign_id='123'
AND log_type IN('EMAIL_SENT', 'EMAIL_OPENED','EMAIL_CLICKED')
AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+05:30','+00:00')
AND CONVERT_TZ('2015-03-01 23:59:58','+05:30','+00:00')
GROUP BY log_type, log_mid_hour;
это даст вам подсчет для каждого середины часа, принимая полную выгоду от вашего индекса.
У меня была очень похожая проблема, размещенная здесь на SO, и получила большую помощь. Вот нить:MySQL MyISAM slow count () запрос, несмотря на покрытие index
в двух словах, я обнаружил, что моя проблема не имела ничего общего с запросом или индексами, а все, что связано с тем, как я настроил таблицы и MySQL. Мой точный же запрос стал намного быстрее, когда я:
- переключился на InnoDB (который вы уже использование)
- переключил кодировку на ASCII. Если вам не нужен utf8, он занимает 3x столько места (и времени для поиска).
- Сделайте каждый размер столбца как можно меньшим, а не нулевым, если это возможно.
- увеличенный размер буферного пула InnoDB MySQL. Многие рекомендации-увеличить его до 70% вашей ОЗУ, если это выделенная машина.
- я отсортировал свою таблицу по индексу покрытия, записал ее через SELECT в OUTFILE, а затем снова вставил ее в новую таблицу. Это физически сортирует все записи в порядке поиска.
Я понятия не имею, какие из этих изменений исправили мою проблему (потому что я был ненаучным и не пробовал их по одному), но это сделало мои запросы 50-100x быстрее. YMMV.