Saltar a contenido

A3.1 Leer el plan de ejecución

Para quién es esto

Es material avanzado. Da por sabido qué es un índice, de la unidad 6.

Una tabla grande para notar la diferencia

Para ver qué cambia con y sin índice hace falta una tabla con muchas filas. Esta tiene 50 000 pedidos (los datos son ficticios y se generan con una CTE recursiva). Para reproducir los ejemplos, ejecuta antes:

CREATE TABLE pedidos (
  id         INTEGER PRIMARY KEY,
  id_cliente INTEGER NOT NULL,
  producto   TEXT    NOT NULL,
  importe    INTEGER NOT NULL,
  fecha      TEXT    NOT NULL
);
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 50000)
INSERT INTO pedidos
SELECT i,
       (i * 7919) % 2000 + 1,
       CASE i % 4 WHEN 0 THEN 'café' WHEN 1 THEN 'té' WHEN 2 THEN 'zumo' ELSE 'tarta' END,
       (i * 31) % 90 + 10,
       date('2024-01-01', '+' || (i % 365) || ' days')
FROM n;

EXPLAIN QUERY PLAN

Delante de cualquier consulta, EXPLAIN QUERY PLAN no la ejecuta: cuenta cómo la ejecutaría. El resultado trae varias columnas, pero la que importa es detail (notused no significa nada y puede cambiar según la versión). En otros motores se llama EXPLAIN (MySQL, PostgreSQL) o EXPLAIN PLAN (Oracle), y la salida es distinta, pero la idea es la misma.

Los pedidos del cliente 42, sin ningún índice:

EXPLAIN QUERY PLAN
SELECT * FROM pedidos WHERE id_cliente = 42;

Resultado:

id parent notused detail
2 0 216 SCAN pedidos

1 fila

SCAN pedidos significa recorrer la tabla entera, fila a fila, para ver cuáles son del cliente 42. Con 50 000 filas es soportable; con 50 millones, no.

SEARCH: ir directo

Buscar por la clave primaria no necesita crear nada, porque la clave ya tiene su propia estructura:

EXPLAIN QUERY PLAN
SELECT * FROM pedidos WHERE id = 42;

Resultado:

id parent notused detail
2 0 33 SEARCH pedidos USING INTEGER PRIMARY KEY (rowid=?)

1 fila

Y si creamos un índice sobre id_cliente, el plan de la consulta de antes cambia:

CREATE INDEX idx_pedidos_cliente ON pedidos(id_cliente);
EXPLAIN QUERY PLAN
SELECT * FROM pedidos WHERE id_cliente = 42;

Resultado:

id parent notused detail
3 0 61 SEARCH pedidos USING INDEX idx_pedidos_cliente (id_cliente=?)

1 fila

Ahora dice SEARCH ... USING INDEX idx_pedidos_cliente (id_cliente=?): va directamente a las filas del cliente 42, sin mirar las demás. La consulta es la misma; solo ha cambiado cómo se ejecuta.

Cómo se lee un plan

Lo que dice el plan Qué significa
SCAN tabla Recorre toda la tabla: lo más caro
SEARCH tabla USING INTEGER PRIMARY KEY (rowid=?) Busca por la clave primaria: muy rápido
SEARCH tabla USING INDEX nombre (col=?) Busca por un índice: rápido
USING COVERING INDEX El índice contiene todas las columnas que se piden: ni siquiera mira la tabla
SCAN tabla USING INDEX Recorre el índice entero (en orden), no la tabla: mejor que lo primero, pero sigue siendo recorrer todo
USE TEMP B-TREE FOR ORDER BY / FOR GROUP BY Tiene que ordenar o agrupar aparte, en una estructura temporal
AUTOMATIC INDEX Ha creado un índice temporal solo para esta consulta: señal de que falta uno de verdad

La regla práctica: SEARCH es lo que quieres ver en las consultas que se repiten mucho y sobre tablas grandes. Un SCAN no siempre es un problema: sobre una tabla de 20 filas, da igual.

Índices de cobertura

Si el índice contiene todas las columnas que la consulta necesita, la base de datos no necesita mirar la tabla. Con un índice sobre (id_cliente, importe), sumar los importes de un cliente se resuelve solo con el índice:

CREATE INDEX idx_cliente_importe ON pedidos(id_cliente, importe);
EXPLAIN QUERY PLAN
SELECT SUM(importe) FROM pedidos WHERE id_cliente = 42;

Resultado:

id parent notused detail
3 0 53 SEARCH pedidos USING COVERING INDEX idx_cliente_importe (id_cliente=?)

1 fila

COVERING INDEX es lo más rápido que puede hacer: lee el índice, que es mucho más pequeño que la tabla, y termina.

Ordenar y agrupar

Agrupar u ordenar por una columna sin índice obliga a montar una estructura temporal:

EXPLAIN QUERY PLAN
SELECT * FROM pedidos ORDER BY importe LIMIT 5;

Resultado:

id parent notused detail
4 0 216 SCAN pedidos
19 0 0 USE TEMP B-TREE FOR ORDER BY

2 filas

USE TEMP B-TREE FOR ORDER BY significa que ordena las 50 000 filas para quedarse con cinco. Con un índice sobre importe, las filas ya están en orden dentro del índice: lee las cinco primeras y termina, sin ordenar nada:

CREATE INDEX idx_pedidos_importe ON pedidos(importe);
EXPLAIN QUERY PLAN
SELECT * FROM pedidos ORDER BY importe LIMIT 5;

Resultado:

id parent notused detail
5 0 223 SCAN pedidos USING INDEX idx_pedidos_importe

1 fila

Los JOIN

Con dos tablas, el plan dice cómo busca cada una. Aquí se une cada pedido con su cliente (y los clientes tienen su clave primaria). Para reproducirlo, crea antes también esta tabla:

CREATE TABLE clientes (
  id     INTEGER PRIMARY KEY,
  nombre TEXT NOT NULL,
  ciudad TEXT NOT NULL
);
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 2000)
INSERT INTO clientes
SELECT i, 'Cliente ' || i, CASE i % 3 WHEN 0 THEN 'Cádiz' WHEN 1 THEN 'Sevilla' ELSE 'Málaga' END
FROM n;
EXPLAIN QUERY PLAN
SELECT c.nombre, p.importe
FROM pedidos p
JOIN clientes c ON c.id = p.id_cliente
WHERE p.id = 10;

Resultado:

id parent notused detail
3 0 45 SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
6 0 45 SEARCH c USING INTEGER PRIMARY KEY (rowid=?)

2 filas

Son dos SEARCH por clave primaria: uno para el pedido 10 y otro para su cliente. Un JOIN es rápido cuando la columna por la que se une tiene índice (aquí, la clave primaria de clientes); cuando no lo tiene, es donde aparece AUTOMATIC INDEX.

Errores frecuentes

Error Cómo evitarlo
Optimizar sin medir Mira primero el plan: el cuello de botella casi nunca está donde se piensa
Crear un índice por cada columna «por si acaso» Cada índice ralentiza los INSERT, UPDATE y DELETE y ocupa espacio: solo los que usan tus consultas
Creer que SCAN es siempre malo Importa el tamaño de la tabla y cuántas veces se ejecuta la consulta
Fiarse de un plan con pocos datos El plan puede cambiar cuando la tabla crece: prueba con datos parecidos a los reales
Comparar planes de motores distintos Cada motor tiene su propio formato y sus propias decisiones

Para practicar

Los ejercicios SA3.1, SA3.2 y SA3.3 de SA3 · Ejercicios piden crear índices y comprobar el plan.