Warum ignoriert MySQL den Index selbst bei erzwungener Bestellung von?

14

Ich führe ein 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)  

Die Indizes in meiner Tabelle:

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)  

Es gibt einen Index für last_name, der vom Optimierer jedoch nicht verwendet wird.
So ich mache:

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)  

Aber nach wie vor ist der Index nicht verwendet! Was mache ich hier falsch?
Hat es damit zu tun, dass der Index ist NON_UNIQUE? Übrigens ist der NachnameVARCHAR(1000)

Von @RolandoMySQLDBA angefordertes Update

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)  
Cratylus
quelle
Bitte führen Sie diese beiden Abfragen aus: 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;. Was ist das Ergebnis jeder Zählung?
RolandoMySQLDBA
@RolandoMySQLDBA: Ich habe OP mit den Informationen aktualisiert, nach denen Sie gefragt haben.
Cratylus
Bitte noch zwei Fragen: 1) SELECT COUNT(1) FullTableCount FROM employees;und 2) SELECT * FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A LIMIT 10;.
RolandoMySQLDBA
Egal, ich sehe die Erklärung mit dem, was ich brauche.
RolandoMySQLDBA
2
@Cratylus Sie eine falsche Antwort akzeptiert, sollten Sie die richtige akzeptieren Antwort von Michael-sqlbot
miracle173

Antworten:

6

PROBLEM 1

Schauen Sie sich die Abfrage an

select last_name from employees order by last_name;

Ich sehe keine aussagekräftige WHERE-Klausel und der MySQL Query Optimizer auch nicht. Es besteht kein Anreiz, einen Index zu verwenden.

PROBLEM 2

Schauen Sie sich die Abfrage an

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

Sie gaben ihm einen Index, aber der Query Opitmizer übernahm. Ich habe dieses Verhalten schon einmal gesehen ( Wie erzwinge ich, dass ein JOIN einen bestimmten Index in MySQL verwendet? )

Warum sollte das passieren?

Ohne eine WHEREKlausel sagt sich das Abfrageoptimierungsprogramm Folgendes:

  • Dies ist eine InnoDB-Tabelle
  • Es ist eine indizierte Spalte
  • Der Index hat die row_id des gen_clust_index (aka Clustered Index)
  • Warum sollte ich mir den Index ansehen, wenn
    • gibt es keine WHEREklausel?
    • Ich müsste immer wieder zum Tisch springen?
  • Da sich alle Zeilen in einer InnoDB-Tabelle in denselben 16-KB-Blöcken befinden wie der gen_clust_index, werde ich stattdessen einen vollständigen Tabellenscan durchführen.

Das Abfrageoptimierungsprogramm hat den Pfad mit dem geringsten Widerstand gewählt.

Sie werden einen kleinen Schock erleben, aber hier ist er: Wussten Sie, dass das Abfrageoptimierungsprogramm MyISAM ganz anders behandelt?

Sie sagen wahrscheinlich HUH ???? WIE ????

MyISAM speichert die Daten in einer .MYDDatei und alle Indizes in der .MYIDatei.

Dieselbe Abfrage erzeugt einen anderen EXPLAIN-Plan, da sich der Index in einer anderen Datei als die Daten befindet. Warum ? Hier ist warum:

  • Die benötigten Daten ( last_nameSpalte) sind bereits im bestellt.MYI
  • Im schlimmsten Fall wird ein vollständiger Index-Scan durchgeführt
  • Sie greifen nur last_nameüber den Index auf die Spalte zu
  • Sie müssen nicht unerwünscht durchsieben
  • Sie werden die temporäre Dateierstellung nicht zum Sortieren auslösen

Wie kann man sich da so sicher sein? Ich habe diese Arbeitstheorie dahingehend getestet, wie die Verwendung eines anderen Speichers einen anderen EXPLAIN-Plan (manchmal einen besseren) generiert: Muss ein Index alle ausgewählten Spalten abdecken, damit er für ORDER BY verwendet werden kann?

RolandoMySQLDBA
quelle
1
-1 @Rolando Diese Antwort ist nicht weniger genau als die richtige Antwort von Michael-sqlbot, aber sie ist falsch. Im Handbuch heißt es: "MySQL verwendet für diese Operationen Indizes: (...) Um eine Tabelle zu sortieren oder zu gruppieren, wenn die Sortierung oder Die Gruppierung erfolgt nach einem ganz linken Präfix eines verwendbaren Index (...) ". Auch einige der anderen Aussagen Ihres Beitrags sind umstritten. Ich würde empfehlen, diese Antwort zu löschen oder zu überarbeiten.
miracle173
Diese Antwort ist nicht richtig. Ein Index kann weiterhin verwendet werden, auch wenn keine WHERE-Klausel vorhanden ist, wenn die Sortierung vermieden wird.
14.
19

Das Problem hierbei ist, dass dies wie ein Präfixindex aussieht. Ich sehe die Tabellendefinition in der Frage nicht, aber sub_part= 700? Sie haben nicht die gesamte Spalte indiziert, daher kann der Index nicht zum Sortieren verwendet werden und ist auch nicht als Deckungsindex nützlich. Es konnte nur verwendet werden, um die Zeilen zu finden, die mit einer übereinstimmen könnten, WHEREund die Serverschicht (über der Speicher-Engine) musste die übereinstimmenden Zeilen weiter filtern. Benötigen Sie wirklich 1000 Zeichen für einen Nachnamen?


Update zur Veranschaulichung: Ich habe eine Tabellentesttabelle mit einem Litle von mehr als 500 Zeilen, die jeweils den Domainnamen einer Website in einer Spalte domain_name VARCHAR(254) NOT NULLund keine Indizes enthält.

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

Wenn die vollständige Spalte indiziert ist, verwendet die Abfrage den Index:

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)

Jetzt lösche ich diesen Index und indiziere nur die ersten 200 Zeichen von 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>

Voila.

Beachten Sie auch, dass der Index mit 200 Zeichen länger ist als der längste Wert in der Spalte ...

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

... aber das macht keinen Unterschied. Ein mit einer Präfixlänge deklarierter Index kann nur für Nachschlagezwecke verwendet werden, nicht zum Sortieren und nicht als überdeckender Index, da er definitionsgemäß nicht den vollständigen Spaltenwert enthält.

Die obigen Abfragen wurden auch in einer InnoDB-Tabelle ausgeführt, aber die Ausführung in einer MyISAM-Tabelle führt zu praktisch identischen Ergebnissen. Der einzige Unterschied in diesem Fall besteht darin, dass die InnoDB-Zählung rowsgeringfügig abweicht (541), während MyISAM die genaue Anzahl der Zeilen anzeigt (563). Dies ist normal, da die beiden Speicher-Engines Index-Tauchgänge sehr unterschiedlich verarbeiten.

Ich würde immer noch behaupten, dass die last_name-Spalte wahrscheinlich größer als nötig ist, aber es ist immer noch möglich , die gesamte Spalte zu indizieren, wenn Sie InnoDB verwenden und MySQL 5.5 oder 5.6 ausführen:

Standardmäßig kann ein Indexschlüssel für einen einspaltigen Index bis zu 767 Byte umfassen. Die gleiche Längenbeschränkung gilt für alle Indexschlüsselpräfixe. Siehe auch Abschnitt 13.1.13, „ CREATE INDEXSyntax“. Sie können dieses Limit beispielsweise mit einem Spaltenpräfixindex von mehr als 255 Zeichen für eine TEXToder VARCHARSpalte erreichen, vorausgesetzt, ein UTF-8Zeichensatz und maximal 3 Byte für jedes Zeichen. Wenn die innodb_large_prefixKonfigurationsoption aktiviert ist, wird diese Längenbegrenzung auf 3072 Bytes erhöht, für InnoDBTabellen, die die Verwendung DYNAMICund COMPRESSEDZeilenformate.

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

Michael - sqlbot
quelle
Interessante Sicht. Die Spalte ist, varchar(1000)aber dies ist jenseits des für den Index zulässigen Höchstwerts, der bei ~ 750 liegt
Cratylus
8
Diese Antwort sollte die akzeptierte sein.
Ypercubeᵀᴹ
1
@ypercube Diese Antwort ist präziser als meine. +1 für Ihren Kommentar und +1 für diese Antwort. Möge dies stattdessen von mir akzeptiert werden.
RolandoMySQLDBA
1
@Timo, das ist eine interessante Frage ... die ich vorschlagen würde, als neue Frage hier zu posten, vielleicht mit einem Link zu dieser Antwort, für den Kontext. Veröffentlichen Sie die vollständige Ausgabe von EXPLAIN SELECT ...sowie SHOW CREATE TABLE ...und, SELECT @@VERSION;da Änderungen am Optimierer möglicherweise versionsübergreifend relevant sind.
Michael - sqlbot
1
Inzwischen kann ich melden, dass (zumindest für 5.7) ein Präfixindex nicht zur Indexierung von null beiträgt, wie ich in meinem obigen Kommentar gefordert habe.
Timo
2

Ich antwortete, weil ein Kommentar die Formatierung nicht unterstützt und RolandoMySQL DBA über gen_clust_index und innodb sprach. Und das ist sehr wichtig bei einem Innodb-basierten Tisch. Dies geht über die normalen DBA-Kenntnisse hinaus, da Sie in der Lage sein müssen, C-Code zu analysieren.

Sie sollten IMMER EINEN PRIMÄREN SCHLÜSSEL oder EINZIGARTIGEN SCHLÜSSEL erstellen, wenn Sie Innodb verwenden. Wenn Sie dies nicht tun, wird innodb seine eigene generierte ROW_ID verwenden, die Ihnen mehr schaden als nützen könnte.

Ich werde versuchen, es einfach zu erklären, da der Beweis auf C-Code basiert.

/**********************************************************************//**
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);
}

Erstes Problem

mutex_enter (& (dict_sys-> mutex));

Diese Zeile stellt sicher, dass nur ein Thread gleichzeitig auf dict_sys-> mutex zugreifen kann. Was wäre, wenn der Wert bereits mutexed wäre? Ja, ein Thread muss warten, damit Sie so etwas wie eine nette zufällige Funktion wie das Sperren von Threads erhalten. Wenn Sie mehr Tabellen ohne Ihren eigenen PRIMARY KEY oder UNIQUE KEY haben, hätten Sie eine nette Funktion mit Innodb ' Table Locking ' ist nicht der Grund, warum MyISAM durch InnoDB ersetzt wurde.

Zweites Problem

(0 == (ID% DICT_HDR_ROW_ID_WRITE_MARGIN))

Modulo (%) -Berechnungen sind langsam und nicht gut, wenn Sie Stapel einfügen, da sie jedes Mal neu berechnet werden müssen ... und weil DICT_HDR_ROW_ID_WRITE_MARGIN (Wert 256) eine Zweierpotenz ist, kann dies viel schneller erfolgen.

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

Anmerkung: Wenn der C-Compiler für die Optimierung konfiguriert wurde und ein guter Optimierer ist, korrigiert der C-Optimierer den "schweren" Code auf die leichtere Version

Motto der Geschichte Erstellen Sie immer Ihren eigenen PRIMARY KEY oder stellen Sie sicher, dass Sie einen EINZIGARTIGEN Index haben, wenn Sie von Anfang an eine Tabelle erstellen

Raymond Nijland
quelle
Fügen Sie die zeilenbasierte Replikation hinzu und die Tatsache, dass die Zeilen-IDs auf den Servern nicht konsistent sind. Umso wichtiger ist es, dass Raymond immer einen Primärschlüssel erstellt.
Bitte schlagen Sie nicht vor, dass dies UNIQUEausreicht - es müssen auch nur Nicht-NULL-Spalten enthalten sein, damit der eindeutige Index in PK hochgestuft wird.
Rick James
"Modulo (%) -Berechnungen sind langsam" - Wichtiger ist, wie viel Prozent der Zeit eines INSERTin dieser Funktion verbracht wird. Ich vermute, ist unbedeutend. Vergleichen Sie den Aufwand, um Spalten herumzuschaufeln, führen Sie BTree-Operationen aus, einschließlich gelegentlichem Block-Split, verschiedenen Mutexen für buffer_pool, Change-Buffer-Stuff usw.
Rick James
Richtig, @RickJames, der Overhead könnte eine sehr kleine Zahl sein, aber viele kleine Zahlen summieren sich auch (wäre immer noch eine Mikrooptimierung). Außerdem ist das erste Problem das größte
Raymond Nijland