Warum verwendet MySQL den falschen Index für die Bestellung per Abfrage?

9

Hier ist meine Tabelle mit ~ 10.000.000 Zeilendaten

CREATE TABLE `votes` (
  `subject_name` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
  `subject_id` int(11) NOT NULL,
  `voter_id` int(11) NOT NULL,
  `rate` int(11) NOT NULL,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`subject_name`,`subject_id`,`voter_id`),
  KEY `IDX_518B7ACFEBB4B8AD` (`voter_id`),
  KEY `subject_timestamp` (`subject_name`,`subject_id`,`updated_at`),
  KEY `voter_timestamp` (`voter_id`,`updated_at`),
  CONSTRAINT `FK_518B7ACFEBB4B8AD` FOREIGN KEY (`voter_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

Hier sind die Index-Kardinalitäten

Geben Sie hier die Bildbeschreibung ein

Also, wenn ich diese Abfrage mache:

SELECT SQL_NO_CACHE * FROM votes WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

Ich hatte erwartet, dass es Index verwendet, voter_timestamp aber MySQL wählt stattdessen diesen:

explain select SQL_NO_CACHE * from votes  where subject_name = 'medium' and voter_id = 1001 and rate = 1 order by updated_at desc limit 20 offset 100;`

type:
    index_merge
possible_keys: 
    PRIMARY,IDX_518B7ACFEBB4B8AD,subject_timestamp,voter_timestamp
key:
    IDX_518B7ACFEBB4B8AD,PRIMARY
key_len:
    102,98
ref:
    NULL
rows:
    9255
filtered:
    10.00
Extra:
    Using intersect(IDX_518B7ACFEBB4B8AD,PRIMARY); Using where; Using filesort

Und ich habe 200-400ms Abfragezeit.

Wenn ich es zwinge, den richtigen Index zu verwenden, wie:

SELECT SQL_NO_CACHE * FROM votes USE INDEX (voter_timestamp) WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

MySQL kann die Ergebnisse in 1-2 ms zurückgeben

und hier ist die Erklärung:

type:
    ref
possible_keys:
    voter_timestamp
key:
    voter_timestamp
key_len:
    4
ref:
    const
rows:
    18714
filtered:
    1.00
Extra:
    Using where

Warum hat mysql den voter_timestampIndex für meine ursprüngliche Abfrage nicht ausgewählt?

Was ich versucht hatte, ist analyze table votes, optimize table votesdiesen Index zu löschen und erneut hinzuzufügen, aber MySQL verwendet immer noch den falschen Index. nicht ganz verstehen, was das Problem ist.

Phönix
quelle
1
@ ypercubeᵀᴹ Ich denke nicht, dass es notwendig ist, alle Spalten in der where-Bedingung zu indizieren. Wie Sie sehen, kann ich den Index (voter_id, updated_at) verwenden und er kann sehr effizient sein. Wenn ich das subject_name = "medium"Teil entferne, kann es auch den richtigen Index auswählen, ohne dass ein Index erforderlich istrate
Phoenix
Der 4-Spalten-Index ist jedoch effizienter als der 2-Spalten-Index (voter_id, updated_at). Ein anderer Index wäre (voter_id, subject_name, updated_at)oder (subject_name, voter_id, updated_at)(ohne die Rate).
Ypercubeᵀᴹ
1
Und ja, Sie haben - irgendwann - recht. Sie benötigen den 4-Spalten-Index nicht. Es ist nur der bestmögliche Index für diese Abfrage. Die 2-Spalten (die Sie für "richtig" halten) sind möglicherweise in Ordnung für die Daten und die Verteilung, die Sie derzeit haben. Bei einer anderen Verteilung könnte es schrecklich sein. Beispiel: Angenommen, 99% der Zeilen hatten eine Rate> 1 und nur 1% eine Rate = 1. Denken Sie, dass die Verwendung des 2-Spalten-Index effizient wäre?
Ypercubeᵀᴹ
Es müsste einen großen Teil des Index durchlaufen und Tausende von Suchvorgängen in der Tabelle durchführen, nur um diese Rate> 1 zu finden und die Zeilen abzulehnen, bis 120 gefunden werden, die den Kriterien entsprechen, die vom Index nicht beurteilt werden können ( subject_name='medium' and rate=1)
ypercubeᵀᴹ
ypercube, Phoenix - MySQL gelangt nicht zum LIMIToder sogar zum, es ORDER BYsei denn, der Index erfüllt zuerst alle Filter. Das heißt, ohne die vollständigen 4 Spalten werden alle relevanten Zeilen gesammelt, alle sortiert und dann die ausgewählt LIMIT. Mit dem 4-Spalten-Index kann die Abfrage die Sortierung vermeiden und anhalten, nachdem nur die LIMITZeilen gelesen wurden .
Rick James

Antworten:

5

MySQL verwendet ein relativ einfaches (einfacher als andere RDBMS) Kostenmodell für die Planung von Abfragen, bei denen das Filtern Ihres Datasets eine hohe Priorität hat. Bei Ihrer ersten Abfrage mit dem Zusammenführungsindex wird geschätzt, dass das Scannen von ~ 9000 Zeilen erforderlich sein wird, während für die zweite Abfrage mit dem Indexhinweis 18000 erforderlich sind. Ich wette, dass dies in der Berechnung ausreicht, um die Skala in Richtung Zusammenführung zu verschieben . Sie können dies bestätigen (oder andere Gründe finden), indem Sie es aktivieren optimizer_trace, Ihre Abfrage ausführen und die Ergebnisse auswerten.

set global optimizer_trace='enabled=on';

-- run your query 

SELECT SQL_NO_CACHE * FROM votes WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

select * from information_schema.`OPTIMIZER_TRACE`;

Eine Bemerkung zu index_merge: In den meisten Fällen werden Sie feststellen, dass es ziemlich teuer ist. Obwohl dies für OLAP-Szenarien sehr nützlich ist, ist es für OLTP möglicherweise nicht sehr gut geeignet, da der Vorgang viel Zeit in Anspruch nehmen kann und der suboptimale Ausführungsplan manchmal tatsächlich schneller ist.

Glücklicherweise bietet MySQL Schalter für das Optimierungsprogramm, sodass Sie es nach Ihren Wünschen anpassen können.

Für alle Optionen, die Sie ausführen können:

show global variables like 'optimizer_switch';

Um eine zu ändern, müssen Sie nicht die gesamte Zeichenfolge kopieren und einfügen. Es funktioniert wie dict.update()in Python.

 set global optimizer_switch='index_merge=off';

Wenn möglich würde ich mir auch Ihre Tabellenstruktur ansehen und verbessern. Ein Primärschlüssel mit ~ 100 Byte und vielen Sekundärschlüsseln wird nicht empfohlen.

Sie haben vier Sekundärschlüssel, von denen einige überflüssig sind. Beispielsweise ist der (voter_id)Index eine Teilmenge von(voter_id, updated_at)

Károly Nagy
quelle
"Index Merge Intersect" wird von MySQL selten verwendet. In vielleicht allen Fällen ist es wesentlich besser, einen Index mit mehr Spalten zu haben. "Index Merge Union" ist manchmal nützlich; sich ORin zu verwandeln UNIONist oft genauso gut oder besser.
Rick James
5

Für diese Abfrage benötigen Sie diesen Index:

INDEX(voter_id, rate, subject_name, updated_at)

Das updated_atmuss das letzte sein; Die anderen drei können in beliebiger Reihenfolge sein. (Die 3-Spalten-Indizes von ypercube sind nicht sehr nützlich, da sie die WHERESpalten nicht beenden, bevor sie auf die ORDER BYSpalte treffen .)

Wenn Sie diesen Index hinzufügen, können Sie wahrscheinlich alle anderen Sekundärschlüssel entfernen:

KEY IDX_518B7ACFEBB4B8AD( voter_id) - Der FK meinen Index KEY verwenden subject_timestamp( subject_name, subject_id, updated_atmeist redundante KEY -), voter_timestamp( voter_id, updated_at), - gewesen sein mag Ihren Versuch

Mit dem 4-Spalten-Index haben Sie die Möglichkeit, die "Paginierung" zu optimieren und zu vermeiden OFFSET. Siehe diesen Blog.

Zu einem anderen Thema ... Wenn ich X_nameund sehe X_id, gehe ich davon aus, dass "Normalisierung" stattfindet. Ich würde erwarten, diese beiden Spalten in einer Tabelle zu sehen, mit praktisch nichts anderem. Ich würde nicht erwarten, beide in einer anderen Tabelle zu sehen.

(voter_id, updated_at)kommt nicht vorbei, voter_idda die Filterung noch nicht abgeschlossen ist WHERE. Da dann der andere Index kleiner ist, wird er ausgewählt. Meins hat 3 Spalten, um sich um das Filtern zu kümmern, dann die Spalte für ORDER BY.

Rick James
quelle