Como posso otimizar ainda mais essa consulta do MySQL?

9

Eu tenho uma consulta que está demorando muito tempo para ser executada (mais de 15 segundos) e só piora com o tempo à medida que meu conjunto de dados cresce. Eu otimizei isso no passado e adicionei índices, classificação em nível de código e outras otimizações, mas ele precisa de mais refinamentos.

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

O objetivo da consulta é obter sound ida classificação média e dos sons lançados mais recentes. Existem cerca de 1500 sons e 2 milhões de classificações.

Eu tenho vários índices em 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      |         | 
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+

e vários em 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      |         | 
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

Aqui está o 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 | 
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+-------------+

Como os resultados são obtidos em cache, o desempenho do site não é muito problemático, mas meus aquecedores de cache estão demorando mais e mais para serem executados devido a essa chamada levar tanto tempo e isso está começando a se tornar um problema. Isso não parece muitos números para triturar em uma consulta…

O que mais posso fazer para melhorar esse desempenho ?

coneybeare
fonte
Você pode mostrar a EXPLAINsaída? 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
Derek Downey #
@coneybeare Este foi um desafio muito interessante para mim hoje !!! +1 para sua pergunta. Desejo que mais perguntas como essa surjam no futuro próximo.
RolandoMySQLDBA
@coneybeare Parece que o novo EXPLAIN lê apenas 21540 linhas (359 X 60) em vez de 2.008.306. Por favor, execute EXPLAIN na consulta que sugeri originalmente na minha resposta. Eu gostaria de ver o número de linhas que resultam disso.
RolandoMySQLDBA
@RolandoMySQLDBA O novo explicar, de fato, mostram que menor quantidade de linhas com o índice, no entanto, o tempo para executar a consulta ainda era cerca de 15 segundos, mostrando nenhuma melhoria
Coneybeare
@coneybeare Eu ajustei bem a consulta. Por favor, execute o EXPLAIN na minha nova consulta. Acrescentei à minha resposta.
RolandoMySQLDBA

Respostas:

7

Depois de examinar a consulta, as tabelas e as cláusulas WHERE AND GROUP BY, recomendo o seguinte:

Recomendação nº 1) Refatorar a consulta

Reorganizei a consulta para fazer três (3) coisas:

  1. crie tabelas temporárias menores
  2. Processe a cláusula WHERE nessas tabelas temporárias
  3. Atraso na adesão ao último

Aqui está minha consulta proposta:

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);

Recomendação nº 2) Indexe a tabela de sons com um índice que acomodará a cláusula WHERE

As colunas desse índice incluem todas as colunas da cláusula WHERE com valores estáticos primeiro e o destino móvel por último

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

Eu sinceramente acredito que você ficará agradavelmente surpreendido. De uma chance !!!

UPDATE 2011-05-21 19:04

Acabei de ver a cardinalidade. OUCH !!! Cardinalidade de 1 para rateable_id. Cara, eu me sinto idiota !!!

UPDATE 2011-05-21 19:20

Talvez fazer o índice seja suficiente para melhorar as coisas.

UPDATE 2011-05-21 22:56

Por favor, execute isto:

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);

UPDATE 2011-05-21 23:34

Eu refatorei novamente. Tente este por favor:

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
;

UPDATE 2011-05-21 23:55

Eu refatorei novamente. Tente este por favor (última vez):

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;

UPDATE 2011-05-22 00:12

Eu odeio desistir !!!!

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;

UPDATE 2011-05-22 07:51

Está me incomodando que as classificações voltem com 2 milhões de linhas no EXPLAIN. Então, isso me atingiu. Você pode precisar de outro índice na tabela de classificações, que começa com rateable_type:

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

O objetivo deste índice é reduzir a tabela temporária que manipula classificações para que seja menor que 2 milhões. Se conseguirmos reduzir significativamente essa tabela temporária (pelo menos metade), teremos uma esperança melhor em sua consulta e a minha também trabalhará mais rapidamente.

Depois de criar esse índice, tente novamente minha consulta proposta original e tente a sua:

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);

ATUALIZAÇÃO 22-05-2011 18:39: PALAVRAS FINAIS

Eu refatorava uma consulta em um procedimento armazenado e adicionava um índice para ajudar a responder a uma pergunta sobre como acelerar as coisas. Recebi 6 votos positivos, a resposta foi aceita e recebi uma recompensa de 200.

Eu também refatorei outra consulta (resultados marginais) e adicionei um índice (resultados dramáticos). Recebi 2 votos positivos e a resposta foi aceita.

Adicionei um índice para mais um desafio de consulta e fui votado uma vez

e agora sua pergunta .

O desejo de responder a todas as perguntas como essas (incluindo a sua) foi inspirado em um vídeo do YouTube que eu assisti nas consultas de refatoração.

Mais uma vez obrigado, @coneybeare !!! Eu queria responder a essa pergunta o máximo possível, não apenas aceitar pontos ou elogios. Agora, sinto que ganhei os pontos !!!

RolandoMySQLDBA
fonte
Eu adicionei o índice, nenhuma melhoria no tempo. Aqui está o novo EXPLICAR: cloud.coneybeare.net/6y7c
Coneybeare
A EXPLICAR na consulta de recomendação 1: cloud.coneybeare.net/6xZ2 Demorou cerca de 30 segundos para executar esta consulta
Coneybeare
Eu tive que editar sua sintaxe um pouco por algum motivo (adicionei um FROM antes da primeira consulta e tive que me livrar do alias AAA). Aqui está o explicam: cloud.coneybeare.net/6xlq A consulta real levou cerca de 30 segundos para ser executado
Coneybeare
@RolandoMySQLDBA: Explique em seu update 23:55: cloud.coneybeare.net/6wrN O ran consulta real ao longo de um minuto para que eu matei o processo
Coneybeare
A segunda seleção interna não pode acessar a tabela de seleção A, assim A.id gera erro.
Coneybeare
3

Obrigado pela saída EXPLAIN. Como você pode perceber a partir dessa afirmação, a razão pela qual está demorando tanto é a tabela completa na tabela de classificações. Nada na instrução WHERE está filtrando as 2 milhões de linhas.

Você pode adicionar um índice em ratings.type, mas meu palpite é que a CARDINALIDADE será muito baixa e você ainda estará pesquisando algumas linhas ratings.

Como alternativa, você pode tentar usar dicas de índice para forçar o mysql a usar os índices de sons.

Atualizada:

Se fosse eu, adicionaria um índice, sounds.createdpois ele tem a melhor chance de filtrar as linhas e provavelmente forçará o otimizador de consultas do mysql a usar os índices da tabela de sons. Cuidado com as consultas que usam prazos criados há muito tempo (1 ano, 3 meses, depende apenas do tamanho da tabela de sons).

Derek Downey
fonte
Parece que sua sugestão foi notável para @coneybeare. +1 de mim também.
RolandoMySQLDBA
O índice criado não foi eliminado a qualquer momento. Aqui está o EXPLAIN atualizado. cloud.coneybeare.net/6xvc
Coneybeare
2

Se essa tiver que ser uma consulta disponível "on-the-fly" , isso limitará um pouco suas opções.

Vou sugerir dividir e conquistar para esse problema.

--
-- 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
fonte
parece que @coneybeare viu algo em sua sugestão. +1 de mim !!!
RolandoMySQLDBA
Na verdade, eu não consegui fazer isso funcionar. Eu estava recebendo erros de sql que não tinha certeza de como abordar. Eu nunca realmente trabalhou com tabelas temporárias
Coneybeare
Eu consegui-lo, eventualmente, (eu tinha para adicionar DE sounds, ratingsà consulta do meio), mas trancado minha caixa de sql e eu tive que matar o processo.
Coneybeare 22/11/11
0

Use JOINs, não subconsultas. Alguma de sua tentativa de subconsulta ajudou?

MOSTRAR CRIAR TABELA sons \ G

MOSTRAR CREATE TABLE ratings \ G

Muitas vezes, é benéfico ter índices "compostos", não os de coluna única. Talvez INDEX (tipo, created_at)

Você está filtrando nas duas tabelas em um JOIN; isso provavelmente será um problema de desempenho.

Existem cerca de 1500 sons e 2 milhões de classificações.

Recomenda que você tenha um ID de incremento automático ratings, crie uma tabela de resumo e use o ID do AI para acompanhar onde você "parou". No entanto, não armazene médias em uma tabela de resumo:

avg (ratings.rating) AS avg_rating,

Em vez disso, mantenha o SUM (ratings.rating). A média das médias é matematicamente incorreta para calcular uma média; (soma de somas) / (soma de contagens) está correto.

Rick James
fonte