2013-05-05 17 views
14

Ho due tabelle:Come rendere la query JOIN utilizzare l'indice?

CREATE TABLE `articles` (
    `id` int(11) NOT NULL AUTO_INCREMENT, 
    `title` varchar(1000) DEFAULT NULL, 
    `last_updated` datetime DEFAULT NULL, 
    PRIMARY KEY (`id`), 
    KEY `last_updated` (`last_updated`), 
) ENGINE=InnoDB AUTO_INCREMENT=799681 DEFAULT CHARSET=utf8 

CREATE TABLE `article_categories` (
    `article_id` int(11) NOT NULL DEFAULT '0', 
    `category_id` int(11) NOT NULL DEFAULT '0', 
    PRIMARY KEY (`article_id`,`category_id`), 
    KEY `category_id` (`category_id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 | 

Questa è la mia domanda:

SELECT a.* 
FROM 
    articles AS a, 
    article_categories AS c 
WHERE 
    a.id = c.article_id 
    AND c.category_id = 78 
    AND a.comment_cnt > 0 
    AND a.deleted = 0 
ORDER BY a.last_updated 
LIMIT 100, 20 

E un EXPLAIN per esso:

*************************** 1. row *************************** 
      id: 1 
    select_type: SIMPLE 
     table: a 
     type: index 
possible_keys: PRIMARY 
      key: last_updated 
     key_len: 9 
      ref: NULL 
     rows: 2040 
     Extra: Using where 
*************************** 2. row *************************** 
      id: 1 
    select_type: SIMPLE 
     table: c 
     type: eq_ref 
possible_keys: PRIMARY,fandom_id 
      key: PRIMARY 
     key_len: 8 
      ref: db.a.id,const 
     rows: 1 
     Extra: Using index 

Esso utilizza un indice di scansione completa di last_updated al primo tabella per l'ordinamento, ma non usa un indice y per il join (type: index in spiegare). Questo è molto negativo per le prestazioni e uccide l'intero server del database, poiché si tratta di una query molto frequente.

Ho provato l'ordine del tavolo di inversione con STRAIGHT_JOIN, ma questo dà filesort, using_temporary, che è anche peggio.

C'è un modo per rendere mysql l'indice di utilizzo per l'unione e per l'ordinamento allo stesso tempo?

aggiornamento === ===

Sono davvero desparate in questo. Forse una sorta di denormalizzazione può aiutare qui?

+0

il tuo shcema non corrisponde alla query o al piano. http://sqlfiddle.com/#!9/bccd7/2. – Jodrell

+0

inizia specificando http://sqlfiddle.com/#!9/ bccd7/5 – Jodrell

+0

fiddle forse più utile http://sqlfiddle.com/#!9/bccd7/7 – Jodrell

risposta

14

Se si dispone di molte categorie, questa query non può essere resa efficiente. Nessun singolo indice può coprire due tabelle contemporaneamente in MySQL.

quello che dovete fare denormalizzazione: aggiungere last_updated, has_comments e deleted in article_categories:

CREATE TABLE `article_categories` (
    `article_id` int(11) NOT NULL DEFAULT '0', 
    `category_id` int(11) NOT NULL DEFAULT '0', 
    `last_updated` timestamp NOT NULL, 
    `has_comments` boolean NOT NULL, 
    `deleted` boolean NOT NULL, 
    PRIMARY KEY (`article_id`,`category_id`), 
    KEY `category_id` (`category_id`), 
    KEY `ix_articlecategories_category_comments_deleted_updated` (category_id, has_comments, deleted, last_updated) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 

ed eseguire questa query:

SELECT * 
FROM (
     SELECT article_id 
     FROM article_categories 
     WHERE (category_id, has_comments, deleted) = (78, 1, 0) 
     ORDER BY 
       last_updated DESC 
     LIMIT 100, 20 
     ) q 
JOIN articles a 
ON  a.id = q.article_id 

Ovviamente si deve aggiornare article_categories così ogni volta che si aggiorna rilevanti colonne in article. Questo può essere fatto in un trigger.

Si noti che la colonna has_comments è booleana: ciò consentirà l'utilizzo di un predicato di uguaglianza per eseguire una scansione a intervallo singolo sull'indice.

Si noti inoltre che lo LIMIT entra nella sottoquery. Questo fa sì che MySQL usi le ricerche in late row che non usa di default. Si veda questo articolo nel mio blog sul perché fanno aumentare le prestazioni:

Se foste su SQL Server, si potrebbe creare una vista indicizzabili su vostra richiesta, che essenzialmente sarebbe un denormalizzato indicizzato copia di article_categories con i campi aggiuntivi, gestiti automaticamente dal server.

Sfortunatamente, MySQL non supporta questo e sarà necessario creare manualmente tale tabella e scrivere codice aggiuntivo per mantenerlo sincronizzato con le tabelle di base.

+0

Questo sembra molto promettente, lo sto segnalando ora! Se "limite" è fuori dall'intervallo, ricevo un avvertimento 'Impossibile DOVE è stato notato dopo aver letto le tabelle const ', è sicuro ignorarlo? –

+1

@SilverLight: certo, si vede solo che si sta tentando di selezionare oltre il limite – Quassnoi

+0

Questa soluzione era la migliore, per quanto riguarda le prestazioni. Naturalmente, ho dovuto sopportare il dolore di modificare i trigger di scrittura dello schema, ma ora la mia query utilizza l'indice, nessuna tabella temporanea, nessun fileort, il tempo medio è 0.00. Grazie. –

2

L'utilizzo di un indice di non copertura è costoso. Per ogni riga, tutte le colonne scoperte devono essere recuperate dalla tabella di base, utilizzando la chiave primaria. Quindi per prima cosa proverei a rendere l'indice su articles che copre. Ciò potrebbe aiutare a convincere MySQL Query Optimizer che l'indice è utile. Per esempio:

KEY IX_Articles_last_updated (last_updated, id, title, comment_cnt, deleted), 

Se il problema persiste, si potrebbe giocare in giro con FORCE INDEX:

SELECT a.* 
FROM article_categories AS c FORCE INDEX (IX_Articles_last_updated) 
JOIN articles AS a FORCE INDEX (PRIMARY) 
ON  a.id = c.article_id 
WHERE c.category_id = 78 
     AND a.comment_cnt > 0 
     AND a.deleted = 0 
ORDER BY 
     a.last_updated 
LIMIT 100, 20 

Il nome dell'indice far rispettare la chiave primaria è sempre "primario".

+0

È un errore che MySQL non ha incluso, ma le colonne non indicizzate come SQL servono. – siride

+0

Grazie, ho ho appena provato a sperimentare con indici di copertura e 'force index' ma senza fortuna. Finché c'è un "ORDER BY", mysql non userà l'indice per nessun join, anche se forzato ... –

+0

L'utilizzo dell'indice non è sempre una vittoria, ma anche la query dovrebbe essere più veloce in termini di query e meno afflitta da risorse per il database. – flaschenpost

1

avrei i seguenti indici disponibili

articoli da tavolo - INDEX (soppresso, LAST_UPDATED, comment_cnt)

tavolo article_categories - INDEX (article_id, category_id) - hai già questo indice

quindi aggiungere Straight_Join per forzare l'esecuzione della query come elencato anziché tentare di utilizzare la tabella article_categories tramite le statistiche eventualmente necessarie per aiutare la query.

SELECT STRAIGHT_JOIN 
     a.* 
    FROM 
     articles AS a 
     JOIN article_categories AS c 
      ON a.id = c.article_id 
      AND c.category_id = 78 
    WHERE 
      a.deleted = 0 
     AND a.comment_cnt > 0 
    ORDER BY 
     a.last_updated 
    LIMIT 
     100, 20 

Come da commento/feedback, vorrei prendere in considerazione inversione basata sul set se la categoria è record base molto più piccolo ... come

SELECT STRAIGHT_JOIN 
     a.* 
    FROM 
     article_categories AS c 
     JOIN articles as a 
      ON c.article_id = a.id 
      AND a.deleted = 0 
      AND a.Comment_cnt > 0 
    WHERE 
     c.category_id = 78 
    ORDER BY 
     a.last_updated 
    LIMIT 
     100, 20 

In questo caso, vorrei assicurare un indice sulla tavolo articoli per indice

- (id, cancellati, LAST_UPDATED)

+0

Questo sembra buono! Ora ho il tipo di query 'ref', con' rows: 409896' (metà della tabella filtrata dal campo 'deleted'). inoltre, ho notato, che lo stesso risultato può essere raggiunto se ho omesso 'comment_cnt' da un indice (non è usato). C'è un modo per includere 'id' nell'indice? –

+0

@SilverLight, aggiungere l'ID all'indice non è importante se è la tabella primaria (come in questo) ENTRARE alla seconda tabella (figlio), e stai già guardando ogni colonna nella tabella degli articoli della tabella, è lì per il join. POTREBBE aggiungerlo, ma vorrei aggiungere come ultima colonna e mantenere lo stato cancellato in primo piano. – DRapp

+0

@SilverLight Un'altra opzione ... hai 409k articoli non cancellati. Vorrei mettere in discussione quanti articoli sono associati con ID di categoria = 78. Potresti considerare di invertire la query per ottenere la categoria 78 PRIMA, quindi unirti agli articoli (ad esempio, solo 38k articoli sono 78 indipendentemente dallo stato eliminato, sei ora 1/10 del record risultato impostato per ulteriori considerazioni: – DRapp

7

Prima di arrivare alla tua richiesta specifica, è importante capire come funziona un indice.

Con le statistiche appropriate, questa query:

select * from foo where bar = 'bar' 

... potrà utilizzare un indice su foo(bar) se è selettiva. Ciò significa che se bar = 'bar' equivale a selezionare la maggior parte delle righe della tabella, sarà più veloce leggere semplicemente la tabella ed eliminare le righe che non si applicano. Al contrario, se bar = 'bar' significa solo selezionare una manciata di righe, leggere l'indice ha senso.

Supponiamo ora gettare in una clausola ordine e che hai indici su ciascuno dei foo(bar) e foo(baz):

select * from foo where bar = 'bar' order by baz 

Se bar = 'bar' è molto selettivo, è a buon mercato per afferrare tutte le righe che soddisfano, e per ordinare loro in memoria. Se non è affatto selettivo, l'indice su foo(baz) ha poco senso perché si recupera comunque l'intera tabella: utilizzarlo significherebbe andare avanti e indietro sulle pagine del disco per leggere le righe in ordine, il che è molto costoso.

Toss in una clausola limite, tuttavia, e foo(baz) potrebbe improvvisamente avere senso:

select * from foo where bar = 'bar' order by baz limit 10 

Se bar = 'bar' è molto selettivo, è ancora una buona opzione. Se non è affatto selettivo, troverai rapidamente 10 righe corrispondenti scansionando l'indice su foo(baz) - potresti leggere 10 righe o 50, ma ne troverai presto dieci buone.

Supponiamo che quest'ultima query con indici su foo(bar, baz) e foo(baz, bar) invece. Gli indici vengono letti da sinistra a destra. Uno ha un buon senso per questa potenziale domanda, l'altro potrebbe non produrne affatto. Pensare a loro in questo modo:

bar baz baz bar 
--------- --------- 
bad aaa aaa bad 
bad bbb aaa bar 
bar aaa bbb bad 
bar bbb bbb bar 

Come si può vedere, l'indice foo(bar, baz) permette di iniziare a leggere a ('bar', 'aaa') e recupero le righe in ordine da quel punto in avanti.

L'indice su foo(baz, bar), al contrario, restituisce righe ordinate per baz indipendentemente da ciò che potrebbe contenere bar. Se bar = 'bar' non è affatto selettivo come criterio, si eseguirà rapidamente le righe corrispondenti per la query, nel qual caso ha senso utilizzarlo. Se è molto selettivo, potresti finire per iterare migliaia di righe di righe prima di trovare una corrispondenza sufficiente a corrispondere a bar = 'bar' - potrebbe comunque essere una buona opzione, ma è altrettanto ottimale.

Con quello che è indirizzato, torniamo alla tua richiesta originale ...

Devi iscriverti articoli con le categorie, per filtrare gli articoli che si trovano in una particolare categoria, con più di un commento, che aren' t cancellato, quindi li ordina in base alla data, quindi ne prende una manciata.

Suppongo che la maggior parte degli articoli non vengano eliminati, quindi un indice su tali criteri non sarà molto utile: rallenterà solo le scritture e la pianificazione delle query.

Suppongo che la maggior parte degli articoli abbia un commento o più, quindi anche questo non sarà selettivo. Cioè non c'è nemmeno bisogno di indicizzarlo.

Senza il filtro di categoria, le opzioni dell'indice sono ragionevolmente evidenti: articles(last_updated); possibilmente con la colonna conteggio dei commenti a destra e il flag eliminato a sinistra.

Con il filtro di categoria, tutto dipende ...

Se il filtro di categoria è molto selettivo, in realtà rende molto buon senso per selezionare tutte le righe che si trovano in tale categoria, ordinarli in memoria, e raccogliere le prime file corrispondenti.

Se il filtro di categoria non è affatto selettiva e produce quasi l'articolo, l'indice articles(last_update) ha un senso: le righe sono validi in tutto il luogo, in modo da leggere righe in ordine fino a trovare abbastanza che partita e voilà.

Nel caso più generale, è solo vagamente selettivo. Per quanto ne so, le statistiche raccolte non esaminano molto le correlazioni. Pertanto, il pianificatore non ha un buon modo di stimare se troverà articoli con la categoria giusta abbastanza veloce da meritare la lettura del secondo indice. Unire e ordinare in memoria di solito costa meno, quindi il pianificatore va con quello.

In ogni caso, hai due opzioni per forzare l'uso di un indice.

Uno è quello di riconoscere che il pianificatore query non è perfetto e di utilizzare un suggerimento:

http://dev.mysql.com/doc/refman/5.5/en/index-hints.html

essere cauti, però, perché a volte il progettista è in realtà corretto nel non voler utilizzare l'indice si' mi piace o vice versione. Inoltre, potrebbe diventare corretto in una versione futura di MySQL, quindi tienilo a mente man mano che mantieni il tuo codice nel corso degli anni.

Modifica: STRAIGHT_JOIN, come segnala anche DRap, con avvertimenti simili.

L'altro consiste nel mantenere una colonna aggiuntiva per contrassegnare gli articoli selezionati di frequente (ad esempio un campo tinyint, impostato su 1 quando appartengono alla categoria specifica), quindi aggiungere un indice su es. articles(cat_78, last_updated). Mantenerlo utilizzando un trigger e lo farai.

+0

Quindi, qual è la vostra soluzione effettiva per la domanda Personalmente, non ho sempre apprezzato gli esempi di "barra" "barra" forniti dalle persone, ma sono solo io. – DRapp

+0

Se si esegue spesso questa query particolare , Suggerirei di aggiungere un campo 'cat_78' come discusso nella mia conclusione, e indice' (cat_78, last_updated) ', e di eliminare completamente il join.Se (per la discussione con DRap) eliminato è selettivo, index anche questo: '(cancellato, cat_78, last_updated)'. O vi daranno risultati migliori che fanno l'effettivo join per quella particolare query –

+0

Al contrario, se eseguite query simili con tutte le categorie, il meglio che potete fare è e fare affidamento sul pianificatore o forzare l'ordine di join/l'uso della scelta dell'indice, sapendo che in alcuni casi non si sarà in grado di farlo correttamente. (Infine, e l'agenda di Postgres offre spesso piani più ottimali, perché raccoglie statistiche migliori ...) –

1

Prima di tutto, consiglierei di leggere l'articolo 3 ways MySQL uses indexes.

E ora, quando si conoscono le basi, è possibile ottimizzare questa particolare query.

MySQL non è in grado di utilizzare l'indice per l'ordine, ma è in grado di generare dati in un ordine di un indice. Poiché MySQL utilizza cicli annidati per l'unione, il campo che si desidera ordinare dovrebbe trovarsi nella prima tabella del join (si vede l'ordine di join nei risultati EXPLAIN e può influenzarlo creando indici specifici e (se non aiuta) forzando gli indici richiesti).

Un'altra cosa importante è che prima di ordinare recuperare tutte le colonne per tutte le righe filtrate dalla tabella a e saltare probabilmente la maggior parte di esse. È molto più efficiente ottenere un elenco di ID di riga richiesti e recuperare solo quelle righe.

Per fare questo lavoro avrete bisogno di un indice di copertura (deleted, comment_cnt, last_updated) sul tavolo a, e ora si può riscrivere la query come segue:

SELECT * 
FROM (
    SELECT a.id 
    FROM articles AS a, 
    JOIN article_categories AS c 
    ON a.id = c.article_id AND c.category_id = 78 
    WHERE a.comment_cnt > 0 AND a.deleted = 0 
    ORDER BY a.last_updated 
    LIMIT 100, 20 
) as ids 
JOIN articles USING (id); 

P.S.La definizione della tabella per la tabella a non contiene la colonna comment_cnt;)

2

È possibile utilizzare l'influenza MySQL da usare TASTI o INDICI

Per

  • Ordinare, o
  • Raggruppamento, o
  • registrazione

Per ulteriori informazioni, seguire this link. Intendevo usarlo per partecipare (ovvero USE INDEX FOR JOIN (My_Index) ma non funzionava come previsto. La rimozione della parte FOR JOIN velocizzava notevolmente la mia query, da più di 3,5 ore a 1-2 secondi. Semplicemente perché MySQL era costretto a usare il diritto index

+0

questo è assolutamente sorprendente e può aumentare la velocità di multipli. – Blauhirn