Повышение скорости MySQL Query - более 150 000 возвращаемых строк замедляет запрос - PullRequest
1 голос
/ 22 марта 2019

Привет У меня сейчас запрос, который занимает 11 (сек) для запуска.У меня есть отчет, который отображается на веб-сайте, который запускает 4 разных запроса, которые похожи, и каждый занимает 11 (сек) каждый для запуска.Я на самом деле не хочу, чтобы клиенту пришлось ждать минуты, чтобы все эти запросы запустились и отобразили данные.

Я использую 4 разных AJAX-запроса для вызова API, чтобы получить нужные мне данные, и все они запускаются сразу, но запросы выполняются один за другим.Если бы был способ заставить все эти запросы выполняться одновременно (параллельно), поэтому общее время загрузки составляет всего 11 (сек), что также решило бы мою проблему, я не думаю, что это возможно.

Вот запрос, который я выполняю:

SELECT device_uuid,
     day_epoch,
     is_repeat
FROM tracking_daily_stats_zone_unique_device_uuids_per_hour
WHERE day_epoch >= 1552435200
AND day_epoch < 1553040000
AND venue_id = 46
AND zone_id IN (102,105,108,110,111,113,116,117,118,121,287)

Я никак не могу придумать, как ускорить этот запрос, ниже приведены рисунки табличных индексов и оператор объяснения для этого запроса.

indexes

explain statement

Я думаю, что в приведенном выше запросе используются соответствующие индексы в условиях where.

Если есть что-то, что вы можете придумать, чтобы ускорить этот запрос, пожалуйста, дайте мне знать, я работаю над ним в течение 3 дней и, похоже, не могу решить проблему.Было бы здорово сократить время запроса до 5 (сек) максимум.Если я ошибаюсь в проблеме AJAX, пожалуйста, дайте мне знать, поскольку это также решило бы мою проблему.

" EDIT "

Я столкнулся с чем-то довольно странным, что можетбыть причиной проблемы.Когда я изменяю диапазон day_epoch на что-то меньшее (5–9-е), которое возвращает 130 000 строк, время запроса составляет 0,7 (с), но затем я добавляю еще один день в этот диапазон (5–10-е), и он возвращает более 150 000 строк, время запроса13 (сек).Я запустил множество различных диапазонов и пришел к выводу, что количество возвращаемых строк превышает 150 000, что оказывает огромное влияние на время запроса.

Определение таблицы -

CREATE TABLE `tracking_daily_stats_zone_unique_device_uuids_per_hour` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `day_epoch` int(10) NOT NULL,
 `day_of_week` tinyint(1) NOT NULL COMMENT 'day of week, monday = 1',
 `hour` int(2) NOT NULL,
 `venue_id` int(5) NOT NULL,
 `zone_id` int(5) NOT NULL,
 `device_uuid` binary(16) NOT NULL COMMENT 'binary representation of the device_uuid, unique for a single day',
 `device_vendor_id` int(5) unsigned NOT NULL DEFAULT '0' COMMENT 'id of the device vendor',
 `first_seen` int(10) unsigned NOT NULL DEFAULT '0',
 `last_seen` int(10) unsigned NOT NULL DEFAULT '0',
 `is_repeat` tinyint(1) NOT NULL COMMENT 'is the device a repeat for this day?',
 `prev_last_seen` int(10) NOT NULL DEFAULT '0' COMMENT 'previous last seen ts',
 PRIMARY KEY (`id`,`venue_id`) USING BTREE,
 KEY `venue_id` (`venue_id`),
 KEY `zone_id` (`zone_id`),
 KEY `day_of_week` (`day_of_week`),
 KEY `day_epoch` (`day_epoch`),
 KEY `hour` (`hour`),
 KEY `device_uuid` (`device_uuid`),
 KEY `is_repeat` (`is_repeat`),
 KEY `device_vendor_id` (`device_vendor_id`)
) ENGINE=InnoDB AUTO_INCREMENT=450967720 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY HASH (venue_id)
PARTITIONS 100 */

Ответы [ 3 ]

1 голос
/ 22 марта 2019

Простое решение состоит в том, чтобы добавить этот специфический для запроса индекс в таблицу:

ALTER TABLE tracking_daily_stats_zone_unique_device_uuids_per_hour 
ADD INDEX complex_idx (`venue_id`, `day_epoch`, `zone_id`)

ПРЕДУПРЕЖДЕНИЕ Это изменение запроса может занять некоторое время для БД.

И затем принудительно вызывайте его, когда вы вызываете:

SELECT device_uuid,
     day_epoch,
     is_repeat
FROM tracking_daily_stats_zone_unique_device_uuids_per_hour
USE INDEX (complex_idx)
WHERE day_epoch >= 1552435200
AND day_epoch < 1553040000
AND venue_id = 46
AND zone_id IN (102,105,108,110,111,113,116,117,118,121,287)

Он определенно не универсален, но должен работать для этого конкретного запроса.

ОБНОВЛЕНИЕ Когда у вас есть секционированная таблица, вы можетеполучить прибыль, форсируя конкретные PARTITION.В нашем случае, поскольку это venue_id, просто принудительно введите его:

SELECT device_uuid,
     day_epoch,
     is_repeat
FROM tracking_daily_stats_zone_unique_device_uuids_per_hour
PARTITION (`p46`)
WHERE day_epoch >= 1552435200
AND day_epoch < 1553040000
AND zone_id IN (102,105,108,110,111,113,116,117,118,121,287)

Где p46 - это объединенная строка из p и venue_id = 46

И еще один трюк, если вы идете такпуть.Вы можете удалить AND venue_id = 46 из предложения WHERE.Потому что в этом разделе нет других данных.

0 голосов
/ 17 апреля 2019

450M рядов довольно большой.Итак, я буду обсуждать различные вопросы, которые могут помочь.

Сокращение данных Большая таблица приводит к увеличению количества операций ввода-вывода, что является основным фактором снижения производительности.(«Маленькие» таблицы, как правило, остаются в кэше и не несут бремени ввода-вывода.)

  • Любой тип INT, даже INT(2) занимает 4 байта.«Час» может легко уместиться в 1 байт TINYINT.Это экономит более 1 ГБ в данных, плюс аналогичное количество в INDEX(hour).
  • Если можно получить hour и day_of_week, не беспокойтесь о том, чтобы иметь их как отдельные столбцы.Это сэкономит больше места.
  • Какая причина использовать 4-байтовый day_epoch вместо 3-байтового DATE?Или, возможно, вам нужен 5-байтный DATETIME или TIMESTAMP.

Оптимальный INDEX (дубль № 1)

Если это всегдаодиночный venue_id, то либо это хорошее первое сокращение с оптимальным индексом:

INDEX(venue_id, zone_id, day_epoch)

Сначала константа, затем IN, затем диапазон.Оптимизатор справляется с этим во многих случаях.(Неясно, может ли число элементов в предложении IN привести к неэффективности.)

Лучший первичный ключ (лучший индекс)

При AUTO_INCREMENT, вероятно, нет веской причины включать столбцы после столбца auto_inc в PK.То есть PRIMARY KEY(id, venue_id) не лучше, чем PRIMARY KEY(id).

InnoDB упорядочивает BTree данных в соответствии с PRIMARY KEY.Таким образом, если вы выбираете несколько строк, и могут расположить их рядом друг с другом в зависимости от PK, вы получите дополнительную производительность.(cf "Clustered".) Итак:

PRIMARY KEY(venue_id, zone_id, day_epoch,  -- this order, as discussed above;
            id)    -- to make sure that the entire PK is unique.
INDEX(id)      -- to keep AUTO_INCREMENT happy

И я согласен с DROPping любых индексов, которые не используются, включая тот, который я рекомендовал выше.Редко полезно индексировать флаги (is_repeat).

UUID

Индексирование UUID может быть смертельно опасным для производительности, если таблица действительно большая.Это происходит из-за случайности UUID / GUID, что приводит к увеличению нагрузки ввода-вывода для вставки новых записей в индекс.

Многомерный

Предполагается, чтоday_epoch иногда несколько дней, у вас, кажется, есть 2 или 3 «измерения»:

  • Диапазон дат
  • Список зон
  • Место проведения.

INDEXes являются одномерными.В этом и заключается проблема.Однако PARTITIONing иногда может помочь.Я кратко обсуждаю это как «случай 2» в http://mysql.rjweb.org/doc.php/partitionmaint.

Нет хорошего способа получить 3 измерения, поэтому давайте сосредоточимся на 2.

  • Вы должныразделить что-то, что является «диапазоном», таким как day_epoch или zone_id.
  • После этого вы должны решить, что добавить в PRIMARY KEY, чтобы вы могли в дальнейшем воспользоваться «кластеризацией»".

План A: Предполагается, что вы ищете только один venue_id одновременно:

PARTITION BY RANGE(day_epoch)  -- see note below

PRIMARY KEY(venue_id, zone_id, id)

План B: Предполагается, что иногда вы выполняете srefineearch для venue_id IN (.., .., ...)следовательно, он не является хорошим первым столбцом для PK:

Ну, у меня нет хорошего совета здесь;так что давайте перейдем к плану А.

Выражение RANGE должно быть числовым.Ваш day_epoch отлично работает как есть.Изменение на DATE потребует BY RANGE(TO_DAYS(...)), который работает нормально.

Вы должны ограничить количество разделов до 50. (81, упомянутый выше, неплох). Проблема в том, что "много"из перегородок вводит разные неэффективности;«слишком мало» разделов приводит к «зачем»?

Обратите внимание, что почти всегда оптимальный PK отличается для многораздельной таблицы от эквивалентной однораздельной таблицы.

Обратите внимание, что я не согласен с разбиением на venue_id, поскольку вместо этого легко разместить этот столбец в начале PK.

Анализ

Предполагается, что вы выполняете поискдля одного venue_id и использования предложенного мной разбиения & PK, вот как работает SELECT:

  1. Фильтр по диапазону дат.Это может ограничить активность одним разделом.
  2. Детализация BTree данных для этого одного раздела, чтобы найти один venue_id.
  3. Хопскотч через данные оттуда, посадка на нужную zone_ids.
  4. Для каждого последующего фильтра на основе даты.
0 голосов
/ 22 марта 2019

Что произойдет, если вы измените порядок условий? Сначала поставьте venue_id = ?. Заказ имеет значение.

Теперь сначала проверяются все строки на:
- day_epoch >= 1552435200
- затем оставшийся набор для day_epoch < 1553040000
- затем оставшийся набор для venue_id = 46
- затем оставшийся набор для zone_id IN (102,105,108,110,111,113,116,117,118,121,287)

При работе с тяжелыми запросами вы всегда должны стараться сделать первый «селектор» наиболее эффективным. Вы можете сделать это, используя правильный индекс для индекса 1 (или комбинации) и убедившись, что первый селектор сужается больше всего (по крайней мере, для целых чисел, в случае строк вам нужна другая тактика).


Иногда запрос просто медленный. Когда у вас много данных (и / или недостаточно ресурсов), вы просто ничего не можете с этим поделать. Вот где вам нужно другое решение: составьте сводную таблицу. Я сомневаюсь, что вы показываете 150.000 строк х4 вашему посетителю. Вы можете суммировать его, например, ежечасно или каждые несколько минут и выбирать из этого меньшего стола


Оффтоп: добавление индекса ко всему только замедляет при вставке / обновлении / удалении. Индексируйте наименьшее количество столбцов, только когда вы действительно фильтруете (например, используете в WHERE или GROUP BY).

...