¿Es realmente necesario que todas las columnas seleccionadas se indexen para que MySQL elija usar el índice?
Esta es una pregunta cargada porque hay factores que determinan si vale la pena usar un índice.
FACTOR # 1
Para cualquier índice dado, ¿cuál es la población clave? En otras palabras, ¿cuál es la cardinalidad (recuento distinto) de todas las tuplas registradas en el índice?
FACTOR # 2
¿Qué motor de almacenamiento estás usando? ¿Se puede acceder a todas las columnas necesarias desde un índice?
QUE SIGUE ???
Tomemos un ejemplo simple: una tabla que contiene dos valores (Masculino y Femenino)
Dejemos crear una tabla con una prueba para el uso del índice
USE test
DROP TABLE IF EXISTS mf;
CREATE TABLE mf
(
id int not null auto_increment,
gender char(1),
primary key (id),
key (gender)
) ENGINE=InnODB;
INSERT INTO mf (gender) VALUES
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
ANALYZE TABLE mf;
EXPLAIN SELECT gender FROM mf WHERE gender='F';
EXPLAIN SELECT gender FROM mf WHERE gender='M';
EXPLAIN SELECT id FROM mf WHERE gender='F';
EXPLAIN SELECT id FROM mf WHERE gender='M';
PRUEBA InnoDB
mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE mf
-> (
-> id int not null auto_increment,
-> gender char(1),
-> primary key (id),
-> key (gender)
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.07 sec)
mysql> INSERT INTO mf (gender) VALUES
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.06 sec)
Records: 40 Duplicates: 0 Warnings: 0
mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status | OK |
+---------+---------+----------+----------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql>
PRUEBA MyISAM
mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE mf
-> (
-> id int not null auto_increment,
-> gender char(1),
-> primary key (id),
-> key (gender)
-> ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.05 sec)
mysql> INSERT INTO mf (gender) VALUES
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.00 sec)
Records: 40 Duplicates: 0 Warnings: 0
mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status | OK |
+---------+---------+----------+----------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 36 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | mf | ALL | gender | NULL | NULL | NULL | 40 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)
mysql>
Análisis para InnoDB
Cuando los datos se cargaron como InnoDB, tenga en cuenta que los cuatro EXPLAIN
planes utilizaron el gender
índice. Los EXPLAIN
planes tercero y cuarto utilizaron el gender
índice a pesar de que los datos solicitados eran id
. ¿Por qué? Porque id
está en PRIMARY KEY
y todos los índices secundarios tienen punteros de referencia de regreso a PRIMARY KEY
(a través de gen_clust_index ).
Análisis para MyISAM
Cuando los datos se cargaron como MyISAM, tenga en cuenta que los primeros tres EXPLAIN
planes utilizaron el gender
índice. En el cuarto EXPLAIN
plan, el Optimizador de consultas decidió no utilizar ningún índice. Optó por un escaneo completo de la tabla en su lugar. ¿Por qué?
Independientemente de DBMS, los optimizadores de consultas funcionan con una regla general muy simple: si se analiza un índice como candidato para realizar la búsqueda y el Optimizador de consultas calcula que debe buscar más del 5% del número total de filas en la tabla:
- se realiza una exploración completa del índice si todas las columnas necesarias para la recuperación están en el índice seleccionado
- una exploración de tabla completa de lo contrario
CONCLUSIÓN
Si no tiene índices de cobertura adecuados, o si la población clave para cualquier tupla es más del 5% de la tabla, deben suceder seis cosas:
- Darse cuenta de que debe perfilar las consultas
- Encuentra todos
WHERE
, GROUP BY
y el orden BY` cláusulas de esas consultas
- Formule índices en este orden
WHERE
columnas de cláusula con valores estáticos
GROUP BY
columnas
ORDER BY
columnas
- Evite escaneos de tabla completa (consultas que carecen de una
WHERE
cláusula sensata )
- Evite poblaciones de clave incorrecta (o al menos guarde en caché esas poblaciones de clave incorrecta)
- Decidir sobre el mejor motor de almacenamiento de MySQL ( InnoDB o MyISAM ) de las Tablas
He escrito sobre esta regla general del 5% en el pasado:
ACTUALIZACIÓN 2012-11-14 13:05 EDT
Eché un vistazo a tu pregunta y a la publicación SO original . Entonces, pensé en lo Analysis for InnoDB
que mencioné antes. Coincide con la person
mesa. ¿Por qué?
Para ambas mesas mf
yperson
- El motor de almacenamiento es InnoDB
- La clave primaria es
id
- El acceso a la tabla es por índice secundario
- Si la tabla fuera MyISAM, veríamos un
EXPLAIN
plan completamente diferente
Ahora, mira a la consulta de la cuestión SO: select * from person order by age\G
. Como no hay WHERE
cláusula, usted solicitó explícitamente un escaneo completo de la tabla . El orden de clasificación predeterminado de la tabla sería por id
(PRIMARY KEY) debido a su auto_increment y el gen_clust_index (también conocido como Clustered Index) está ordenado por rowid interno . Cuando ordenó por el índice, tenga en cuenta que los índices secundarios de InnoDB tienen el rowid adjunto a cada entrada de índice. Esto produce la necesidad interna de acceso completo a las filas cada vez.
Configurar ORDER BY
en una tabla InnoDB puede ser una tarea bastante desalentadora si ignora estos hechos sobre cómo se organizan los índices de InnoDB.
Volviendo a esa consulta SO, ya que explícitamente exigió un escaneo completo de la tabla , en mi humilde opinión, MySQL Query Optimizer hizo lo correcto (o al menos, eligió el camino de menor resistencia). Cuando se trata de InnoDB y la consulta SO, es mucho más fácil realizar un escaneo completo de la tabla y luego algunos en filesort
lugar de hacer un escaneo de índice completo y una búsqueda de fila a través del gen_clust_index para cada entrada de índice secundario.
No soy partidario del uso de Index Hints porque ignora el plan EXPLAIN. No obstante, si realmente conoce sus datos mejor que InnoDB, tendrá que recurrir a Indicaciones de índice, especialmente con consultas que no tienen WHERE
cláusula.
ACTUALIZACIÓN 2012-11-14 14:21 EDT
De acuerdo con el libro Understanding MySQL Internals
El párrafo 7 de la página 202 dice lo siguiente:
Los datos se almacenan en una estructura especial llamada índice agrupado , que es un árbol B con la clave primaria que actúa como el valor clave, y el registro real (en lugar de un puntero) en la parte de datos. Por lo tanto, cada tabla InnoDB debe tener una clave primaria. Si no se proporciona uno, se agrega una columna de ID de fila especial que normalmente no es visible para el usuario para que actúe como clave principal. Una clave secundaria almacenará el valor de la clave primaria que identifica el registro. El código del árbol B se puede encontrar en innobase / btr / btr0btr.c .
Es por eso que dije antes: es mucho más fácil realizar un escaneo completo de la tabla y luego ordenar algunos archivos en lugar de hacer un escaneo de índice completo y una búsqueda de fila a través del gen_clust_index para cada entrada de índice secundaria . InnoDB va a hacer una búsqueda de doble índice cada vez . Eso suena un poco brutal, pero esos son solo los hechos. Nuevamente, tenga en cuenta la falta de WHERE
cláusula. Esto, en sí mismo, es una pista para que MySQL Query Optimizer haga un escaneo completo de la tabla.
FOR ORDER BY
(que es el caso específico en esta pregunta). La pregunta decía que en este caso el motor de almacenamiento eraInnoDB
(y la pregunta SO original muestra que las 10k filas están distribuidas de manera bastante uniforme en 8 elementos, la cardinalidad tampoco debería ser un problema aquí). Lamentablemente, no creo que esto responda la pregunta.