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 ?


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 !!!


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


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

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.