A3.2 Índices a fondo y consultas lentas¶
Esta página usa la tabla pedidos de A3.1, con las 50 000 filas, y su script de creación.
Índices con varias columnas: el orden importa¶
Un índice sobre (id_cliente, fecha) funciona como una agenda ordenada primero por cliente y, dentro de cada cliente, por fecha. Eso decide qué consultas puede acelerar. Con ese único índice:
Filtrando por las dos columnas, usa el índice completo:
CREATE INDEX idx_cliente_fecha ON pedidos(id_cliente, fecha);
EXPLAIN QUERY PLAN
SELECT * FROM pedidos WHERE id_cliente = 42 AND fecha >= '2024-06-01';
Resultado:
| id | parent | notused | detail |
|---|---|---|---|
| 3 | 0 | 51 | SEARCH pedidos USING INDEX idx_cliente_fecha (id_cliente=? AND fecha>?) |
1 fila
Filtrando solo por la primera columna, también le sirve (es como buscar en la agenda solo por el apellido):
CREATE INDEX idx_cliente_fecha ON pedidos(id_cliente, fecha);
EXPLAIN QUERY PLAN
SELECT * FROM pedidos WHERE id_cliente = 42;
Resultado:
| id | parent | notused | detail |
|---|---|---|---|
| 3 | 0 | 62 | SEARCH pedidos USING INDEX idx_cliente_fecha (id_cliente=?) |
1 fila
Filtrando solo por la segunda columna, no le sirve: en una agenda ordenada por apellido, buscar por nombre obliga a leerla toda:
CREATE INDEX idx_cliente_fecha ON pedidos(id_cliente, fecha);
EXPLAIN QUERY PLAN
SELECT * FROM pedidos WHERE fecha = '2024-06-01';
Resultado:
| id | parent | notused | detail |
|---|---|---|---|
| 2 | 0 | 216 | SCAN pedidos |
1 fila
La regla: un índice compuesto sirve para las consultas que filtran por sus primeras columnas, en orden ((a), (a, b)...), pero no para las que se saltan la primera. Por eso se pone primero la columna que más se usa en igualdad (=) y después las de rangos (>, <).
Consultas que no pueden usar el índice¶
Un índice sobre fecha ordena las fechas tal cual están guardadas. Si en el WHERE transformas la columna con una función, la base de datos ya no puede buscar en el índice, porque lo que compara es otra cosa. Contar los pedidos de marzo de 2024, de dos formas:
Con una función sobre la columna (substr):
CREATE INDEX idx_pedidos_fecha ON pedidos(fecha);
EXPLAIN QUERY PLAN
SELECT COUNT(*) FROM pedidos WHERE substr(fecha, 1, 7) = '2024-03';
Resultado:
| id | parent | notused | detail |
|---|---|---|---|
| 3 | 0 | 213 | SCAN pedidos USING COVERING INDEX idx_pedidos_fecha |
1 fila
Con un rango sobre la columna, que dice lo mismo sin tocarla:
CREATE INDEX idx_pedidos_fecha ON pedidos(fecha);
EXPLAIN QUERY PLAN
SELECT COUNT(*) FROM pedidos
WHERE fecha >= '2024-03-01' AND fecha < '2024-04-01';
Resultado:
| id | parent | notused | detail |
|---|---|---|---|
| 3 | 0 | 153 | SEARCH pedidos USING COVERING INDEX idx_pedidos_fecha (fecha>? AND fecha<?) |
1 fila
La primera recorre el índice entero (SCAN ... USING COVERING INDEX: las 50 000 entradas) y la segunda va directamente al rango (SEARCH ... (fecha>? AND fecha<?)). Las dos devuelven el mismo número de pedidos:
CREATE INDEX idx_pedidos_fecha ON pedidos(fecha);
SELECT COUNT(*) AS pedidos_de_marzo FROM pedidos
WHERE fecha >= '2024-03-01' AND fecha < '2024-04-01';
Resultado:
| pedidos_de_marzo |
|---|
| 4247 |
1 fila
A las consultas que sí pueden aprovechar un índice se les llama sargables (del inglés SARG-able, «argumento de búsqueda»). Lo que las rompe:
| Rompe el índice | Alternativa |
|---|---|
Una función sobre la columna (substr(fecha, 1, 7) = ..., LOWER(nombre) = ...) |
Un rango sobre la columna, o guardar el dato ya transformado |
Una operación sobre la columna (importe * 2 > 100) |
Pasa la operación al otro lado: importe > 50 |
LIKE '%texto' (comodín al principio) |
Ninguno sencillo: ningún índice ordenado ayuda a buscar «lo que acaba en» |
Un LIKE con el comodín al principio siempre recorre la tabla entera:
EXPLAIN QUERY PLAN
SELECT * FROM pedidos WHERE producto LIKE '%fé';
Resultado:
| id | parent | notused | detail |
|---|---|---|---|
| 2 | 0 | 216 | SCAN pedidos |
1 fila
Las estadísticas: ANALYZE¶
Para decidir entre dos formas de ejecutar una consulta, el motor necesita saber cómo son los datos: cuántas filas hay y cuántas se repiten por cada valor. Eso lo guarda ANALYZE, en una tabla del propio sistema:
CREATE INDEX idx_pedidos_cliente ON pedidos(id_cliente);
CREATE INDEX idx_pedidos_fecha ON pedidos(fecha);
ANALYZE;
SELECT tbl, idx, stat FROM sqlite_stat1 ORDER BY idx;
Resultado:
| tbl | idx | stat |
|---|---|---|
| pedidos | idx_pedidos_cliente | 50000 25 |
| pedidos | idx_pedidos_fecha | 50000 137 |
2 filas
Cada stat son números: el primero es el total de filas; los siguientes, cuántas filas coinciden de media con cada valor de la primera columna del índice. idx_pedidos_cliente dice 50000 25: 50 000 filas y unas 25 por cliente; idx_pedidos_fecha dice 137: unas 137 por fecha. Con estos números el motor sabe que buscar por cliente devuelve pocas filas y que merece la pena usar el índice. En otros motores el equivalente es ANALYZE (PostgreSQL, MySQL) o las estadísticas del optimizador (Oracle, SQL Server).
Paginar bien¶
Una web que muestra los pedidos de cinco en cinco necesita saltar a una página concreta. La forma habitual es LIMIT con OFFSET:
SELECT id, id_cliente, importe FROM pedidos ORDER BY id LIMIT 5 OFFSET 40000;
Funciona, pero la base de datos tiene que recorrer y descartar las 40 000 filas anteriores para llegar a la página. Cuanto más avanzas, más tarda. La alternativa, la paginación por clave, recuerda el último id visto y pide «los siguientes»:
SELECT id, id_cliente, importe
FROM pedidos
WHERE id > 40000
ORDER BY id
LIMIT 5;
Resultado:
| id | id_cliente | importe |
|---|---|---|
| 40001 | 1920 | 21 |
| 40002 | 1839 | 52 |
| 40003 | 1758 | 83 |
| 40004 | 1677 | 24 |
| 40005 | 1596 | 55 |
5 filas
Da exactamente la misma página, pero va directamente a la posición por la clave primaria, con el mismo coste en la página 1 que en la 8000. Su limitación: solo permite pasar a la página siguiente (o anterior), no saltar a una cualquiera.
Errores frecuentes¶
| Error | Cómo evitarlo |
|---|---|
| Poner una columna que no se filtra por igualdad primero en el índice compuesto | Primero las de =, luego las de rango |
Un índice sobre fecha y filtrar con substr(fecha, ...) o strftime(...) |
Filtra con un rango sobre la columna |
LIKE '%texto' esperando que use el índice |
No puede: busca otro enfoque (búsqueda de texto completo) |
Olvidar ANALYZE en una tabla que ha cambiado mucho |
Actualiza las estadísticas tras cargas grandes |
Paginar con OFFSET enormes |
Paginación por clave |
| Un índice de más para cada consulta distinta | Mira cuáles usan de verdad tus consultas frecuentes y borra el resto |
Para practicar¶
Los ejercicios SA3.4, SA3.5 y SA3.6 de SA3 · Ejercicios usan rangos, estadísticas y paginación por clave.