Perché MySQL ignora l'indice anche in vigore per questo ordine?


14

Corro un EXPLAIN:

mysql> explain select last_name from employees order by last_name;
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra          |
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 10031 | Using filesort |
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
1 row in set (0.00 sec)  

Gli indici nella mia tabella:

mysql> show index from employees;  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
| Table     | Non_unique | Key_name      | Seq_in_index | Column_name   | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
| employees |          0 | PRIMARY       |            1 | subsidiary_id | A         |           6 |     NULL | NULL   |      | BTREE      |         |               |  
| employees |          0 | PRIMARY       |            2 | employee_id   | A         |       10031 |     NULL | NULL   |      | BTREE      |         |               |  
| employees |          1 | idx_last_name |            1 | last_name     | A         |       10031 |      700 | NULL   |      | BTREE      |         |               |  
| employees |          1 | date_of_birth |            1 | date_of_birth | A         |       10031 |     NULL | NULL   | YES  | BTREE      |         |               |  
| employees |          1 | date_of_birth |            2 | subsidiary_id | A         |       10031 |     NULL | NULL   |      | BTREE      |         |               |  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
5 rows in set (0.02 sec)  

C'è un indice su last_name ma l'ottimizzatore non lo utilizza.
Faccio così:

mysql> explain select last_name from employees force index(idx_last_name) order by last_name;  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra          |  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 10031 | Using filesort |  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
1 row in set (0.00 sec)  

Ma ancora l'indice è non utilizzata! Cosa sto facendo di sbagliato qui?
Ha a che fare con il fatto che l'indice è NON_UNIQUE? A proposito il cognome èVARCHAR(1000)

Aggiornamento richiesto da @RolandoMySQLDBA

mysql> SELECT COUNT(DISTINCT last_name) DistinctCount FROM employees;  
+---------------+  
| DistinctCount |  
+---------------+  
|         10000 |  
+---------------+  
1 row in set (0.05 sec)  


mysql> SELECT COUNT(1) FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A;  
+----------+  
| COUNT(1) |  
+----------+  
|        0 |  
+----------+  
1 row in set (0.15 sec)  

Esegui queste due query: 1) SELECT COUNT(DISTINCT last_name) DistinctCount FROM employees;2) SELECT COUNT(1) FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A;. Qual è il risultato di ogni conteggio?
RolandoMySQLDBA il

@RolandoMySQLDBA: ho aggiornato l'OP con le informazioni richieste.
Cratylus,

Altre due domande, per favore: 1) SELECT COUNT(1) FullTableCount FROM employees;e 2) SELECT * FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A LIMIT 10;.
RolandoMySQLDBA il

Non importa, vedo la spiegazione con ciò di cui ho bisogno.
RolandoMySQLDBA il

2
@Cratylus hai accettato una risposta sbagliata, dovresti accettare la risposta
miracle173

Risposte:


6

PROBLEMA N. 1

Guarda la domanda

select last_name from employees order by last_name;

Non vedo una clausola WHERE significativa e nemmeno lo Strumento per ottimizzare le query MySQL. Non vi è alcun incentivo a utilizzare un indice.

PROBLEMA N. 2

Guarda la domanda

select last_name from employees force index(idx_last_name) order by last_name; 

Gli hai dato un indice, ma Quit Opitmizer è subentrato. Ho già visto questo comportamento prima ( Come posso forzare un JOIN a utilizzare un indice specifico in MySQL? )

Perché dovrebbe succedere?

Senza una WHEREclausola, Query Optimizer dice a se stesso quanto segue:

  • Questa è una tabella InnoDB
  • È una colonna indicizzata
  • L'indice ha il row_id del gen_clust_index (aka Clustered Index)
  • Perché dovrei guardare l'indice quando
    • Non c'è WHERE clausola?
    • Dovrei sempre tornare al tavolo?
  • Poiché tutte le righe in una tabella InnoDB risiedono negli stessi blocchi da 16 KB di gen_clust_index, eseguirò invece una scansione completa della tabella.

Lo Strumento per ottimizzare le query ha scelto il percorso di minor resistenza.

Ti verrà un po 'di shock, ma ecco qui: lo sapevi che lo Strumento per ottimizzare le query gestirà MyISAM in modo diverso?

Probabilmente stai dicendo HUH ???? COME ????

MyISAM memorizza i dati in un .MYDfile e tutti gli indici nel file.MYI file.

La stessa query produrrà un piano EXPLAIN diverso perché l'indice risiede in un file diverso dai dati. Perché ? Ecco perché:

  • I dati necessari (last_name colonna) sono già ordinati in.MYI
  • Nel peggiore dei casi, avrai una scansione dell'indice completa
  • Accederai solo alla colonna last_name dall'indice
  • Non è necessario setacciare gli indesiderati
  • Non verrà attivata la creazione del file temporaneo per l'ordinamento

Come può esserne così sicuro? Ho testato questa teoria di funzionamento su come l'utilizzo di un diverso archivio genererà un diverso piano EXPLAIN (a volte uno migliore): un indice deve coprire tutte le colonne selezionate affinché possa essere utilizzato per ORDER BY?


1
-1 @Rolando questa risposta non è meno precisa della risposta corretta di Michael-sqlbot ma è errata, ad esempio il manuale dice: "MySQL utilizza gli indici per queste operazioni: (...) Per ordinare o raggruppare una tabella se l'ordinamento o il raggruppamento viene eseguito sul prefisso più a sinistra di un indice utilizzabile (...) ". Anche alcune delle altre dichiarazioni del tuo post sono discutibili. Ti consiglierei di eliminare questa risposta o di rielaborarla.
miracle173,

Questa risposta non è corretta Un indice può ancora essere utilizzato anche se non esiste una clausola WHERE se si evita l'ordinamento.
Oysteing

19

In realtà, il problema qui è che sembra un indice prefisso. Non vedo la definizione della tabella nella domanda, ma sub_part= 700? Non hai indicizzato l'intera colonna, quindi l'indice non può essere utilizzato per l'ordinamento e non è utile nemmeno come indice di copertura. Potrebbe essere utilizzato solo per trovare le righe che "potrebbero" corrispondere a WHEREe il livello del server (sopra il motore di archiviazione) dovrebbe filtrare ulteriormente le righe corrispondenti. Hai davvero bisogno di 1000 caratteri per un cognome?


aggiornamento per illustrare: ho una tabella di test tabella con un minimo di oltre 500 righe al suo interno, ognuna con il nome di dominio di un sito Web in una colonna domain_name VARCHAR(254) NOT NULLe senza indici.

mysql> alter table keydemo add key(domain_name);
Query OK, 0 rows affected (0.17 sec)
Records: 0  Duplicates: 0  Warnings: 0

Con l'intera colonna indicizzata, la query utilizza l'indice:

mysql> explain select domain_name from keydemo order by domain_name;
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
| id | select_type | table   | type  | possible_keys | key         | key_len | ref  | rows | Extra       |
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
|  1 | SIMPLE      | keydemo | index | NULL          | domain_name | 764     | NULL |  541 | Using index |
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
1 row in set (0.01 sec)

Quindi, ora, lascerò cadere quell'indice e indicizzerò solo i primi 200 caratteri di domain_name.

mysql> alter table keydemo drop key domain_name;
Query OK, 0 rows affected (0.11 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> alter table keydemo add key(domain_name(200));
Query OK, 0 rows affected (0.08 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain select domain_name from keydemo order by domain_name;
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
| id | select_type | table   | type | possible_keys | key  | key_len | ref  | rows | Extra          |
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
|  1 | SIMPLE      | keydemo | ALL  | NULL          | NULL | NULL    | NULL |  541 | Using filesort |
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
1 row in set (0.00 sec)

mysql>

Ecco.

Si noti inoltre che l'indice, con 200 caratteri, è più lungo del valore più lungo nella colonna ...

mysql> select max(length(domain_name)) from keydemo;
+--------------------------+
| max(length(domain_name)) |
+--------------------------+
|                       43 |
+--------------------------+
1 row in set (0.04 sec)

... ma questo non fa alcuna differenza. Un indice dichiarato con una lunghezza prefisso può essere utilizzato solo per le ricerche, non per l'ordinamento e non come indice di copertura, poiché per definizione non contiene l'intero valore della colonna.

Inoltre, le query precedenti sono state eseguite su una tabella InnoDB, ma eseguirle su una tabella MyISAM produce risultati praticamente identici. L' unica differenza in questo caso è che InnoDB contarows è leggermente disattivato (541) mentre MyISAM mostra il numero esatto di righe (563) che è un comportamento normale poiché i due motori di archiviazione gestiscono le immersioni dell'indice in modo molto diverso.

Asserirei comunque che la colonna last_name è probabilmente più grande del necessario, ma è comunque possibile indicizzare l'intera colonna, se si utilizza InnoDB e si esegue MySQL 5.5 o 5.6:

Per impostazione predefinita, una chiave di indice per un indice a colonna singola può contenere fino a 767 byte. Lo stesso limite di lunghezza si applica a qualsiasi prefisso chiave indice. Vedere la Sezione 13.1.13, " CREATE INDEXSintassi". Ad esempio, è possibile raggiungere questo limite con un indice del prefisso di colonna di oltre 255 caratteri su una TEXTo VARCHARcolonna, presupponendo un UTF-8set di caratteri e un massimo di 3 byte per ciascun carattere. Quando l' innodb_large_prefixopzione di configurazione è abilitata, questo limite di lunghezza viene aumentato a 3072 byte, per le InnoDBtabelle che utilizzano DYNAMICeCOMPRESSED formati di riga .

- http://dev.mysql.com/doc/refman/5.5/en/innodb-rest restrizioni.html


Punto di vista interessante. La colonna è varchar(1000)ma questo è oltre il massimo consentito per l'indice che è ~ 750
Cratylus

8
Questa risposta dovrebbe essere quella accettata.
ypercubeᵀᴹ

1
@ypercube Questa risposta è più precisa della mia. +1 per il tuo commento e +1 per questa risposta. Possa questo essere accettato invece sul mio.
RolandoMySQLDBA

1
@Timo, questa è una domanda interessante ... che suggerirei di pubblicare come nuova domanda, qui, forse con un link a questa risposta, per il contesto. Pubblica l'output completo da EXPLAIN SELECT ..., così come SHOW CREATE TABLE ...e SELECT @@VERSION;poiché le modifiche all'ottimizzatore tra le versioni possono essere rilevanti.
Michael - sqlbot,

1
Ormai posso segnalare che (almeno per 5.7) un indice di prefisso non aiuta con l'indicizzazione null, come ho chiesto nel mio commento sopra.
Timo,

2

Ho risposto perché un commento non supporterà la formulazione e il DBA RolandoMySQL ha parlato di gen_clust_index e innodb. E questo è molto importante su una tabella basata su innodb. Questo va oltre le normali conoscenze DBA perché devi essere in grado di analizzare il codice C.

Dovresti SEMPRE SEMPRE creare una CHIAVE PRIMARIA o una CHIAVE UNICA se stai usando Innodb. Se non innodb utilizzerà il proprio ROW_ID generato, che potrebbe farti più male che bene.

Proverò a spiegarlo facilmente perché la prova si basa sul codice C.

/**********************************************************************//**
Returns a new row id.
@return the new id */
UNIV_INLINE
row_id_t
dict_sys_get_new_row_id(void)
/*=========================*/
{
    row_id_t    id;

    mutex_enter(&(dict_sys->mutex));

    id = dict_sys->row_id;

    if (0 == (id % DICT_HDR_ROW_ID_WRITE_MARGIN)) {
          dict_hdr_flush_row_id();
    }

    dict_sys->row_id++;
    mutex_exit(&(dict_sys->mutex));
    return(id);
}

Primo problema

mutex_enter (& (dict_sys-> mutex));

Questa riga si assicura che solo un thread possa accedere a dict_sys-> mutex contemporaneamente. E se il valore fosse già disattivato ... sì, un thread deve attendere in modo da ottenere qualcosa come una bella funzione casuale come il blocco dei thread o se hai più tabelle senza il tuo PRIMARY KEY o UNIQUE KEY, allora avresti una bella funzionalità con innodb ' blocco tabella ' non è questo il motivo per cui MyISAM è stato sostituito da InnoDB perché al di fuori della simpatica funzionalità che si chiama blocco basato su record / riga ..

Secondo problema

(0 == (ID% DICT_HDR_ROW_ID_WRITE_MARGIN))

i calcoli del modulo (%) sono lenti non vanno bene se si inserisce un batch perché deve essere ricalcolato ogni volta ... e poiché DICT_HDR_ROW_ID_WRITE_MARGIN (valore 256) è una potenza di due, questo potrebbe essere reso molto più veloce ..

(0 == (id & (DICT_HDR_ROW_ID_WRITE_MARGIN - 1)))

Nota a margine se il compilatore C è stato configurato per l'ottimizzazione ed è un buon ottimizzatore, l'ottimizzatore C fisserà il codice "pesante" alla versione più leggera

motto della storia crea sempre la tua PRIMARY KEY o assicurati di avere un indice UNICO quando crei una tabella dall'inizio


Aggiungi la replica basata su righe e il fatto che gli ID di riga non siano coerenti tra i server e il punto di Raymond sulla creazione di una chiave primaria è ancora più importante.

Non suggerire che UNIQUEsia sufficiente: deve anche includere solo colonne non NULL affinché l'indice univoco possa essere promosso a PK.
Rick James,

"I calcoli del modulo (%) sono lenti" - Più importante è quale percentuale del tempo di un INSERTviene impiegato in questa funzione. Ho il sospetto che sia insignificante. Contrasta lo sforzo di spalare colonne, eseguire operazioni BTree, tra cui una suddivisione occasionale di blocchi, vari mutex sul buffer_pool, roba buffer di modifica, ecc.
Rick James,

True @RickJames il sovraccarico potrebbe essere un numero molto piccolo, ma anche molti piccoli numeri si sommano (sarebbe comunque una micro ottimizzazione) .. Inoltre il primo problema è il più problematico
Raymond Nijland
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.