Perché mysql utilizza l'indice errato per ordine per query?


9

Ecco la mia tabella con ~ 10.000.000 di righe di dati

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;

Ecco le cardinalità degli indici

inserisci qui la descrizione dell'immagine

Quindi quando faccio questa domanda:

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;

Mi aspettavo che usasse l'indice voter_timestamp ma mysql ha scelto di usare questo invece:

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

E ho ottenuto un tempo di interrogazione di 200-400 ms.

Se lo costringo a usare l'indice giusto come:

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 può restituire i risultati in 1-2ms

ed ecco la spiegazione:

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

Allora perché mysql non ha scelto l' voter_timestampindice per la mia query originale?

Quello che avevo provato è analyze table votes, optimize table voteseliminare quell'indice e aggiungerlo di nuovo, ma mysql utilizza ancora l'indice errato. non capisco bene qual è il problema.


1
@ ypercubeᵀᴹ Non credo sia necessario indicizzare tutte le colonne nella condizione where, come vedi se forzo a usare l'indice (voter_id, updated_at), può usarlo ed essere molto efficiente. Se rimuovo la subject_name = "medium"parte può anche scegliere l'indice giusto, non c'è bisogno di indicizzarerate
Phoenix

Tuttavia, l'indice a 4 colonne sarà più efficiente del 2 (voter_id, updated_at). Un altro indice sarebbe (voter_id, subject_name, updated_at)o (subject_name, voter_id, updated_at)(senza il tasso).
ypercubeᵀᴹ

1
E sì, hai ragione su un certo punto. Non è necessario l'indice a 4 colonne. È solo il miglior indice possibile per questa query. La colonna 2 (che ritieni sia "giusta") forse va bene per i dati e la distribuzione che hai attualmente. Con una distribuzione diversa, potrebbe essere orribile. Esempio: supponiamo che il 99% delle righe abbia un tasso> 1 e che solo l'1% abbia un tasso = 1. Pensi che l'uso dell'indice a 2 colonne sarebbe efficace?
ypercubeᵀᴹ

Dovrebbe attraversare gran parte dell'indice e fare migliaia di ricerche sulla tabella, solo per trovare quel tasso> 1 e rifiutare le righe, fino a trovare 120 che soddisfano i criteri che non possono essere giudicati dall'indice ( subject_name='medium' and rate=1)
ypercubeᵀᴹ

ypercube, Phoenix - MySQL non riuscirà a raggiungere LIMITo addirittura a ORDER BYmeno che l'indice non soddisfi prima tutti i filtri. Cioè, senza le 4 colonne complete, raccoglierà tutte le righe pertinenti, le ordinerà tutte, quindi sceglierà LIMIT. Con l'indice a 4 colonne, la query può evitare l'ordinamento e arrestarsi dopo aver letto solo le LIMITrighe.
Rick James,

Risposte:


5

MySQL sta utilizzando un modello di costo relativamente semplice (più semplice rispetto agli altri RDBMS) per pianificare query in cui il filtraggio del set di dati ha una priorità piuttosto elevata. Nella tua prima query con l'indice di unione si stima che sarà necessaria la scansione di ~ 9000 righe mentre la seconda con l'indice dell'indice richiederà 18000. La mia scommessa sarebbe che questo pesa nel calcolo abbastanza da spostare la scala verso l'unione . Puoi confermarlo (o trovare altri motivi) accendendo optimizer_trace, eseguendo la query e valutando i risultati.

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`;

Un'osservazione su index_merge: nella maggior parte dei casi scoprirai che è piuttosto costoso. Sebbene sia molto utile per gli scenari di tipo OLAP, potrebbe non essere molto adatto per OLTP poiché l'operazione può richiedere un tempo significativo della query e, come si può vedere, a volte il piano di esecuzione non ottimale è in realtà più veloce.

Fortunatamente MySQL fornisce switch per l'ottimizzatore in modo che tu possa personalizzarlo come desideri.

Per tutte le opzioni è possibile eseguire:

show global variables like 'optimizer_switch';

Per cambiarne uno non è necessario copiare incollare l'intera stringa. Funziona come dict.update()in Python.

 set global optimizer_switch='index_merge=off';

Se possibile, darei anche un'occhiata alla struttura del tuo tavolo e migliorerò. Avere una chiave primaria di ~ 100 byte con molte chiavi secondarie non è davvero consigliato.

Hai quattro chiavi secondarie e alcune di esse sono superflue, ad esempio (voter_id)index è un sottoinsieme di(voter_id, updated_at)


"Index merge intersect" è raramente utilizzato da MySQL. In forse tutti i casi, è significativamente meglio avere un indice con più colonne. "L'indice unione unione" è talvolta utile; trasformarsi ORin UNIONè spesso buono o migliore.
Rick James,

5

Per quella query, è necessario questo indice:

INDEX(voter_id, rate, subject_name, updated_at)

L' updated_atdeve essere l'ultima; gli altri tre possono essere in qualsiasi ordine. (Gli indici a 3 colonne di ypercube non sono molto utili poiché non finiscono le WHEREcolonne prima di colpire la ORDER BYcolonna.)

Quando aggiungi questo indice, probabilmente puoi sbarazzarti di tutte le altre chiavi secondarie:

KEY IDX_518B7ACFEBB4B8AD( voter_id), - L'FK può usare la mia chiave di indice subject_timestamp( subject_name, subject_id, updated_at), - KEY lo più ridondante voter_timestamp( voter_id, updated_at), - potrebbe essere stato il tentativo di

Con l'indice a 4 colonne, hai la possibilità di ottimizzare la "impaginazione" ed evitare OFFSET. Vedi questo blog

Su un altro argomento ... Quando vedo X_namee X_idpresumo che la "normalizzazione" stia succedendo. Mi aspetterei di vedere quelle due colonne in una tabella, praticamente nient'altro. Non mi aspetterei di vedere entrambi in qualche altra tabella.

(voter_id, updated_at)non passerà voter_idpoiché non ha terminato il filtro (il WHERE). Quindi, poiché l'altro indice è più piccolo, viene raccolto. Il mio ha 3 colonne per occuparsi del filtraggio, quindi la colonna per ORDER BY.

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.