2012-12-13 7 views
15

Sto utilizzando Drupal 6 con MySQL versione 5.0.95 e in un'impasse in cui una delle mie query che visualizza i contenuti in base alla data di articolo più recente rallenta ea causa del la frequenza di utilizzo uccide del tutto le prestazioni del sito. La query in questione è il seguente:Impossibile ottimizzare la query MySQL che utilizza una clausola ORDER BY

 SELECT n.nid, 
      n.title, 
      ma.field_article_date_format_value, 
      ma.field_article_summary_value 
     FROM node n 
INNER JOIN content_type_article ma ON n.nid=ma.nid 
INNER JOIN term_node tn   ON n.nid=tn.nid 
     WHERE tn.tid= 153 
     AND n.status=1 
    ORDER BY ma.field_article_date_format_value DESC 
     LIMIT 0, 11; 

La EXPLAIN della query mostra il risultato qui sotto:

+----+-------------+-------+--------+--------------------------+---------+---------+----------------------+-------+---------------------------------+ 
| id | select_type | table | type | possible_keys   | key  | key_len | ref     | rows | Extra       | 
+----+-------------+-------+--------+--------------------------+---------+---------+----------------------+-------+---------------------------------+ 
| 1 | SIMPLE  | tn | ref | PRIMARY,nid    | PRIMARY | 4  | const    | 19006 | Using temporary; Using filesort | 
| 1 | SIMPLE  | ma | ref | nid,ix_article_date  | nid  | 4  | drupal_mm_stg.tn.nid |  1 |         | 
| 1 | SIMPLE  | n  | eq_ref | PRIMARY,node_status_type | PRIMARY | 4  | drupal_mm_stg.ma.nid |  1 | Using where      | 
+----+-------------+-------+--------+--------------------------+---------+---------+----------------------+-------+---------------------------------+ 

Questa query sembrava relativamente semplice e diretto e recupera gli articoli che appartengono ad una categoria (termine) 153 e sono di stato 1 (pubblicato). Ma a quanto pare Usando la tabella temporanea e Usando filesort si intende che la query è destinata a fallire da ciò che ho imparato navigando a riguardo.

La rimozione di field_article_date_format_value dalla clausola ORDER BY risolve l'utilizzo temporaneo; L'uso di filesort riduce il tempo di esecuzione della query ma è richiesto e non può essere scambiato, sfortunatamente lo stesso vale anche per le prestazioni del sito.

La mia impressione è che gran parte del problema deriva dalla tabella term_node che mappa gli articoli in categorie ed è una tabella di relazioni a molti molti che significa se l'articolo X è associato a 5 categorie C1 .... C5 avrà 5 voci in quella tabella, questa tabella proviene da drupal out-of-the-box.

lotta contro i contenuti pesanti DB è qualcosa di nuovo per me e passando attraverso alcune delle domande rivolte simili ( When ordering by date desc, "Using temporary" slows down query, MySQL performance optimization: order by datetime field) ho cercato di creare un indice composto per il content_type_article cui campo datetime viene utilizzato nella clausola ORDER BY lungo con un'altra chiave (nid) e tentò di forzare l'INDICE.

SELECT n.nid, n.title, 
      ma.field_article_date_format_value, 
      ma.field_article_summary_value 
     FROM node n 
INNER JOIN content_type_article ma FORCE INDEX (ix_article_date) ON n.nid=ma.nid 
INNER JOIN term_node tn ON n.nid=tn.nid 
    WHERE tn.tid= 153 
     AND n.status=1 
    ORDER BY ma.field_article_date_format_value DESC 
    LIMIT 0, 11; 

Il risultato e la seguente interrogazione EXPLAIN non sembra aiutare molto

+----+-------------+-------+--------+--------------------------+-----------------+---------+----------------------+-------+---------------------------------+ 
| id | select_type | table | type | possible_keys   | key    | key_len | ref     | rows | Extra       | 
+----+-------------+-------+--------+--------------------------+-----------------+---------+----------------------+-------+---------------------------------+ 
| 1 | SIMPLE  | tn | ref | PRIMARY,nid    | PRIMARY   | 4  | const    | 18748 | Using temporary; Using filesort | 
| 1 | SIMPLE  | ma | ref | ix_article_date   | ix_article_date | 4  | drupal_mm_stg.tn.nid |  1 |         | 
| 1 | SIMPLE  | n  | eq_ref | PRIMARY,node_status_type | PRIMARY   | 4  | drupal_mm_stg.ma.nid |  1 | Using where      | 
+----+-------------+-------+--------+--------------------------+-----------------+---------+----------------------+-------+---------------------------------+ 

campi n.nid, ca.nid, ma.field_article_date_format_value sono tutti indicizzati. Interrogare il DB con Limite 0,11 richiede circa 7-10 secondi con la clausola ORDER BY, ma senza di esso la query impiega a malapena un secondo. Il motore del database è MyISAM. Qualsiasi aiuto su questo sarebbe molto apprezzato.

Qualsiasi risposta che potrebbe aiutarmi a ottenere questa query come una normale (alla stessa velocità di una query senza ordinamento per data) sarebbe ottima. I miei tentativi con la creazione di una query composita come combinazione di nid e field_article_date_format_value e l'utilizzo nella query non hanno aiutato la causa. Sono aperto a fornire ulteriori informazioni sul problema e su eventuali nuovi suggerimenti.

risposta

2

MySQL sta "ottimizzando" la query in modo che selezioni prima dalla tabella term_node, anche se si specifica di selezionare prima dal nodo. Non conoscendo i dati, non sono sicuro quale sia il modo ottimale. La tabella term_node è certamente il punto in cui i problemi di rendimento sono dovuti al fatto che da lì vengono selezionati ~ 19.000 record.

I limiti senza ORDER BY sono quasi sempre più veloci perché MySQL si arresta non appena trova il limite specificato. Con un ordine BY, prima deve trovare tutti i record e ordinarli, quindi ottenere il limite specificato.

La semplice cosa da provare è spostare la condizione WHERE nella clausola JOIN, che è dove dovrebbe essere. Questo filtro è specifico per la tabella che viene unita. Ciò assicurerà che MySQL non lo ottimizzi in modo errato.

INNER JOIN term_node tn ON n.nid=tn.nid AND tn.tid=153 

Una cosa più complicata è fare un SELECT sulla tabella term_node e JOIN su quello. Si chiama DERIVED TABLE e lo vedrai definito come tale in EXPLAIN. Dato che hai detto che era un numero molti a molti, ho aggiunto un parametro DISTINCT per ridurre il numero di record su cui partecipare.

SELECT ... 
FROM node n 
INNER JOIN content_type_article ma FORCE INDEX (ix_article_date) ON n.nid=ma.nid 
INNER JOIN (SELECT DISTINCT nid FROM term_node WHERE tid=153) tn ON n.nid=tn.nid 
WHERE n.status=1 
ORDER BY ma.field_article_date_format_value DESC 
LIMIT 0,11 

MySQL 5.0 presenta alcune limitazioni con le tabelle derivate, pertanto potrebbe non funzionare. Anche se ci sono problemi di lavoro.

+0

Grazie per la risposta, sebbene il term_nodo sia N: N i nodi risultanti per un particolare termine saranno distinti nel mio caso .. Ho provato l'approccio della tabella derivata in precedenza ma l'esecuzione della query era quasi la stessa di quella convenzionale . – optimusprime619

4

Using temporary; Using filesort significa che MySQL deve solo creare una tabella dei risultati temporanea e ordinarla per ottenere il risultato desiderato. Questo è spesso una conseguenza del costrutto ORDER BY ... DESC LIMIT 0,n che stai utilizzando per ottenere gli ultimi post. Di per sé non è un segno di fallimento. Vedi questo: http://www.mysqlperformanceblog.com/2009/03/05/what-does-using-filesort-mean-in-mysql/

Ecco alcune cose da provare. Non sono del tutto sicuro che funzioneranno; è difficile sapere se i tuoi dati non sono sperimentabili.

Esiste un indice BTREE su content_type_article.field_article_date_format_value? Se è così, questo può aiutare.

Devi mostrare gli 11 articoli più recenti? Oppure puoi visualizzare gli 11 articoli più recenti che sono apparsi nell'ultima settimana o mese? In tal caso, è possibile aggiungere questa riga alla clausola WHERE. Esso filtra le tue cose per data invece di dover ricominciare dall'inizio del tempo per trovare gli articoli corrispondenti. Ciò sarà particolarmente utile se si dispone di un sito Drupal consolidato da molto tempo.

AND ma.field_article_date_format_value >= (CURRENT_TIME() - INTERVAL 1 MONTH) 

Innanzitutto, prova ad invertire l'ordine delle operazioni join interno. In secondo luogo, incorporare il tid = 153 nel criterio di join. Questo può ridurre la dimensione della tabella temporanea che è necessario ordinare. Tutti insieme i miei suggerimenti sono i seguenti:

SELECT n.nid, 
      n.title, 
      ma.field_article_date_format_value, 
      ma.field_article_summary_value 
     FROM node n 
INNER JOIN term_node tn   ON (n.nid=tn.nid AND tn.tid = 153) 
INNER JOIN content_type_article ma ON n.nid=ma.nid 
    WHERE n.status=1 
     AND ma.field_article_date_format_value >= (CURRENT_TIME() - INTERVAL 1 MONTH) 
    ORDER BY ma.field_article_date_format_value DESC 
    LIMIT 0, 11; 

Quelli sono

+0

Grazie per il feedback, sì, BTree è a posto, la mia preoccupazione è che c'è un modo per negare l'utilizzo della tabella temporanea e l'ordinamento con l'indicizzazione .. e la logica aziendale richiede che gli articoli più recenti vengano visualizzati piuttosto che una settimana o un mese – optimusprime619

6

Dando uno sguardo alla vostra query e il spiegare, sembra avere il n.status = 1 nella clausola in cui sta facendo la ricerca molto inefficiente perché è necessario restituire l'intero set definito dai join e quindi applicare lo stato = 1. Provare a iniziare il join dalla tabella term_node che viene immediatamente filtrata dal WHERE e quindi effettuare i join aggiungendo immediatamente la condizione di stato. Fare un tentativo e per favore dimmi come va.

SELECT n.nid, n.title, 
      ma.field_article_date_format_value, 
      ma.field_article_summary_value 
     FROM term_node tn 
INNER JOIN node n ON n.nid=tn.nid AND n.status=1 
INNER JOIN content_type_article ma FORCE INDEX (ix_article_date) ON n.nid=ma.nid 
    WHERE tn.tid= 153 
    ORDER BY ma.field_article_date_format_value DESC 
    LIMIT 0, 11; 
4

1) Copertura indici

Penso che la risposta può essere semplice "indici che coprono".

Soprattutto nella tabella content_type_article. L '"indice di copertura" ha l'espressione in ORDER BY come colonna principale e include tutte le colonne a cui fa riferimento la query.Ecco l'indice che ho creato (sul mio tavolo di prova):

CREATE INDEX ct_article_ix9 
    ON content_type_article 
     (field_article_date_format_value, nid, field_article_summary_value); 

Ed ecco un estratto del spiegano ottengo dalla query (dopo costruisco esempio tabelle, utilizzando il motore InnoDB, tra cui un indice di copertura su ogni tavolo):

_type table type key    ref   Extra      
------ ----- ----- -------------- ----------- ------------------------ 
SIMPLE ma index ct_article_ix9 NULL   Using index 
SIMPLE n ref node_ix9   ma.nid  Using where; Using index 
SIMPLE tn ref term_node_ix9 n.nid,const Using where; Using index 

noti che non c'è 'Using filesort' mostrato nel piano, e il piano mostra 'Using index' per ogni tabella con i riferimenti nella query, che in pratica significa che tutti i dati necessari dalla query viene recuperato dalle pagine di indice , senza bisogno di fare riferimento a nessuna pagina dalla tabella sottostante. (Le tabelle hanno molto più righe che le mie tabelle di prova, ma se è possibile ottenere un spiegare piano che assomiglia a questo, si possono ottenere prestazioni migliori.)


Per completezza, ecco l'intera produzione SPIEGARE:

+----+-------------+-------+-------+---------------+----------------+---------+---------------------+------+--------------------------+ 
| id | select_type | table | type | possible_keys | key   | key_len | ref     | rows | Extra     | 
+----+-------------+-------+-------+---------------+----------------+---------+-------- ------------+------+--------------------------+ 
| 1 | SIMPLE  | ma | index | NULL   | ct_article_ix9 | 27  | NULL    | 1 | Using index    | 
| 1 | SIMPLE  | n  | ref | node_ix9  | node_ix9  | 10  | testps.ma.nid,const | 11 | Using where; Using index | 
| 1 | SIMPLE  | tn | ref | term_node_ix9 | term_node_ix9 | 10  | testps.n.nid,const | 11 | Using where; Using index | 
+----+-------------+-------+-------+---------------+----------------+---------+---------------------+------+--------------------------+ 
3 rows in set (0.00 sec) 

ho fatto nessuna modifica alla tua ricerca, ad eccezione di omettere il FORCE INDEX suggerimento. Qui ci sono gli altri due "indici che coprono" che ho creato per le altre due tabelle si fa riferimento nella query:

CREATE INDEX node_ix9 
    ON node (`nid`,`status`,`title`); 

CREATE INDEX term_node_ix9 
    ON term_node (nid,tid); 

(Si noti che se nid è la chiave di clustering sul tavolo node, potrebbe non essere necessario l'indice di copertura sul tavolo nodo.)


2) Uso correlato subquery in luogo di join?

Se l'idea precedente non migliora nulla, allora, come un'altra alternativa, dal momento che la query originale sta tornando ad un massimo di 11 righe, si potrebbe provare a riscrivere la query per evitare le operazioni di unire, e invece fare uso di correlato sottoquery. Qualcosa come la query qui sotto.

Si noti che questa query differisce significativamente dalla query originale. La differenza è che con questa query, una riga della tabella context_type_article verrà restituita una sola volta. Con la query che utilizza i join, una riga da quella tabella potrebbe essere abbinata a più righe dalle tabelle node e term_node, che restituirebbero la stessa riga più di una volta. Questo può essere visto come desiderabile o indesiderabile, in realtà dipende dalla cardinalità e dal fatto che il set di risultati soddisfi le specifiche.

SELECT (SELECT n2.nid 
      FROM node n2 
      WHERE n2.nid = ma.nid 
      AND n2.status = 1 
      LIMIT 1 
     ) AS `nid` 
     , (SELECT n3.title 
      FROM node n3 
      WHERE n3.nid = ma.nid 
      AND n3.status = 1 
      LIMIT 1 
     ) AS `title` 
     , ma.field_article_date_format_value 
     , ma.field_article_summary_value 
    FROM content_type_article ma 
    WHERE EXISTS 
     (SELECT 1 
      FROM node n1 
      WHERE n1.nid = ma.nid 
      AND n1.status = 1 
     )     
    AND EXISTS 
     (SELECT 1 
      FROM term_node tn 
      WHERE tn.nid = ma.nid 
      AND tn.tid = 153 
     ) 
    ORDER BY ma.field_article_date_format_value DESC 
    LIMIT 0,11 

(A volte, una query utilizzando questo tipo di "sottoquery orrelated" possono avere prestazioni notevolmente peggiore di una query equivalente che fa aderire le operazioni. Ma in alcuni casi, una query come questo può effettivamente funzionare meglio, soprattutto in considerazione un numero molto limitato di righe da restituire)

Ecco l'output spiegare per la query:.

+----+--------------------+-------+-------+---------------+----------------+---------+---------------------+------+--------------------------+ 
| id | select_type  | table | type | possible_keys | key   | key_len | ref     | rows | Extra     | 
+----+--------------------+-------+-------+---------------+----------------+---------+---------------------+------+--------------------------+ 
| 1 | PRIMARY   | ma | index | NULL   | ct_article_ix9 | 27  | NULL    | 11 | Using where; Using index | 
| 5 | DEPENDENT SUBQUERY | tn | ref | term_node_ix9 | term_node_ix9 | 10  | testps.ma.nid,const | 13 | Using where; Using index | 
| 4 | DEPENDENT SUBQUERY | n1 | ref | node_ix9  | node_ix9  | 10  | testps.ma.nid,const | 12 | Using where; Using index | 
| 3 | DEPENDENT SUBQUERY | n3 | ref | node_ix9  | node_ix9  | 10  | testps.ma.nid,const | 12 | Using where; Using index | 
| 2 | DEPENDENT SUBQUERY | n2 | ref | node_ix9  | node_ix9  | 10  | testps.ma.nid,const | 12 | Using where; Using index | 
+----+--------------------+-------+-------+---------------+----------------+---------+---------------------+------+--------------------------+ 
5 rows in set (0.00 sec) 

noti che nuovamente, ogni accesso è 'Using index', il che significa che l'interrogazione è soddisfatta direttamente dalle pagine di indice, rathe r di dover visitare qualsiasi pagina di dati nella tabella sottostante.


tabelle Esempio

Qui ci sono le tabelle di esempio (insieme con gli indici) che ho costruito e popolato, sulla base delle informazioni dalla tua domanda:

CREATE TABLE `node` (`id` INT PRIMARY KEY, `nid` INT, `title` VARCHAR(10),`status` INT); 
CREATE INDEX node_ix9 ON node (`nid`,`status`,`title`); 
INSERT INTO `node` VALUES (1,1,'foo',1),(2,2,'bar',0),(3,3,'fee',1),(4,4,'fi',0),(5,5,'fo',1),(6,6,'fum',0),(7,7,'derp',1); 
INSERT INTO `node` SELECT id+7,nid+7,title,`status` FROM node; 
INSERT INTO `node` SELECT id+14,nid+14,title,`status` FROM node; 
INSERT INTO `node` SELECT id+28,nid+28,title,`status` FROM node; 
INSERT INTO `node` SELECT id+56,nid+56,title,`status` FROM node; 

CREATE TABLE content_type_article (id INT PRIMARY KEY, nid INT, field_article_date_format_value DATETIME, field_article_summary_value VARCHAR(10)); 
CREATE INDEX ct_article_ix9 ON content_type_article (field_article_date_format_value, nid, field_article_summary_value); 
INSERT INTO content_type_article VALUES (1001,1,'2012-01-01','foo'),(1002,2,'2012-01-02','bar'),(1003,3,'2012-01-03','fee'),(1004,4,'2012-01-04','fi'),(1005,5,'2012-01-05','fo'),(1006,6,'2012-01-06','fum'),(1007,7,'2012-01-07','derp'); 
INSERT INTO content_type_article SELECT id+7,nid+7, DATE_ADD(field_article_date_format_value,INTERVAL 7 DAY),field_article_summary_value FROM content_type_article; 
INSERT INTO content_type_article SELECT id+14,nid+14, DATE_ADD(field_article_date_format_value,INTERVAL 14 DAY),field_article_summary_value FROM content_type_article; 
INSERT INTO content_type_article SELECT id+28,nid+28, DATE_ADD(field_article_date_format_value,INTERVAL 28 DAY),field_article_summary_value FROM content_type_article; 
INSERT INTO content_type_article SELECT id+56,nid+56, DATE_ADD(field_article_date_format_value,INTERVAL 56 DAY),field_article_summary_value FROM content_type_article; 

CREATE TABLE term_node (id INT, tid INT, nid INT); 
CREATE INDEX term_node_ix9 ON term_node (nid,tid); 
INSERT INTO term_node VALUES (2001,153,1),(2002,153,2),(2003,153,3),(2004,153,4),(2005,153,5),(2006,153,6),(2007,153,7); 
INSERT INTO term_node SELECT id+7, tid, nid+7 FROM term_node; 
INSERT INTO term_node SELECT id+14, tid, nid+14 FROM term_node; 
INSERT INTO term_node SELECT id+28, tid, nid+28 FROM term_node; 
INSERT INTO term_node SELECT id+56, tid, nid+56 FROM term_node; 
1

È davvero vuole evitare l'operazione di ordinamento avviene del tutto se è possibile sfruttando un indice preordinato.

Per scoprire se è possibile, immagina i tuoi dati denormalizzati in un'unica tabella e assicurati che tutto ciò che deve essere incluso nella tua clausola WHERE sia specificabile con un VALORE UNICO. per esempio. se è necessario utilizzare una clausola IN su una delle colonne, l'ordinamento è inevitabile.

Ecco uno screenshot di alcuni dati di esempio:

Sample data denormalised and sorted by tid, status DESC, date DESC

Quindi, se avete avuto i vostri dati denormalizzati, si potrebbe interrogare sulla tid e lo stato utilizzando i valori singoli e poi ordina per data discendente. Ciò significherebbe il seguente indice in quel caso avrebbe funzionato perfettamente:

create index ix1 on denormalisedtable(tid, status, date desc); 

Se tu avessi questo, la query avrebbe colpito solo i primi 10 righe e non avrebbe mai bisogno di ordinare.

Così - come si fa a ottenere le stesse prestazioni SENZA denormalizzare ...

penso che si dovrebbe essere in grado di utilizzare la clausola STRAIGHT_JOIN per imporre l'ordine che MySQL seleziona dalle tabelle - si vuole farlo seleziona dal tavolo che stai ordinando per ultimo.

Prova questo:

SELECT n.nid, 
     n.title, 
     ma.field_article_date_format_value, 
     ma.field_article_summary_value 
FROM node n 
STRAIGHT_JOIN term_node tn   ON n.nid=tn.nid 
STRAIGHT_JOIN content_type_article ma ON n.nid=ma.nid 
WHERE tn.tid= 153 
    AND n.status=1 
ORDER BY ma.field_article_date_format_value DESC 
LIMIT 0, 11; 

L'idea è di ottenere MySQL per selezionare dal tavolo nodo e poi dal tavolo term_node e poi finalmente dalla tabella content_type_article (la tabella che contiene la colonna si ordina on) .

Quest'ultima join è il tuo più importante e si desidera che accada utilizzando un indice in modo che la clausola LIMIT in grado di lavorare senza bisogno di ordinare i dati.

Questo unico indice potrebbe fare il trucco:

create index ix1 on content_type_article(nid, field_article_date_format_value desc); 

o

create index ix1 on content_type_article(nid, field_article_date_format_value desc, field_article_summary_value); 

(per un indice di copertura)

dico potrebbe, perché io non ne so abbastanza circa il MySQL Optimizer per sapere se è abbastanza intelligente da gestire i molteplici valori di colonna "nid" che verranno inseriti nel content_type_article senza dover ricorrere ai dati.

A rigor di logica, dovrebbe essere in grado di lavorare in fretta - per esempio se 5 valori nid vengono inseriti nella tabella content_type_article finale, allora dovrebbe essere in grado di ottenere i primi 10 di ciascuno direttamente dall'indice e unire i risultati insieme, quindi scegliere la top 10 finale, cioè un totale di 50 righe lette da questo tabella insted del 19006 completo che stai vedendo attualmente.

Fammi sapere come va.

Se funziona per voi, sarà possibile ottimizzare ulteriormente utilizzando indici di copertura sulle altre tabelle per accelerare i primi due join.