Оптимизация медленности подсчета (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 |                                          |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
  1. я изменил 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 |                                                           |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
  1. я выполнил три запроса по отдельности, удалив 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, то перемещение его вверх по дереву индекса заставит его работать медленнее, что, по крайней мере, скажет вам, что он не может его использовать.

Я не могу думаю, сколько угодно можно играть.


  1. subscriber_id бесполезно в вашем ключе, потому что вы группируете по вычисляемому полю вне ключа (log_date) перед подсчетом различных подписчиков. Это объясняет, почему это так медленно, потому что MySQL должен сортировать и фильтровать дубликаты абонентов без использования ключа.

  2. может быть ошибка с вашим условием log_time : у вас должно быть противоположное преобразование часового пояса вашего выбора (т. е. '+05:30','+00:00'), но у него не будет никаких основных инцидент во время запроса.

  3. вы можете избежать "Союза всех", сделав 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. Мой точный же запрос стал намного быстрее, когда я:

  1. переключился на InnoDB (который вы уже использование)
  2. переключил кодировку на ASCII. Если вам не нужен utf8, он занимает 3x столько места (и времени для поиска).
  3. Сделайте каждый размер столбца как можно меньшим, а не нулевым, если это возможно.
  4. увеличенный размер буферного пула InnoDB MySQL. Многие рекомендации-увеличить его до 70% вашей ОЗУ, если это выделенная машина.
  5. я отсортировал свою таблицу по индексу покрытия, записал ее через SELECT в OUTFILE, а затем снова вставил ее в новую таблицу. Это физически сортирует все записи в порядке поиска.

Я понятия не имею, какие из этих изменений исправили мою проблему (потому что я был ненаучным и не пробовал их по одному), но это сделало мои запросы 50-100x быстрее. YMMV.