Come posso ottimizzare ulteriormente questa query MySQL?


9

Ho una query che impiega un tempo particolarmente lungo (15+ secondi) e peggiora con il tempo man mano che il mio set di dati cresce. In passato l'ho ottimizzato e ho aggiunto indici, ordinamento a livello di codice e altre ottimizzazioni, ma necessita di ulteriori perfezionamenti.

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

Lo scopo della query è quello di ottenere me sound ide la valutazione media dei suoni rilasciati più recenti. Ci sono circa 1500 suoni e 2 milioni di voti.

Ho diversi indici su 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 molti altri 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      |         | 
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

Ecco il 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 | 
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+-------------+

Cachemo i risultati una volta ottenuti, quindi le prestazioni del sito non rappresentano un grosso problema, ma i miei dispositivi di riscaldamento della cache impiegano sempre più tempo a funzionare a causa di questa chiamata che impiega così tanto tempo e questo sta iniziando a diventare un problema. Questo non sembra un sacco di numeri da scricchiolare in una query ...

Cosa posso fare di più per migliorare le prestazioni ?


Puoi mostrare l' EXPLAINoutput? 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 Questa è stata una sfida molto interessante per me oggi !!! +1 per la tua domanda. Vorrei che altre domande come questa arrivassero nel prossimo futuro.
RolandoMySQLDBA

@coneybeare Sembra che il nuovo EXPLAIN legga solo 21540 righe (359 X 60) invece di 2.008.306. Esegui EXPLAIN sulla query originariamente suggerita nella mia risposta. Vorrei vedere il numero di righe che ne derivano.
RolandoMySQLDBA

@RolandoMySQLDBA La nuova spiegazione mostra in effetti che una quantità minore di righe con l'indice, tuttavia, il tempo per eseguire la query era ancora di circa 15 secondi, senza mostrare alcun miglioramento
coneybeare

@coneybeare Ho messo a punto la query. Esegui EXPLAIN sulla mia nuova query. L'ho aggiunto alla mia risposta.
RolandoMySQLDBA

Risposte:


7

Dopo aver esaminato la query, le tabelle e le clausole WHERE AND GROUP BY, raccomando quanto segue:

Raccomandazione n. 1) Rifattorizzare la query

Ho riorganizzato la query per fare tre (3) cose:

  1. creare tabelle temporanee più piccole
  2. Elaborare la clausola WHERE su tali tabelle temporanee
  3. Ritardo ad aderire all'ultimo

Ecco la mia proposta di query:

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

Raccomandazione n. 2) Indicizzare la tabella dei suoni con un indice che ospiterà la clausola WHERE

Le colonne di questo indice includono tutte le colonne della clausola WHERE con i valori statici per primi e il target mobile per ultimo

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

Sinceramente credo che rimarrai piacevolmente sorpreso. Provaci !!!

AGGIORNAMENTO 2011-05-21 19:04

Ho appena visto la cardinalità. AHIA !!! Cardinalità di 1 per rateable_id. Ragazzo, mi sento stupido !!!

AGGIORNAMENTO 2011-05-21 19:20

Forse fare l'indice sarà sufficiente per migliorare le cose.

AGGIORNAMENTO 2011-05-21 22:56

Si prega di eseguire questo:

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

AGGIORNAMENTO 2011-05-21 23:34

L'ho rifattorizzato di nuovo. Prova questo per favore:

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
;

AGGIORNAMENTO 2011-05-21 23:55

L'ho rifattorizzato di nuovo. Prova questo per favore (ultima volta):

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;

AGGIORNAMENTO 2011-05-22 00:12

Odio arrendermi !!!!

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;

AGGIORNAMENTO 2011-05-22 07:51

Mi ha infastidito il fatto che le valutazioni stiano tornando con 2 milioni di righe in EXPLAIN. Quindi mi ha colpito. Potrebbe essere necessario un altro indice nella tabella delle valutazioni che inizia con rateable_type:

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

L'obiettivo di questo indice è ridurre la tabella temporanea che manipola le classificazioni in modo che sia inferiore a 2 milioni. Se riusciamo a ridurre significativamente quella tabella temporanea (almeno la metà), allora possiamo avere una speranza in più nella tua query e far funzionare il mio più velocemente.

Dopo aver creato quell'indice, riprova la mia query proposta originale e prova anche la tua:

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

AGGIORNAMENTO 2011-05-22 18:39: PAROLE FINALI

Avevo refactored una query in una procedura memorizzata e avevo aggiunto un indice per aiutare a rispondere a una domanda sull'accelerazione delle cose. Ho ricevuto 6 voti, ho accettato la risposta e ho ricevuto una taglia 200.

Avevo anche riformulato un'altra query (risultati marginali) e aggiunto un indice (risultati drammatici). Ho ottenuto 2 voti e ho accettato la risposta.

Ho aggiunto un indice per l'ennesima sfida di query e sono stato votato una volta

e ora la tua domanda .

Il voler rispondere a tutte queste domande (compresa la tua) è stato ispirato da un video di YouTube che ho visto su domande di refactoring.

Grazie ancora, @coneybeare !!! Volevo rispondere a questa domanda nella massima misura possibile, non solo accettare punti o riconoscimenti. Ora, sento di aver guadagnato i punti !!!


Ho aggiunto l'indice, nessun miglioramento nei tempi. Ecco il nuovo EXPLAIN: cloud.coneybeare.net/6y7c
coneybeare

EXPLAIN sulla query dalla raccomandazione 1: cloud.coneybeare.net/6xZ2 Ci sono voluti circa 30 secondi per eseguire questa query
coneybeare

Ho dovuto modificare leggermente la sintassi per qualche motivo (ho aggiunto un DA prima della prima query e ho dovuto eliminare l'alias AAA). Ecco l'esempio: cloud.coneybeare.net/6xlq L' esecuzione della query effettiva ha richiesto circa 30 secondi
coneybeare,

@RolandoMySQLDBA: EXPLAIN sull'aggiornamento 23:55: cloud.coneybeare.net/6wrN La query effettiva è durata più di un minuto, quindi ho
interrotto

La seconda selezione interna non può accedere alla tabella di selezione A, quindi A.id genera un errore.
Coneybeare,

3

Grazie per l'uscita EXPLAIN. Come puoi dire da questa affermazione, il motivo per cui sta impiegando così tanto tempo è la scansione completa della tabella delle valutazioni. Nulla nell'istruzione WHERE sta filtrando le 2 milioni di righe.

Potresti aggiungere un indice su rating.type, ma la mia ipotesi è che CARDINALITÀ sarà davvero basso e continuerai a scansionare un bel po 'di righe ratings.

In alternativa, puoi provare a utilizzare i suggerimenti sugli indici per forzare mysql a utilizzare gli indici dei suoni.

aggiornato:

Se fossi in me, aggiungerei un indice sounds.createdpoiché ha le migliori possibilità di filtrare le righe e probabilmente forzerò l'ottimizzatore di query mysql a utilizzare gli indici della tabella dei suoni. Fai attenzione alle query che utilizzano intervalli di tempo creati a lungo (1 anno, 3 mesi, dipende solo dalle dimensioni della tabella dei suoni).


Sembra che il tuo suggerimento sia stato notevole per @coneybeare. +1 anche da me.
RolandoMySQLDBA

L'indice creato non si è rasato in qualsiasi momento. Ecco l'EXPLAIN aggiornato. cloud.coneybeare.net/6xvc
coneybeare

2

Se questa deve essere una query disponibile "al volo" , ciò limita un po 'le tue opzioni.

Suggerirò di dividere e conquistare questo 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";

sembra che @coneybeare abbia visto qualcosa nel tuo suggerimento. +1 da me !!!
RolandoMySQLDBA

In realtà non sono riuscito a farlo funzionare. Stavo ricevendo errori sql che non ero sicuro di come affrontare. Non ho mai davvero lavorato con i tavoli temporanei
coneybeare,

Alla fine l'ho ricevuto (ho dovuto aggiungere FROM sounds, ratingsalla query centrale), ma ha bloccato la mia casella sql e ho dovuto terminare il processo.
Coneybeare,

0

Usa JOIN, non subquery. Qualcuno dei tuoi tentativi di subquery ti è stato d'aiuto?

MOSTRA CREA TABELLA suoni \ G

MOSTRA CREA TABELLA valutazioni \ G

Spesso è utile avere indici "composti", non quelli a colonna singola. Forse INDICE (tipo, creato_at)

Stai filtrando su entrambe le tabelle in un JOIN; è probabile che sia un problema di prestazioni.

Ci sono circa 1500 suoni e 2 milioni di voti.

Consiglia di avere un ID auto_increment attivo ratings, creare una tabella di riepilogo e utilizzare l'id AI per tenere traccia di dove "si era interrotto". Tuttavia, non memorizzare le medie in una tabella di riepilogo:

avg (rating.rating) AS avg_rating,

Invece, mantieni il SUM (rating.rating). La media delle medie è matematicamente errata per calcolare una media; (somma delle somme) / (somma dei conteggi) è corretta.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.