¿Por qué mysql usa el índice incorrecto para ordenar por consulta?

9

Aquí está mi tabla con ~ 10,000,000 filas de datos

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;

Aquí están los índices de cardinalidades

ingrese la descripción de la imagen aquí

Entonces cuando hago esta consulta:

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;

Esperaba que voter_timestamp usara índice pero mysql elige usar esto en su lugar:

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

Y obtuve 200-400ms de tiempo de consulta.

Si lo fuerzo a usar el índice correcto como:

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 puede devolver los resultados en 1-2 ms

y aquí está la explicación:

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

Entonces, ¿por qué mysql no eligió el voter_timestampíndice para mi consulta original?

Lo que había intentado es analyze table votes, optimize table votessoltar ese índice y agregarlo nuevamente, pero mysql todavía usa el índice incorrecto. No entiendo bien cuál es el problema.

Fénix
fuente
1
@ ypercubeᵀᴹ No creo que sea necesario indexar todas las columnas en la condición where, como puede ver si fuerzo a usar el índice (voter_id, updated_at), puede usarlo y ser muy eficiente. Si elimino la subject_name = "medium"pieza, también puede elegir el índice correcto, no es necesario indexarrate
Phoenix
Aún así, el índice de 4 columnas será más eficiente que el 2 (voter_id, updated_at). Otro índice sería (voter_id, subject_name, updated_at)o (subject_name, voter_id, updated_at)(sin la tasa).
ypercubeᵀᴹ
1
Y sí, tienes razón, en algún momento. No necesita el índice de 4 columnas. Es el mejor índice posible para esta consulta. La columna 2 (que crees que es "correcta") puede estar bien para los datos y la distribución que tienes actualmente. Con una distribución diferente, podría ser horrible. Ejemplo: supongamos que el 99% de las filas tenían una tasa> 1 y solo el 1% tenía una tasa = 1. ¿Crees que usar el índice de 2 columnas sería eficiente?
ypercubeᵀᴹ
Tendría que atravesar una gran parte del índice y realizar miles de búsquedas en la tabla, solo para encontrar esa tasa> 1 y rechazar las filas, hasta que encuentre 120 que se ajusten a los criterios que el índice no puede juzgar ( subject_name='medium' and rate=1)
ypercubeᵀᴹ
ypercube, Phoenix: MySQL no llegará al LIMITo incluso a ORDER BYmenos que el índice satisfaga primero todo el filtrado. Es decir, sin las 4 columnas completas, recopilará todas las filas relevantes, las ordenará todas y luego las eliminará LIMIT. Con el índice de 4 columnas, la consulta puede evitar la clasificación y detenerse después de leer solo las LIMITfilas.
Rick James

Respuestas:

5

MySQL está utilizando un modelo de costos relativamente simple (más simple que otros RDBMS) para planificar consultas en las que filtrar su conjunto de datos tiene una prioridad bastante alta. En su primera consulta con el índice de fusión, se estima que será necesario escanear ~ 9000 filas, mientras que la segunda con la sugerencia de índice requerirá 18000. Mi apuesta sería que esto pesa en el cálculo lo suficiente como para mover la escala hacia la fusión. . Puede confirmar esto (o encontrar otros motivos) activando optimizer_trace, ejecutando su consulta y evaluando los resultados.

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 comentario sobre index_merge: en la mayoría de los casos, encontrará que es bastante costoso. Aunque es muy útil para escenarios de tipo OLAP, podría no ser muy adecuado para OLTP porque la operación puede llevar un tiempo considerable de su consulta y, como puede ver, a veces el plan de ejecución subóptimo es en realidad más rápido.

Afortunadamente, MySQL proporciona conmutadores para el optimizador para que pueda personalizarlo como desee.

Para todas las opciones que puede ejecutar:

show global variables like 'optimizer_switch';

Para cambiar uno, no tiene que copiar y pegar toda la cadena. Funciona como dict.update()en python.

 set global optimizer_switch='index_merge=off';

Si es posible, también examinaría la estructura de su tabla y mejoraría. No se recomienda tener una clave primaria de ~ 100 bytes con muchas claves secundarias.

Tiene cuatro claves secundarias y algunas de ellas son superfluas, por ejemplo, el (voter_id)índice es un subconjunto de(voter_id, updated_at)

Károly Nagy
fuente
MySQL rara vez utiliza "Index merge intersect". Quizás en todos los casos, es significativamente mejor tener un índice con más columnas. "Unión de índice de fusión" es a veces útil; convertirse ORen a UNIONmenudo es tan bueno o mejor.
Rick James
5

Para esa consulta, necesita este índice:

INDEX(voter_id, rate, subject_name, updated_at)

El updated_atdebe ser el último; los otros tres pueden estar en cualquier orden. (Los índices de 3 columnas de ypercube no son muy útiles ya que no terminan las WHEREcolumnas antes de golpear la ORDER BYcolumna).

A medida que agrega este índice, probablemente pueda deshacerse de todas las demás claves secundarias:

KEY IDX_518B7ACFEBB4B8AD( voter_id), - El FK puede usar mi clave de índice subject_timestamp( subject_name, subject_id, updated_at), - CLAVE mayormente redundante voter_timestamp( voter_id, updated_at), - que puede haber sido su intento

Con el índice de 4 columnas, tiene la posibilidad de optimizar la "paginación" y evitarla OFFSET. Ver este blog

Sobre otro tema ... Cuando veo X_namey X_id, supongo que está ocurriendo la "normalización". Esperaría ver esas dos columnas en una tabla, con prácticamente nada más. Yo no esperaría ver tanto en alguna otra mesa.

(voter_id, updated_at)no pasará voter_idya que no ha terminado con el filtrado (the WHERE). Luego, dado que el otro índice es más pequeño, se selecciona. El mío tiene 3 columnas para encargarse del filtrado, luego la columna para ORDER BY.

Rick James
fuente