Mysql
 sql >> Database >  >> RDS >> Mysql

Come posso ottimizzare ulteriormente una query di tabella derivata che ha prestazioni migliori rispetto all'equivalente JOINed?

Bene, ho trovato una soluzione. Ci sono volute molte sperimentazioni e penso un po' di fortuna cieca, ma eccola qui:

CREATE TABLE magic ENGINE=MEMORY
SELECT
  s.shop_id AS shop_id,
  s.id AS shift_id,
  st.dow AS dow,
  st.start AS start,
  st.end AS end,
  su.user_id AS manager_id
FROM shifts s
JOIN shift_times st ON s.id = st.shift_id
JOIN shifts_users su ON s.id = su.shift_id
JOIN shift_positions sp ON su.shift_position_id = sp.id AND sp.level = 1

ALTER TABLE magic ADD INDEX (shop_id, dow);

CREATE TABLE tickets_extra ENGINE=MyISAM
SELECT 
  t.id AS ticket_id,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.created) = m.dow
    AND TIME(t.created) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_created,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.resolved) = m.dow
    AND TIME(t.resolved) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_resolved
FROM tickets t;
DROP TABLE magic;

Spiegazione lunga

Ora, spiegherò perché funziona e il mio processo e i passaggi per arrivare qui.

Innanzitutto, sapevo che la query che stavo provando soffriva a causa dell'enorme tabella derivata e dei successivi JOIN su questa. Stavo prendendo la mia tabella dei biglietti ben indicizzata e unendo su di essa tutti i dati shift_times, quindi lasciando che MySQL masticasse quello mentre tentava di unirsi alla tabella shift e shift_positions. Questo colosso derivato sarebbe fino a un pasticcio non indicizzato di 2 milioni di righe.

Ora, sapevo che stava succedendo. Il motivo per cui stavo seguendo questa strada era perché il modo "corretto" per farlo, usando rigorosamente JOINs, richiedeva una quantità di tempo ancora più lunga. Ciò è dovuto al pessimo caos necessario per determinare chi è il manager di un determinato turno. Devo unirmi a shift_times per scoprire qual è anche il turno corretto, mentre contemporaneamente mi unisco a shift_positions per capire il livello dell'utente. Non credo che l'ottimizzatore MySQL lo gestisca molto bene e finisca per creare un'ENORME mostruosità di una tabella temporanea dei join, filtrando quindi ciò che non si applica.

Quindi, poiché la tabella derivata sembrava essere la "strada da percorrere", ho ostinatamente insistito su questo per un po'. Ho provato a puntarlo in una clausola JOIN, nessun miglioramento. Ho provato a creare una tabella temporanea con la tabella derivata al suo interno, ma ancora una volta era troppo lento poiché la tabella temporanea non era indicizzata.

Mi sono reso conto che dovevo gestire questo calcolo di turni, tempi, posizioni in modo sano. Ho pensato, forse una VISTA sarebbe stata la strada da percorrere. E se creassi una VIEW che contenesse queste informazioni:(shop_id, shift_id, dow, start, end, manager_id). Quindi, dovrei semplicemente unirmi alla tabella dei biglietti tramite shop_id e l'intero calcolo DAYOFWEEK/TIME, e sarei in affari. Ovviamente, non ricordavo che MySQL gestisce le VIEW in modo piuttosto assiduo. Non li materializza affatto, esegue semplicemente la query che avresti usato per ottenere la vista per te. Quindi, unendo i ticket a questo, stavo essenzialmente eseguendo la mia query originale, nessun miglioramento.

Quindi, invece di una VIEW ho deciso di utilizzare una TAVOLA TEMPORANEA. Funzionava bene se recuperavo solo uno dei gestori (creato o risolto) alla volta, ma era comunque piuttosto lento. Inoltre, ho scoperto che con MySQL non è possibile fare riferimento alla stessa tabella due volte nella stessa query (dovrei unirmi alla mia tabella temporanea due volte per poter distinguere tra manager_created e manager_resolved). Questo è un grande WTF, dato che posso farlo fintanto che non specifichi "TEMPORARY" - è qui che è entrato in gioco CREATE TABLE magic ENGINE=MEMORY.

Con questa pseudo tabella temporanea in mano, ho provato di nuovo il mio JOIN solo per manager_created. Ha funzionato bene, ma ancora piuttosto lento. Tuttavia, quando mi sono unito di nuovo per ottenere manager_resolved nella stessa query, il tempo della query è tornato nella stratosfera. Osservando l'EXPLAIN è stata mostrata la scansione completa dei biglietti da tavolo (righe ~ 2 mln), come previsto, e i JOIN sul tavolo magico a ~ 2.087 ciascuno. Ancora una volta, sembrava che stavo andando incontro a un errore.

Ora ho iniziato a pensare a come evitare del tutto i JOIN ed è allora che ho trovato un oscuro post sulla bacheca di messaggi antichi in cui qualcuno suggeriva di utilizzare le sottoselezioni (non riesco a trovare il collegamento nella mia cronologia). Questo è ciò che ha portato alla seconda query SELECT mostrata sopra (quella di creazione biglietti_extra). Nel caso della selezione di un solo campo manager, ha funzionato bene, ma ancora una volta con entrambi è stata una schifezza. Ho guardato EXPLAIN e ho visto questo:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 173825
        Extra: 
*************************** 2. row ***************************
           id: 3
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
*************************** 3. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
3 rows in set (0.00 sec)

Ack, la temuta SUBQUERY DIPENDENTE. Viene spesso suggerito di evitarli, poiché MySQL di solito li esegue in modo outside-in, eseguendo la query interna per ogni riga di quella esterna. L'ho ignorato e mi sono chiesto:"Beh... e se avessi indicizzato questo stupido tavolo magico?". Nasce così l'indice ADD (shop_id, dow).

Dai un'occhiata:

mysql> CREATE TABLE magic ENGINE=MEMORY
<snip>
Query OK, 3220 rows affected (0.40 sec)

mysql> ALTER TABLE magic ADD INDEX (shop_id, dow);
Query OK, 3220 rows affected (0.02 sec)

mysql> CREATE TABLE tickets_extra ENGINE=MyISAM
<snip>
Query OK, 1933769 rows affected (24.18 sec)

mysql> drop table magic;
Query OK, 0 rows affected (0.00 sec)

Ora QUESTO È di cosa sto parlando!

Conclusione

Questa è sicuramente la prima volta che creo al volo una tabella non TEMPORANEA e la indico al volo, semplicemente per eseguire una singola query in modo efficiente. Immagino di aver sempre pensato che l'aggiunta di un indice al volo fosse un'operazione proibitivamente costosa. (L'aggiunta di un indice sulla tabella dei miei biglietti di 2 milioni di righe può richiedere più di un'ora). Eppure, per sole 3.000 righe è un gioco da ragazzi.

Non aver paura delle SUBQUERIE DIPENDENTI, della creazione di tabelle TEMPORANEE che in realtà non lo sono, dell'indicizzazione al volo o degli alieni. Possono essere tutte cose buone nella giusta situazione.

Grazie per tutto l'aiuto StackOverflow. MrGreen