¿Por qué MySQL ignora el índice incluso en vigencia para este orden?

14

Ejecuto 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)  

Los índices en mi tabla:

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)  

Hay un índice en last_name pero el optimizador no lo usa.
Así que hago:

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)  

¡Pero todavía no se usa el índice ! ¿Qué estoy haciendo mal aquí?
¿Tiene que ver con el hecho de que el índice es NON_UNIQUE? Por cierto el apellido esVARCHAR(1000)

Actualización solicitada por @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)  
Cratilo
fuente
Ejecute estas dos consultas: 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;. ¿Cuál es el resultado de cada recuento?
RolandoMySQLDBA
@RolandoMySQLDBA: Actualicé OP con la información que solicitó.
Cratylus
Dos consultas más, por favor: 1) SELECT COUNT(1) FullTableCount FROM employees;y 2) SELECT * FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A LIMIT 10;.
RolandoMySQLDBA
No importa, veo la explicación con lo que necesito.
RolandoMySQLDBA
2
@Cratylus que aceptó una respuesta incorrecta, usted debe aceptar la correcta respuesta de Michael-sqlbot
miracle173

Respuestas:

6

PROBLEMA # 1

Mira la consulta

select last_name from employees order by last_name;

No veo una cláusula WHERE significativa, y tampoco MySQL Query Optimizer. No hay incentivo para usar un índice.

PROBLEMA # 2

Mira la consulta

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

Le dio un índice, pero el Opitmizer de consulta se hizo cargo. He visto este comportamiento antes ( ¿Cómo forzo a JOIN a usar un índice específico en MySQL? )

¿Por qué debería pasar esto?

Sin una WHEREcláusula, Query Optimizer se dice lo siguiente:

  • Esta es una tabla InnoDB
  • Es una columna indexada
  • El índice tiene el row_id del gen_clust_index (también conocido como Clustered Index)
  • ¿Por qué debería mirar el índice cuando
    • no hay WHEREclausula?
    • ¿Siempre tendría que volver a la mesa?
  • Como todas las filas de una tabla InnoDB residen en los mismos bloques de 16K que gen_clust_index, haré un análisis completo de la tabla.

El Optimizador de consultas eligió el camino de menor resistencia.

Tendrás un pequeño shock, pero aquí está: ¿Sabías que el Optimizador de consultas manejará MyISAM de manera bastante diferente?

¿Probablemente estás diciendo HUH ???? CÓMO ????

MyISAM almacena los datos en un .MYDarchivo y todos los índices en el .MYIarchivo.

La misma consulta producirá un plan EXPLAIN diferente porque el índice vive en un archivo diferente de los datos. Por qué ? Aquí es por qué:

  • Los datos necesarios ( last_namecolumna) ya están ordenados en el.MYI
  • En el peor de los casos, tendrá un escaneo de índice completo
  • Solo accederás a la columna last_namedesde el índice
  • No es necesario tamizar a través de indeseados
  • No activará la creación de archivos temporales para ordenar

¿Cómo puede estar tan seguro de esto? He probado esta teoría de trabajo sobre cómo el uso de un almacenamiento diferente generará un plan EXPLICAR diferente (a veces mejor): ¿Debe un índice cubrir todas las columnas seleccionadas para que se use ORDER BY?

RolandoMySQLDBA
fuente
1
-1 @Rolando esta respuesta no es menos precisa que la respuesta correcta de Michael-sqlbot pero es incorrecta, por ejemplo, el manual dice: "MySQL usa índices para estas operaciones: (...) Para ordenar o agrupar una tabla si la ordenación o la agrupación se realiza en el prefijo más a la izquierda de un índice utilizable (...) ". También algunas de las otras declaraciones de su publicación son discutibles. Le recomendaría que elimine esta respuesta o que la reelabore.
milagro173
Esta respuesta no es correcta. Aún se puede usar un índice incluso si no hay una cláusula WHERE si se evita la clasificación.
Oysteing
19

En realidad, el problema aquí es que esto parece un índice de prefijo. No veo la definición de la tabla en la pregunta, pero sub_part= 700? No ha indexado toda la columna, por lo que el índice no se puede usar para ordenar y tampoco es útil como índice de cobertura. Solo se podría usar para encontrar las filas que "podrían" coincidir con WHEREay la capa del servidor (sobre el motor de almacenamiento) tendría que filtrar aún más las filas coincidentes. ¿Realmente necesitas 1000 caracteres para un apellido?


Actualización para ilustrar: Tengo una tabla de prueba de tabla con un poco más de 500 filas, cada una con el nombre de dominio de un sitio web en una columna domain_name VARCHAR(254) NOT NULLy sin índices.

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

Con la columna completa indexada, la consulta usa el índice:

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)

Entonces, ahora, soltaré ese índice y solo indexaré los primeros 200 caracteres de 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

Tenga en cuenta también que el índice, con 200 caracteres, es más largo que el valor más largo en la columna ...

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

... pero eso no hace ninguna diferencia. Un índice declarado con una longitud de prefijo solo se puede usar para búsquedas, no para ordenar, y no como índice de cobertura, ya que no contiene el valor de columna completo, por definición.

Además, las consultas anteriores se ejecutaron en una tabla InnoDB, pero ejecutarlas en una tabla MyISAM arroja resultados prácticamente idénticos. La única diferencia en este caso es que el recuento de InnoDB rowsestá ligeramente desactivado (541) mientras que MyISAM muestra el número exacto de filas (563), que es un comportamiento normal ya que los dos motores de almacenamiento manejan inmersiones de índice de manera muy diferente.

Todavía afirmaría que la columna last_name es probablemente más grande de lo necesario, pero aún es posible indexar toda la columna, si está utilizando InnoDB y ejecuta MySQL 5.5 o 5.6:

De manera predeterminada, una clave de índice para un índice de una sola columna puede tener hasta 767 bytes. El mismo límite de longitud se aplica a cualquier prefijo de clave de índice. Consulte la Sección 13.1.13, " CREATE INDEXSintaxis". Por ejemplo, puede alcanzar este límite con un índice de prefijo de columna de más de 255 caracteres en una columna TEXTo VARCHAR, suponiendo un UTF-8conjunto de caracteres y el máximo de 3 bytes para cada carácter. Cuando la innodb_large_prefixopción de configuración está habilitada, este límite de longitud se eleva a 3072 bytes, para las InnoDBtablas que usan los formatos de fila DYNAMICy COMPRESSED.

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

Michael - sqlbot
fuente
Interesante punto de vista. La columna es varchar(1000)pero esto está más allá del índice máximo permitido para el índice, que es ~ 750
Cratylus
8
Esta respuesta debe ser la aceptada.
ypercubeᵀᴹ
1
@ypercube Esta respuesta es más precisa que la mía. +1 por su comentario y +1 por esta respuesta. Que esto se acepte en mi lugar.
RolandoMySQLDBA
1
@Timo, esa es una pregunta interesante ... que sugeriría publicar como una nueva pregunta, aquí, tal vez con un enlace a esta respuesta, por contexto. Publique la salida completa desde EXPLAIN SELECT ..., así como SHOW CREATE TABLE ...y SELECT @@VERSION;desde que los cambios en el optimizador en todas las versiones pueden ser relevantes.
Michael - sqlbot el
1
Por ahora puedo informar que (al menos para 5.7) un índice de prefijo no ayuda con la indexación nula, como pedí en mi comentario anterior.
Timo
2

Respondí porque un comentario no admitirá el formateo y RolandoMySQL DBA habló sobre gen_clust_index e innodb. Y esto es muy importante en una tabla basada en innodb. Esto va más allá del conocimiento normal de DBA porque necesita poder analizar el código C ..

SIEMPRE debe hacer SIEMPRE una CLAVE PRIMARIA o una CLAVE ÚNICA si está usando Innodb. Si no lo hace, innodb usará su propio ROW_ID generado que podría hacerle más daño que bien.

Intentaré explicarlo fácilmente porque la prueba se basa en el código 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);
}

Primer problema

mutex_enter (& (dict_sys-> mutex));

Esta línea se asegura de que solo un hilo pueda acceder a dict_sys-> mutex al mismo tiempo. ¿Qué pasaría si el valor ya tuviera un mutexed? Sí, un subproceso tiene que esperar para obtener una característica aleatoria agradable, como el bloqueo de subprocesos o si tiene más tablas sin su propia PRIMARY KEY o UNIQUE KEY, entonces tendría una buena característica con innodb ' bloqueo de tabla ' no es esta la razón por la cual MyISAM fue reemplazado por InnoDB porque fuera de la característica agradable llamada bloqueo basado en registros / filas.

Segundo problema

(0 == (id% DICT_HDR_ROW_ID_WRITE_MARGIN))

los cálculos de módulo (%) son lentos, no son buenos si está insertando por lotes porque necesita ser recalculado cada vez ..., y porque DICT_HDR_ROW_ID_WRITE_MARGIN (valor 256) es una potencia de dos, esto podría hacerse mucho más rápido.

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

Nota al margen si el compilador de C se configuró para optimizar y es un buen optimizador, el optimizador de C reparará el código "pesado" a la versión más ligera

el lema de la historia siempre crea tu propia CLAVE PRIMARIA o asegúrate de tener un índice ÚNICO cuando creas una tabla desde el principio

Raymond Nijland
fuente
Agregue la replicación basada en filas y el hecho de que los ID de fila no son consistentes en todos los servidores, y el punto de Raymond sobre la creación de una clave primaria siempre es aún más importante.
No sugiera que UNIQUEsea ​​suficiente; también debe incluir solo columnas que no sean NULL para que el índice único se promocione a PK.
Rick James
"los cálculos de módulo (%) son lentos": más importante es qué porcentaje del tiempo de un tiempo INSERTse dedica a esta función. Sospecho que es insignificante. Contraste el esfuerzo de palear columnas, haga operaciones de BTree, incluyendo una división de bloques ocasional, varios mutexes en el buffer_pool, cosas de cambio de buffer, etc.
Rick James
Verdadero @RickJames, la sobrecarga puede ser un número muy pequeño, pero también se suman muchos números pequeños (aún sería una micro optimización). Además, el primer problema es el más problemático
Raymond Nijland el