Un indice deve coprire tutte le colonne selezionate per poter essere utilizzato per ORDER BY?


15

Alla SO, qualcuno ha recentemente chiesto Perché ORDER BY non utilizza l'indice?

La situazione riguardava una semplice tabella InnoDB in MySQL composta da tre colonne e 10k righe. Una delle colonne, un numero intero, è stata indicizzata e l'OP ha cercato di recuperare l'intera tabella ordinata su quella colonna:

SELECT * FROM person ORDER BY age

Ha allegato l' EXPLAINoutput mostrando che questa query è stata risolta con un filesort(anziché l'indice) e ha chiesto perché sarebbe stato.

Nonostante il suggerimento FORCE INDEX FOR ORDER BY (age) causando l'indice da utilizzare , qualcuno ha risposto (con supporto commenti / upvotes da altri) che un indice è usato solo per l'ordinamento quando le colonne selezionate sono tutti lettura dall'indice (cioè come normalmente indicato con Using indexnella Extracolonna di EXPLAINuscita). In seguito è stata fornita una spiegazione che attraversare l'indice e quindi recuperare le colonne dalla tabella risulta in I / O casuali, che MySQL considera più costosi di un filesort.

Questo sembra volare di fronte al capitolo manuale ORDER BYsull'ottimizzazione , che non solo trasmette la forte impressione che soddisfare ORDER BYda un indice sia preferibile eseguire un ulteriore ordinamento (in effetti, filesortè una combinazione di quicksort e mergesort e quindi deve avere un limite inferiore di ; mentre si cammina nell'indice in ordine e si cerca nel tavolo dovrebbe essere — anche questo ha perfettamente senso), ma trascura anche di menzionare questa presunta "ottimizzazione", pur affermando:Ω(nlog n)O(n)

Le seguenti query utilizzano l'indice per risolvere la ORDER BYparte:

SELECT * FROM t1
  ORDER BY key_part1,key_part2,... ;

Secondo la mia lettura, questo è esattamente il caso in questa situazione (eppure l'indice non veniva utilizzato senza un suggerimento esplicito).

Le mie domande sono:

  • È davvero necessario che tutte le colonne selezionate vengano indicizzate affinché MySQL scelga di utilizzare l'indice?

    • In tal caso, dove è documentato (se non del tutto)?

    • In caso contrario, cosa stava succedendo qui?

Risposte:


14

È davvero necessario che tutte le colonne selezionate vengano indicizzate affinché MySQL scelga di utilizzare l'indice?

Questa è una domanda caricata perché ci sono fattori che determinano se vale la pena usare un indice.

FATTORE # 1

Per ogni dato indice, qual è la popolazione chiave? In altre parole, qual è la cardinalità (conteggio distinto) di tutte le tuple registrate nell'indice?

FATTORE # 2

Quale motore di archiviazione stai usando? Tutte le colonne necessarie sono accessibili da un indice?

QUAL È IL PROSSIMO ???

Facciamo un semplice esempio: una tabella che contiene due valori (maschio e femmina)

Lasciate creare una tabella di questo tipo con un test per l'utilizzo dell'indice

USE test
DROP TABLE IF EXISTS mf;
CREATE TABLE mf
(
    id int not null auto_increment,
    gender char(1),
    primary key (id),
    key (gender)
) ENGINE=InnODB;
INSERT INTO mf (gender) VALUES
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
ANALYZE TABLE mf;
EXPLAIN SELECT gender FROM mf WHERE gender='F';
EXPLAIN SELECT gender FROM mf WHERE gender='M';
EXPLAIN SELECT id FROM mf WHERE gender='F';
EXPLAIN SELECT id FROM mf WHERE gender='M';

TEST InnoDB

mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE mf
    -> (
    ->     id int not null auto_increment,
    ->     gender char(1),
    ->     primary key (id),
    ->     key (gender)
    -> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.07 sec)

mysql> INSERT INTO mf (gender) VALUES
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.06 sec)
Records: 40  Duplicates: 0  Warnings: 0

mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table   | Op      | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status   | OK       |
+---------+---------+----------+----------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |   37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |   37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql>

TEST MyISAM

mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE mf
    -> (
    ->     id int not null auto_increment,
    ->     gender char(1),
    ->     primary key (id),
    ->     key (gender)
    -> ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.05 sec)

mysql> INSERT INTO mf (gender) VALUES
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.00 sec)
Records: 40  Duplicates: 0  Warnings: 0

mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table   | Op      | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status   | OK       |
+---------+---------+----------+----------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |   36 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra       |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | mf    | ALL  | gender        | NULL | NULL    | NULL |   40 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

mysql>

Analisi per InnoDB

Quando i dati sono stati caricati come InnoDB, si noti che tutti e quattro i EXPLAINpiani hanno utilizzato l' genderindice. Il terzo e il quarto EXPLAINpiano utilizzavano l' genderindice anche se i dati richiesti erano id. Perché? Perché idè in PRIMARY KEYe tutti gli indici secondari hanno puntatori di riferimento al PRIMARY KEY(tramite il gen_clust_index ).

Analisi per MyISAM

Quando i dati sono stati caricati come MyISAM, si noti che i primi tre EXPLAINpiani hanno utilizzato l' genderindice. Nel quartoEXPLAIN piano, lo Strumento per ottimizzare le query ha deciso di non utilizzare affatto un indice. Invece ha optato per una scansione completa della tabella. Perché?

Indipendentemente dal DBMS, gli ottimizzatori di query funzionano secondo una regola empirica molto semplice: se un indice viene selezionato come candidato da utilizzare per eseguire la ricerca e lo Strumento per ottimizzare la query calcola che deve cercare oltre il 5% del numero totale di righe nella tabella:

  • viene eseguita una scansione completa dell'indice se tutte le colonne necessarie per il recupero si trovano nell'indice selezionato
  • una scansione completa della tabella altrimenti

CONCLUSIONE

Se non si dispone di indici di copertura adeguati o se la popolazione chiave per una data tupla è superiore al 5% della tabella, devono accadere sei cose:

  1. Vieni a capire che devi profilare le query
  2. Trova tutte WHERE, GROUP BYe ORDER BY` clausole da quelle query
  3. Formulare gli indici in questo ordine
    • WHERE colonne della clausola con valori statici
    • GROUP BY colonne
    • ORDER BY colonne
  4. Evita scansioni di tabelle complete (query prive di una WHEREclausola ragionevole )
  5. Evita popolazioni chiave sbagliate (o almeno memorizza nella cache quelle popolazioni chiave sbagliate)
  6. Decidi il miglior motore di archiviazione MySQL ( InnoDB o MyISAM ) per le tabelle

Ho scritto su questa regola empirica del 5% in passato:

AGGIORNAMENTO 2012-11-14 13:05 EDT

Ho dato uno sguardo alla tua domanda e al post SO originale . Poi, ho pensato al mio che Analysis for InnoDBho menzionato prima. Coincide con il persontavolo. Perché?

Per entrambi i tavoli mfeperson

  • Storage Engine è InnoDB
  • La chiave primaria è id
  • L'accesso alla tabella è per indice secondario
  • Se la tabella fosse MyISAM, vedremmo un EXPLAINpiano completamente diverso

Ora, guardate la query dalla questione SO: select * from person order by age\G. Poiché non esiste una WHEREclausola, è stata esplicitamente richiesta una scansione completa della tabella . L'ordinamento predefinito della tabella sarebbe per id(PRIMARY KEY) a causa del suo auto_increment e gen_clust_index (aka Clustered Index) è ordinato da rowid interno . Quando hai ordinato l'indice, tieni presente che gli indici secondari InnoDB hanno il rowid associato a ciascuna voce di indice. Ciò produce ogni volta la necessità interna di accedere alla riga completa.

L'impostazione ORDER BYsu una tabella InnoDB può essere un'attività piuttosto scoraggiante se si ignorano questi fatti su come sono organizzati gli indici InnoDB.

Tornando a quella query SO, poiché hai esplicitamente richiesto una scansione completa della tabella , IMHO lo MySQL Query Optimizer ha fatto la cosa giusta (o almeno, ha scelto il percorso di minor resistenza). Quando si tratta di InnoDB e della query SO, è molto più semplice eseguire una scansione completa della tabella e poi alcunifilesort piuttosto che eseguire una scansione completa dell'indice e una ricerca di riga tramite gen_clust_index per ogni voce di indice secondaria.

Non sono un sostenitore dell'utilizzo dei suggerimenti sull'indice perché ignora il piano EXPLAIN. Tuttavia, se conosci davvero i tuoi dati meglio di InnoDB, dovrai ricorrere a Suggerimenti per l'indice, in particolare con query che non hanno WHEREclausole.

AGGIORNAMENTO 2012-11-14 14:21 EDT

Secondo il libro Understanding MySQL Internals

inserisci qui la descrizione dell'immagine

Pagina 202 Il paragrafo 7 dice quanto segue:

I dati sono archiviati in una struttura speciale chiamata indice cluster , che è un albero B con la chiave primaria che funge da valore della chiave e il record effettivo (anziché un puntatore) nella parte di dati. Pertanto, ogni tabella InnoDB deve avere una chiave primaria. Se non ne viene fornito uno, viene aggiunta una colonna ID riga speciale normalmente non visibile all'utente per fungere da chiave primaria. Una chiave secondaria memorizzerà il valore della chiave primaria che identifica il record. Il codice B-tree si trova in innobase / btr / btr0btr.c .

Questo è il motivo per cui ho affermato in precedenza: è molto più semplice eseguire una scansione completa della tabella e quindi alcuni fileort piuttosto che eseguire una scansione completa dell'indice e una ricerca di riga tramite gen_clust_index per ogni voce di indice secondaria . InnoDB eseguirà una doppia ricerca dell'indice ogni volta . Sembra un po 'brutale, ma sono solo i fatti. Ancora una volta, prendere in considerazione la mancanza di WHEREclausola. Questo, di per sé, è il suggerimento di MySQL Query Optimizer per eseguire una scansione completa della tabella.


Rolando, grazie per una risposta così approfondita e dettagliata. Tuttavia, non sembra essere rilevante per la selezione degli indici FOR ORDER BY(che è il caso specifico in questa domanda). La domanda affermava che in questo caso il motore di archiviazione era InnoDB(e la domanda SO originale mostra che le 10k righe sono distribuite in modo abbastanza uniforme su 8 elementi, anche la cardinalità non dovrebbe essere un problema). Purtroppo, non penso che questo risponda alla domanda.
Eggyal

Questo è interessante, poiché anche la prima parte è stata il mio primo istinto (non aveva una buona cardinalità, quindi mysql ha scelto di utilizzare la scansione completa). Ma più leggo, quella regola non sembra applicarsi all'ordine tramite l'ottimizzazione. Sei sicuro che ordina per chiave primaria per gli indici cluster innodb? Questo post indica che la chiave primaria viene aggiunta alla fine, quindi l'ordinamento non dovrebbe ancora trovarsi nelle colonne esplicite dell'indice? In breve, sono ancora perplesso!
Derek Downey,

1
La filesortselezione è stata decisa dallo Strumento per ottimizzare le query per un semplice motivo: manca di conoscenza preliminare dei dati in tuo possesso. Se la tua scelta di utilizzare i suggerimenti sull'indice (in base al problema n. 2) ti dà un tempo di esecuzione soddisfacente, allora provaci. La risposta che ho fornito è stata solo un esercizio accademico per mostrare quanto possa essere temperante lo Strumento per ottimizzare le query MySQL e suggerire linee d'azione.
RolandoMySQLDBA

1
Ho letto e riletto questo e altri post e posso solo concordare sul fatto che ciò ha a che fare con l'ordinamento innodb sulla chiave primaria poiché stiamo selezionando tutto (e non un indice di copertura). Sono sorpreso che non ci sia menzione di questa stranezza specifica di InnoDB nella pagina del documento di ottimizzazione ORDER BY. Comunque, +1 a Rolando
Derek Downey

1
@eggyal Questo è stato scritto questa settimana. Notare lo stesso piano EXPLAIN e la scansione completa impiega più tempo se il set di dati non si adatta alla memoria.
Derek Downey,

0

Adattato (con il permesso) dalla risposta di Denis ad un'altra domanda su SO:

Dal momento che tutti i record (o quasi) verranno recuperati dalla query, di solito stai meglio senza alcun indice. Il motivo è che in realtà costa qualcosa leggere un indice.

Mentre stai andando per l'intera tabella, leggere sequenzialmente la tabella e ordinare le sue righe in memoria potrebbe essere il tuo piano più economico. Se hai solo bisogno di poche righe e la maggior parte corrisponderà alla clausola where, andare per l'indice più piccolo farà il trucco.

Per capire perché, immagina l'I / O del disco coinvolto.

Supponiamo di voler l'intera tabella senza un indice. Per fare questo, leggi data_page1, data_page2, data_page3, ecc., Visitando le varie pagine del disco coinvolte nell'ordine, fino a raggiungere la fine della tabella. Quindi ordina e ritorna.

Se desideri le prime 5 righe senza un indice, dovresti leggere in sequenza l'intera tabella come prima, ordinando in modo heap le prime 5 righe. Certo, è un sacco di lettura e ordinamento per una manciata di righe.

Supponiamo, ora, che tu voglia l'intera tabella con un indice. Per fare ciò, leggi index_page1, index_page2, ecc., In sequenza. Questo ti porta quindi a visitare, diciamo, data_page3, quindi data_page1, quindi data_page3 di nuovo, quindi data_page2, ecc., In un ordine completamente casuale (quello in base al quale le righe ordinate compaiono nei dati). L'IO coinvolto rende più economico leggere l'intero disordine in sequenza e ordinare il sacco in memoria.

Se si desidera semplicemente le prime 5 righe di una tabella indicizzata, al contrario, l'utilizzo dell'indice diventa la strategia corretta. Nel peggiore dei casi carica 5 pagine di dati in memoria e vai avanti.

Un buon pianificatore di query SQL, tra l'altro, deciderà se utilizzare un indice o meno in base alla frammentazione dei dati. Se recuperare le righe in ordine significa zoomare avanti e indietro sulla tabella, un buon pianificatore può decidere che non vale la pena usare l'indice. Al contrario, se la tabella è raggruppata utilizzando lo stesso indice, le righe sono garantite in ordine, aumentando la probabilità che venga utilizzato.

Ma poi, se unisci la stessa query con un'altra tabella e quell'altra tabella ha una clausola where estremamente selettiva che può usare un piccolo indice, il planner potrebbe decidere che in realtà è meglio farlo, ad esempio recuperare tutti gli ID delle righe contrassegnate come foo, hash Unisciti ai tavoli e heap ordinali in memoria.

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.