Как я могу оптимизировать этот запрос MySQL дальше?

9

У меня есть запрос, который занимает особенно много времени (15+ секунд), и он только ухудшается со временем по мере роста моего набора данных. Я оптимизировал это в прошлом и добавил индексы, сортировку на уровне кода и другие оптимизации, но это требует некоторой дальнейшей доработки.

SELECT sounds.*, avg(ratings.rating) AS avg_rating, count(ratings.rating) AS votes FROM `sounds` 
INNER JOIN ratings ON sounds.id = ratings.rateable_id 
WHERE (ratings.rateable_type = 'Sound' 
   AND sounds.blacklisted = false 
   AND sounds.ready_for_deployment = true 
   AND sounds.deployed = true 
   AND sounds.type = "Sound" 
   AND sounds.created_at > "2011-03-26 21:25:49") 
GROUP BY ratings.rateable_id

Цель запроса - получить sound idсреднюю оценку самых последних выпущенных звуков. Есть около 1500 звуков и 2 миллиона оценок.

У меня есть несколько показателей sounds

mysql> show index from sounds;
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+————+
| Table  | Non_unique | Key_name                                 | Seq_in_index | Column_name          | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+————+
| sounds |          0 | PRIMARY                                  |            1 | id                   | A         |        1388 |     NULL | NULL   |      | BTREE      |         | 
| sounds |          1 | sounds_ready_for_deployment_and_deployed |            1 | deployed             | A         |           5 |     NULL | NULL   | YES  | BTREE      |         | 
| sounds |          1 | sounds_ready_for_deployment_and_deployed |            2 | ready_for_deployment | A         |          12 |     NULL | NULL   | YES  | BTREE      |         | 
| sounds |          1 | sounds_name                              |            1 | name                 | A         |        1388 |     NULL | NULL   |      | BTREE      |         | 
| sounds |          1 | sounds_description                       |            1 | description          | A         |        1388 |      128 | NULL   | YES  | BTREE      |         | 
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+

и несколько на ratings

mysql> show index from ratings;
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+————+
| Table   | Non_unique | Key_name                                | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+————+
| ratings |          0 | PRIMARY                                 |            1 | id          | A         |     2008251 |     NULL | NULL   |      | BTREE      |         | 
| ratings |          1 | index_ratings_on_rateable_id_and_rating |            1 | rateable_id | A         |          18 |     NULL | NULL   |      | BTREE      |         | 
| ratings |          1 | index_ratings_on_rateable_id_and_rating |            2 | rating      | A         |        9297 |     NULL | NULL   | YES  | BTREE      |         | 
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

Здесь EXPLAIN

mysql> EXPLAIN SELECT sounds.*, avg(ratings.rating) AS avg_rating, count(ratings.rating) AS votes FROM sounds INNER JOIN ratings ON sounds.id = ratings.rateable_id WHERE (ratings.rateable_type = 'Sound' AND sounds.blacklisted = false AND sounds.ready_for_deployment = true AND sounds.deployed = true AND sounds.type = "Sound" AND sounds.created_at > "2011-03-26 21:25:49") GROUP BY ratings.rateable_id;
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+——————+
| id | select_type | table   | type   | possible_keys                                    | key                                     | key_len | ref                                     | rows    | Extra       |
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+——————+
|  1 | SIMPLE      | ratings | index  | index_ratings_on_rateable_id_and_rating          | index_ratings_on_rateable_id_and_rating | 9       | NULL                                    | 2008306 | Using where | 
|  1 | SIMPLE      | sounds  | eq_ref | PRIMARY,sounds_ready_for_deployment_and_deployed | PRIMARY                                 | 4       | redacted_production.ratings.rateable_id |       1 | Using where | 
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+-------------+

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

Что еще я могу сделать, чтобы сделать это лучше ?

coneybeare
источник
Можете ли вы показать EXPLAINвыход? EXPLAIN SELECT sounds.*, avg(ratings.rating) AS avg_rating, count(ratings.rating) AS votes FROM sounds INNER JOIN ratings ON sounds.id = ratings.rateable_id WHERE (ratings.rateable_type = 'Sound' AND sounds.blacklisted = false AND sounds.ready_for_deployment = true AND sounds.deployed = true AND sounds.type = "Sound" AND sounds.created_at > "2011-03-26 21:25:49") GROUP BY ratings.rateable_id
Дерек Дауни
@coneybeare Это было очень интересное испытание для меня сегодня !!! +1 за твой вопрос. Я хотел бы, чтобы в ближайшем будущем появилось больше подобных вопросов.
RolandoMySQLDBA
@coneybeare Похоже, что новый EXPLAIN читает только 21540 строк (359 X 60) вместо 2 008 306. Пожалуйста, запустите EXPLAIN для запроса, который я первоначально предложил в своем ответе. Я хотел бы видеть количество строк, которые приходят от этого.
RolandoMySQLDBA
@RolandoMySQLDBA Новое объяснение действительно показывает, что меньшее количество строк с индексом, однако, время выполнения запроса было все еще около 15 секунд, без улучшения
coneybeare
@coneybeare Я хорошо настроил запрос. Пожалуйста, запустите EXPLAIN по моему новому запросу. Я добавил это к своему ответу.
RolandoMySQLDBA

Ответы:

7

После просмотра запроса, таблиц и предложений WHERE AND GROUP BY я рекомендую следующее:

Рекомендация № 1) Рефакторинг запроса

Я реорганизовал запрос, чтобы сделать три (3) вещи:

  1. создавать меньшие временные таблицы
  2. Обработайте предложение WHERE для этих временных таблиц
  3. Задержка присоединения до самого последнего

Вот мой предложенный запрос:

SELECT
  sounds.*,srkeys.avg_rating,srkeys.votes
FROM
(
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
) srkeys INNER JOIN sounds USING (id);

Рекомендация № 2) Индексируйте таблицу звуков индексом, который будет соответствовать предложению WHERE.

Столбцы этого индекса включают все столбцы из предложения WHERE со статическими значениями в первую очередь и перемещением цели в последнюю очередь

ALTER TABLE sounds ADD INDEX support_index
(blacklisted,ready_for_deployment,deployed,type,created_at);

Я искренне верю, что вы будете приятно удивлены. Попробуйте!

ОБНОВЛЕНИЕ 2011-05-21 19:04

Я только что увидел мощность. Ой! Количество элементов 1 для rateable_id. Мальчик, я чувствую себя глупо !!!

ОБНОВЛЕНИЕ 2011-05-21 19:20

Возможно, создание индекса будет достаточно, чтобы улучшить положение вещей.

ОБНОВЛЕНИЕ 2011-05-21 22:56

Пожалуйста, запустите это:

EXPLAIN SELECT
  sounds.*,srkeys.avg_rating,srkeys.votes
FROM
(
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes FROM
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
) srkeys INNER JOIN sounds USING (id);

ОБНОВЛЕНИЕ 2011-05-21 23:34

Я рефакторинг это снова. Попробуйте это пожалуйста:

EXPLAIN
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes FROM
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
;

ОБНОВЛЕНИЕ 2011-05-21 23:55

Я рефакторинг это снова. Попробуйте это пожалуйста (в последний раз):

EXPLAIN
  SELECT A.id,avg(B.rating) AS avg_rating, count(B.rating) AS votes FROM
  (
    SELECT BB.* FROM
    (
      SELECT id FROM sounds
      WHERE blacklisted = false 
      AND   ready_for_deployment = true 
      AND   deployed = true 
      AND   type = "Sound" 
      AND   created_at > '2011-03-26 21:25:49'
    ) AA INNER JOIN sounds BB USING (id)
  ) A INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) B
  ON A.id = B.rateable_id
  GROUP BY B.rateable_id;

ОБНОВЛЕНИЕ 2011-05-22 00:12

Я ненавижу сдаваться !!!!

EXPLAIN
  SELECT A.*,avg(B.rating) AS avg_rating, count(B.rating) AS votes FROM
  (
    SELECT BB.* FROM
    (
      SELECT id FROM sounds
      WHERE blacklisted = false 
      AND   ready_for_deployment = true 
      AND   deployed = true 
      AND   type = "Sound" 
      AND   created_at > '2011-03-26 21:25:49'
    ) AA INNER JOIN sounds BB USING (id)
  ) A,
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
    AND AAA.rateable_id = A.id
  ) B
  GROUP BY B.rateable_id;

ОБНОВЛЕНИЕ 2011-05-22 07:51

Меня беспокоит, что рейтинги возвращаются с 2 миллионами строк в ОБЪЯСНЕНИИ. Затем он ударил меня. Вам может понадобиться другой индекс в таблице рейтингов, который начинается с rateable_type:

ALTER TABLE ratings ADD INDEX
rateable_type_rateable_id_ndx (rateable_type,rateable_id);

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

После создания этого индекса, пожалуйста, повторите мой исходный предложенный запрос, а также попробуйте свой:

SELECT
  sounds.*,srkeys.avg_rating,srkeys.votes
FROM
(
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
) srkeys INNER JOIN sounds USING (id);

ОБНОВЛЕНИЕ 2011-05-22 18:39: ЗАКЛЮЧИТЕЛЬНЫЕ СЛОВА

Я реорганизовал запрос в хранимой процедуре и добавил индекс, чтобы помочь ответить на вопрос об ускорении процесса. Я получил 6 голосов, принял ответ и получил награду в размере 200.

Я также реорганизовал другой запрос (маргинальные результаты) и добавил индекс (впечатляющие результаты). Я получил 2 отзыва и получил ответ.

Я добавил индекс для еще одного запроса запроса и один раз проголосовал

а теперь твой вопрос .

Желание ответить на все подобные вопросы (включая ваши) было вдохновлено видео на YouTube, которое я посмотрел о рефакторинге запросов.

Еще раз спасибо, @coneybeare !!! Я хотел ответить на этот вопрос в максимально возможной степени, а не просто принимать баллы или почести. Теперь я чувствую, что заработал очки !!!

RolandoMySQLDBA
источник
Я добавил индекс, никаких улучшений по времени. Вот новый ОБЪЯСНЕНИЕ
coneybeare
ОБЪЯСНЕНИЕ по запросу из рекомендации 1: cloud.coneybeare.net/6xZ2 Выполнение этого запроса заняло около 30 секунд
coneybeare
Мне почему-то пришлось немного изменить ваш синтаксис (я добавил FROM перед первым запросом, и мне пришлось избавиться от псевдонима AAA). Вот ОБЪЯСНЕНИЕ : cloud.coneybeare.net/6xlq На выполнение самого запроса ушло около 30 секунд
coneybeare
@RolandoMySQLDBA: ОБЪЯСНИТЕ о своем обновлении 23:55: cloud.coneybeare.net/6wrN Фактический запрос выполнялся за минуту, поэтому я убил процесс
coneybeare
Второй внутренний выбор не может получить доступ к таблице выбора A, поэтому A.id выдает ошибку.
coneybeare
3

Спасибо за выход EXPLAIN. Как вы можете сказать из этого заявления, причина, по которой это занимает так много времени, заключается в полном сканировании таблицы в таблице рейтингов. Ничто в операторе WHERE не фильтрует 2 миллиона строк.

Вы можете добавить индекс для ratings.type, но я предполагаю, что CARDINALITY будет очень низким, и вы все равно будете сканировать довольно много строк ratings.

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

Обновлено:

Если бы это был я, я бы добавил индекс, так sounds.createdкак он лучше всего фильтрует строки и, вероятно, заставит оптимизатор запросов mysql использовать индексы таблицы звуков. Остерегайтесь запросов, которые используют давно созданные временные рамки (1 год, 3 месяца, зависит только от размера таблицы звуков).

Дерек Дауни
источник
Похоже, ваше предложение было примечательным для @coneybeare. +1 от меня тоже.
RolandoMySQLDBA
Индекс на созданный не сбрил в любое время. Вот обновленное объяснение. cloud.coneybeare.net/6xvc
coneybeare
2

Если это должен быть доступный запрос «на лету» , то это немного ограничивает ваши возможности.

Я собираюсь предложить разделяй и властвуй для этой проблемы.

--
-- Create an in-memory table
CREATE TEMPORARY TABLE rating_aggregates (
rateable_id INT,
avg_rating NUMERIC,
votes NUMERIC
);
--
-- For now, just aggregate. 
INSERT INTO rating_aggregates
SELECT ratings.rateable_id, 
avg(ratings.rating) AS avg_rating, 
count(ratings.rating) AS votes FROM `sounds`  
WHERE ratings.rateable_type = 'Sound' 
GROUP BY ratings.rateable_id;
--
-- Now get your final product --
SELECT 
sounds.*, 
rating_aggregates.avg_rating, 
rating_aggregates.votes AS votes,
rating_aggregates.rateable_id 
FROM rating_aggregates 
INNER JOIN sounds ON (sounds.id = rating_aggregates.rateable_id) 
WHERE 
ratings.rateable_type = 'Sound' 
   AND sounds.blacklisted = false 
   AND sounds.ready_for_deployment = true 
   AND sounds.deployed = true 
   AND sounds.type = "Sound" 
   AND sounds.created_at > "2011-03-26 21:25:49";
randomx
источник
Кажется, @coneybeare видел что-то в вашем предложении. +1 от меня !!!
RolandoMySQLDBA
Я на самом деле не мог заставить это работать. Я получал ошибки SQL, которые я не был уверен, как подойти. Я никогда не работал с временными таблицами
coneybeare
Я получил его в конце концов (мне пришлось добавить FROM sounds, ratingsк среднему запросу), но он заблокировал мой sql box, и мне пришлось убить процесс.
coneybeare
0

Используйте СОЕДИНЕНИЯ, а не подзапросы. Помогла ли какая-нибудь из ваших попыток подзапроса?

ПОКАЗАТЬ СОЗДАТЬ ТАБЛИЦУ звуков \ G

ПОКАЗАТЬ CREATE TABLE рейтинги \ G

Часто полезно иметь «составные» индексы, а не одностолбчатые. Возможно, ИНДЕКС (тип, create_at)

Вы фильтруете обе таблицы в JOIN; это может быть проблемой производительности.

Есть около 1500 звуков и 2 миллиона оценок.

Рекомендую, чтобы у вас был идентификатор auto_increment ratings, создать сводную таблицу и использовать идентификатор AI для отслеживания того, где вы «остановились». Однако не храните средние значения в сводной таблице:

avg (ratings.rating) AS avg_rating,

Вместо этого сохраните СУММУ (рейтинг. Рейтинг). Среднее значение среднего математически неверно для вычисления среднего значения; (сумма сумм) / (сумма отсчетов) верна.

Рик Джеймс
источник