Consultas lentas
En resumen: Una consulta sigue lenta a pesar del índice cuando la columna está dentro de una función o de un LIKE, cuando pide todas las columnas, cuando un OR comprueba también una columna sin índice o cuando cuenta todo para responder «¿hay alguno?».
Problema
El índice existe y aun así es lento
El informe anual cuenta las visitas de 2026. Hay un índice sobre seen_at. Ejecuta la consulta y mira el tiempo:
Y el plan:
SCAN visits USING COVERING INDEX. La base de datos lee el índice porque es más pequeño que la tabla, pero lo lee entero: las 300 000 entradas. No hay búsqueda.
La idea
La columna tiene que estar sola
El índice está ordenado por seen_at. No está ordenado por substr(seen_at, 1, 4). Para comprobar la condición, la base de datos tiene que calcular la función para cada entrada, y para eso tiene que recorrerlas todas.
La misma condición se puede escribir con límites, de modo que la columna quede sola en un lado:
El resultado es el mismo, y el plan es SEARCH. Compara los tiempos de las dos consultas.
Con la aritmética pasa igual. customer_id + 0 = 1234 da lo mismo que customer_id = 1234, pero el índice no ayuda:
La regla: no toques la columna en WHERE. La aritmética pasa al otro lado (price > 10 / 1.2, no price * 1.2 > 10), y una función sobre una fecha se convierte en un intervalo.
Pruébalo
Octubre
En el campo está la consulta del informe anual. El informe mensual quiere las visitas de octubre de 2026. Escribe la condición de modo que use el índice sobre seen_at.
Solución
SELECT COUNT(*) FROM visits
WHERE seen_at >= '2026-10-01' AND seen_at < '2026-11-01';WHERE seen_at >= '2026-10-01' AND seen_at < '2026-11-01'. 9 988 visitas. La comprobación mira también el plan: con substr(seen_at, 1, 7) = '2026-10' o LIKE '2026-10%' el resultado es correcto, pero el plan es SCAN y la respuesta no pasa. Con los límites el plan es SEARCH … (seen_at>? AND seen_at<?). El límite superior es «antes del 1 de noviembre», no «hasta el 31 de octubre»: así no piensas cuántos días tiene el mes, y un valor con hora (2026-10-31 18:30) también entra.
Míralo
LIKE y el índice
Parece que LIKE '2026-10%' debería usar el índice: se conoce el principio, como en una guía telefónica. Mira el plan:
En SQLite es SCAN. La razón: LIKE en SQLite no distingue entre mayúsculas y minúsculas, mientras que el índice está ordenado distinguiéndolas. SQLite usa un índice para LIKE 'inicio%' solo bajo ciertas condiciones, por ejemplo si la columna o el índice son COLLATE NOCASE. Para las fechas, el camino seguro es el intervalo de la tarjeta anterior.
Un patrón que empieza por % no puede usar un índice normal en ninguna base de datos. El principio es desconocido, así que la búsqueda no tiene por dónde empezar:
Más precisamente, para curiosos
La excepción a la regla de las funciones es un índice sobre una expresión: CREATE INDEX idx_visits_year ON visits(substr(seen_at, 1, 4)) vuelve a convertir WHERE substr(seen_at, 1, 4) = '2026' en un SEARCH. El índice solo ayuda a las consultas con la misma expresión.
En SQLite, GLOB '2026-10*' distingue entre mayúsculas y minúsculas y sí usa el índice (el plan es SEARCH). Lo mismo hace PRAGMA case_sensitive_like = ON, pero cambia el comportamiento de todos los LIKE de la conexión.
En PostgreSQL un índice normal ayuda con LIKE 'inicio%' solo con la colación C o si se creó con text_pattern_ops. Para buscar en medio de un texto hay tipos de índice aparte (la extensión pg_trgm). La búsqueda de texto completo está en el módulo sobre los tipos de bases de datos.
La idea
Solo las columnas que necesitas
El programa muestra en qué fechas visitaron los clientes del 1000 al 1100. Dos consultas con las mismas filas. Compara los tiempos:
La segunda es más rápida, porque customer_id y seen_at están en el índice (customer_id, seen_at), y este las cubre: la base de datos no abre la tabla. SELECT * quiere también product_id, así que para cada una de las 6 060 entradas la base de datos va además a la tabla. Pon en SELECT solo las columnas que el programa usa de verdad. Así también viaja menos información hacia el programa.
La idea
OR e IN
OR sobre la misma columna es lo mismo que IN. SQLite los ejecuta igual:
La trampa es OR entre columnas distintas. No hay índice sobre product_id. Las filas con product_id = 4 pueden estar en cualquier lugar de la tabla, así que la base de datos lo lee todo, y el índice del cliente no ayuda:
Si las dos columnas tienen índice, SQLite busca en cada una por separado y une los resultados (el plan dice MULTI-INDEX OR). Si a una mitad le falta el índice, sigue siendo SCAN.
La idea
«¿Hay alguno?», no «¿cuántos?»
La página del producto 13 muestra «Popular» si alguien lo ha visto. Contar encuentra las 12 000 visitas:
La página solo necesita «sí» o «no». EXISTS se detiene en la primera fila que encuentra:
No hay índice sobre product_id, pero EXISTS sigue siendo rápido, porque la primera visita del producto está cerca del principio de la tabla. Si no existe esa fila, EXISTS también lo leerá todo. Así que también aquí ayuda un índice. Otra forma es SELECT 1 FROM … WHERE … LIMIT 1: un resultado vacío significa «no».
Ejercicio
Dos tareas
1. En el campo está la consulta antigua de 2026. El cliente 1234 pregunta cuántas veces miró productos en 2025. Escríbela de modo que el plan sea SEARCH tanto por el cliente como por la fecha.
Las visitas del cliente 1234 en 2025, con SEARCH en las dos columnas:
Solución
SELECT COUNT(*) FROM visits
WHERE customer_id = 1234
AND seen_at >= '2025-01-01' AND seen_at < '2026-01-01';WHERE customer_id = 1234 AND seen_at >= '2025-01-01' AND seen_at < '2026-01-01'. 33 visitas. La comprobación no deja pasar un SCAN. Si la búsqueda usa las dos columnas lo ves con EXPLAIN QUERY PLAN. Las dos correcciones son la columna sin + 0 y el intervalo en lugar de substr. Con EXPLAIN QUERY PLAN delante de la consulta dice SEARCH visits USING COVERING INDEX idx_visits_customer_seen (customer_id=? AND seen_at>? AND seen_at<?).
2. El botón «Ver otra vez» se muestra si el cliente 4321 ha visto algún producto a partir del 1 de octubre de 2026. Devuelve 1 o 0 en una columna seen.
¿Tiene el cliente 4321 visitas a partir del 2026-10-01?
Solución
SELECT EXISTS (
SELECT 1 FROM visits
WHERE customer_id = 4321 AND seen_at >= '2026-10-01'
) AS seen;SELECT EXISTS (SELECT 1 FROM visits WHERE customer_id = 4321 AND seen_at >= '2026-10-01') AS seen; Sí: 1. La comprobación compara solo el valor, así que COUNT(*) > 0 también pasa, pero cuenta todas las visitas cuando solo hace falta la primera.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Subconsultas · Índices · EXPLAIN: el plan de la consulta · Índices de varias columnas
- Ejercicios sobre el tema: Índices y velocidad