Día 7 – Índices y optimización: mejorar el rendimiento de tus consultas

Página de inicio

Día 7 – Índices y optimización: mejorar el rendimiento de tus consultas

¡Llegamos al último día del curso! 🎉 Hoy aprenderás a hacer que tus consultas SQL sean rápidas y eficientes.

Con pocas filas (10, 100, 1000) cualquier consulta es rápida. Pero cuando tienes millones de registros, una consulta mal optimizada puede tardar minutos u horas. Una consulta bien optimizada con índices adecuados puede tardar milisegundos.

Los índices son como el índice de un libro: en lugar de leer todo el libro (tabla completa) para encontrar algo, vas directo a la página correcta.


Escenario del día

Imagina que tu base de datos ha crecido:

  • 1 millón de usuarios
  • 10 millones de pedidos
  • 50 millones de detalles de pedidos

De repente:

  • Buscar un usuario por email tarda 30 segundos
  • Listar pedidos de un usuario tarda 2 minutos
  • Generar un informe mensual se queda colgado

Sin índices adecuados, PostgreSQL tiene que leer toda la tabla para cada consulta. Con índices, puede ir directo a los datos que necesita.


1. ¿Qué es un índice?

Un índice es una estructura de datos adicional que acelera las búsquedas en una tabla.

Analogía: Biblioteca

Sin índice: Para encontrar un libro sobre «SQL», tienes que revisar libro por libro toda la biblioteca.

Con índice: Miras el catálogo (índice), que te dice exactamente en qué estante está «SQL».

Cómo funcionan los índices

  • PostgreSQL crea una estructura en árbol (B-tree) que organiza los datos
  • Cuando buscas algo, recorre el árbol (muy rápido) en lugar de escanear toda la tabla
  • Trade-off: Los índices aceleran las lecturas pero ralentizan las escrituras (INSERT, UPDATE, DELETE) porque hay que actualizar también el índice

2. Ver índices existentes

Por defecto, PostgreSQL crea automáticamente índices en:

  • Primary Keys (id)
  • Unique constraints (email)
-- Ver todos los índices de una tabla
SELECT 
    indexname,
    indexdef
FROM pg_indexes
WHERE tablename = 'usuarios'
ORDER BY indexname;

Resultado:

indexnameindexdef
usuarios_pkeyCREATE UNIQUE INDEX usuarios_pkey ON public.usuarios USING btree (id)
usuarios_email_keyCREATE UNIQUE INDEX usuarios_email_key ON public.usuarios USING btree (email)

3. Crear índices

Sintaxis básica

CREATE INDEX nombre_indice ON tabla (columna);

Ejemplo: Índice en columna frecuentemente buscada

-- Crear índice en país (buscamos usuarios por país a menudo)
CREATE INDEX idx_usuarios_pais ON usuarios (pais);

Verificar que se creó:

SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'usuarios'
AND indexname = 'idx_usuarios_pais';

Índice en múltiples columnas (compuesto)

Si frecuentemente filtras por dos columnas a la vez:

-- Índice compuesto en país y fecha de registro
CREATE INDEX idx_usuarios_pais_fecha ON usuarios (pais, fecha_registro);

Importante: El orden importa. Este índice es útil para:

  • WHERE pais = 'España' AND fecha_registro > '2024-01-01'
  • WHERE pais = 'España' (usa solo la primera columna)
  • WHERE fecha_registro > '2024-01-01' (no usa el índice porque pais no está)

Índice UNIQUE

-- Evitar emails duplicados con índice
CREATE UNIQUE INDEX idx_usuarios_email_unique ON usuarios (email);

PostgreSQL ya crea esto automáticamente si defines la columna como UNIQUE.

Índice parcial (condicional)

Útil para indexar solo un subconjunto de filas:

-- Índice solo para productos en stock
CREATE INDEX idx_productos_stock ON productos (nombre)
WHERE stock > 0;

Esto es más eficiente que indexar toda la tabla si solo te interesan productos disponibles.

Índice en expresiones

-- Índice para búsquedas case-insensitive
CREATE INDEX idx_usuarios_nombre_lower ON usuarios (LOWER(nombre));

Ahora puedes buscar sin importar mayúsculas:

SELECT * FROM usuarios WHERE LOWER(nombre) = 'ana garcia';

4. EXPLAIN – Analizar consultas

EXPLAIN te muestra cómo PostgreSQL ejecuta una consulta.

Sintaxis básica

EXPLAIN SELECT * FROM usuarios WHERE pais = 'España';

Resultado:

Seq Scan on usuarios  (cost=0.00..1.12 rows=2 width=328)
  Filter: ((pais)::text = 'España'::text)

Interpretación:

  • Seq Scan = Sequential Scan = Escaneo secuencial (lee toda la tabla)
  • cost: Estimación de costo (mayor = más lento)
  • rows: Número estimado de filas que devolverá
  • Filter: Condición aplicada

EXPLAIN ANALYZE – Con ejecución real

EXPLAIN ANALYZE SELECT * FROM usuarios WHERE pais = 'España';

Resultado:

Seq Scan on usuarios  (cost=0.00..1.12 rows=2 width=328) (actual time=0.012..0.018 rows=4 loops=1)
  Filter: ((pais)::text = 'España'::text)
  Rows Removed by Filter: 6
Planning Time: 0.091 ms
Execution Time: 0.034 ms

Interpretación adicional:

  • actual time: Tiempo real de ejecución
  • rows=4: Devolvió 4 filas realmente
  • Execution Time: Tiempo total (0.034 ms = muy rápido)

Comparar sin índice vs con índice

-- Sin índice: Seq Scan
EXPLAIN ANALYZE SELECT * FROM usuarios WHERE pais = 'España';

-- Crear índice
CREATE INDEX idx_usuarios_pais ON usuarios (pais);

-- Con índice: Index Scan o Bitmap Index Scan
EXPLAIN ANALYZE SELECT * FROM usuarios WHERE pais = 'España';

Con índice verás algo como:

Bitmap Heap Scan on usuarios  (cost=4.19..11.32 rows=4 width=328)
  Recheck Cond: ((pais)::text = 'España'::text)
  ->  Bitmap Index Scan on idx_usuarios_pais  (cost=0.00..4.19 rows=4 width=0)
        Index Cond: ((pais)::text = 'España'::text)

Index Scan = Usa el índice (mucho más rápido en tablas grandes).


5. Tipos de escaneo (Scan types)

Sequential Scan (Seq Scan)

  • Lee toda la tabla fila por fila
  • Lento en tablas grandes
  • Rápido si devuelves muchas filas (>20% de la tabla)

Index Scan

  • Usa el índice para ir directo a las filas
  • Muy rápido para pocas filas
  • Accede a filas de forma aleatoria (puede ser lento si devuelve muchas)

Bitmap Index Scan

  • PostgreSQL usa el índice para crear un «mapa de bits» de filas
  • Luego lee las filas en orden secuencial (más eficiente)
  • Bueno para consultas que devuelven muchas filas

Index Only Scan

  • El índice contiene todas las columnas necesarias
  • No necesita leer la tabla en absoluto
  • El más rápido
-- Ejemplo: El índice tiene 'email', y solo pedimos 'email'
CREATE INDEX idx_usuarios_email ON usuarios (email);

-- Index Only Scan (muy rápido)
EXPLAIN ANALYZE SELECT email FROM usuarios WHERE email LIKE 'ana%';

6. Cuándo usar índices

✅ SÍ crear índice en:

  • Primary Keys (automático)
  • Foreign Keys (muy recomendado)
  • Columnas en WHERE frecuentes (país, categoría, estado)
  • Columnas en JOIN (usuario_id, producto_id)
  • Columnas en ORDER BY (fecha, nombre)
  • Columnas UNIQUE (email, username)

❌ NO crear índice en:

  • Tablas muy pequeñas (<1000 filas) → Seq Scan es más rápido
  • Columnas con poca variación (género con solo M/F → 50% de las filas)
  • Columnas que cambias constantemente (contador de visitas)
  • Tablas con muchas escrituras (logs, métricas en tiempo real)

Regla general

  • Si una columna aparece frecuentemente en WHERE, JOIN u ORDER BY → Considera un índice
  • Si una tabla tiene muchas lecturas y pocas escrituras → Más índices
  • Si una tabla tiene muchas escrituras → Menos índices

7. Índices en nuestras tablas de ejemplo

Índices recomendados para la base de datos del curso

-- Foreign Keys (muy importante)
CREATE INDEX idx_pedidos_usuario_id ON pedidos (usuario_id);
CREATE INDEX idx_detalle_pedidos_pedido_id ON detalle_pedidos (pedido_id);
CREATE INDEX idx_detalle_pedidos_producto_id ON detalle_pedidos (producto_id);

-- Búsquedas frecuentes
CREATE INDEX idx_usuarios_pais ON usuarios (pais);
CREATE INDEX idx_productos_categoria ON productos (categoria);
CREATE INDEX idx_pedidos_fecha ON pedidos (fecha);

-- Índices compuestos útiles
CREATE INDEX idx_productos_categoria_precio ON productos (categoria, precio);
CREATE INDEX idx_usuarios_pais_fecha ON usuarios (pais, fecha_registro);

Verificar mejora con EXPLAIN

-- ANTES del índice
EXPLAIN ANALYZE 
SELECT * FROM pedidos WHERE usuario_id = 1;
-- Seq Scan: 0.05 ms

-- Crear índice
CREATE INDEX idx_pedidos_usuario_id ON pedidos (usuario_id);

-- DESPUÉS del índice
EXPLAIN ANALYZE 
SELECT * FROM pedidos WHERE usuario_id = 1;
-- Index Scan: 0.02 ms (2x más rápido)

En tablas grandes, la diferencia puede ser de segundos vs milisegundos.


8. Mantenimiento de índices

Ver tamaño de índices

-- Tamaño de todos los índices
SELECT 
    schemaname,
    tablename,
    indexname,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC;

Índices no utilizados

-- Índices que nunca se usan (candidatos para eliminar)
SELECT 
    schemaname,
    tablename,
    indexname,
    idx_scan AS number_of_scans,
    idx_tup_read AS tuples_read,
    idx_tup_fetch AS tuples_fetched
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND indexrelname NOT LIKE 'pg_toast%'
ORDER BY pg_relation_size(indexrelid) DESC;

Eliminar índices

-- Eliminar índice no usado
DROP INDEX IF EXISTS idx_usuarios_fecha_registro;

REINDEX – Reconstruir índices

Con el tiempo, los índices pueden fragmentarse. Reconstruirlos puede mejorar el rendimiento:

-- Reconstruir un índice específico
REINDEX INDEX idx_usuarios_pais;

-- Reconstruir todos los índices de una tabla
REINDEX TABLE usuarios;

-- Reconstruir todos los índices de la base de datos (cuidado, puede tardar)
REINDEX DATABASE aprendiendo_sql;

9. Otras técnicas de optimización

VACUUM – Limpiar espacio

PostgreSQL no borra físicamente las filas eliminadas inmediatamente. VACUUM las limpia:

-- Analizar y limpiar una tabla
VACUUM ANALYZE usuarios;

-- Limpiar toda la base de datos
VACUUM;

VACUUM FULL (más agresivo, bloquea la tabla):

VACUUM FULL usuarios;

ANALYZE – Actualizar estadísticas

PostgreSQL usa estadísticas para decidir qué plan de ejecución usar. Actualízalas después de grandes cambios:

-- Actualizar estadísticas de una tabla
ANALYZE usuarios;

-- Toda la base de datos
ANALYZE;

Configuración de PostgreSQL

Algunos parámetros importantes (editables en postgresql.conf):

-- Ver configuración actual
SHOW shared_buffers;      -- Memoria para cachear datos
SHOW work_mem;            -- Memoria para operaciones (ORDER BY, JOIN)
SHOW effective_cache_size;  -- Estimación de memoria disponible total

Ajustes típicos para mejor rendimiento (requieren reinicio):

shared_buffers = 256MB         # 25% de la RAM
work_mem = 16MB                # Para operaciones grandes
effective_cache_size = 1GB     # 50-75% de la RAM

10. Optimización de consultas comunes

Problema: Consulta lenta con JOINs

-- Lenta: Sin índices en foreign keys
EXPLAIN ANALYZE
SELECT 
    u.nombre,
    COUNT(p.id) AS total_pedidos,
    SUM(p.total) AS gasto_total
FROM usuarios u
LEFT JOIN pedidos p ON u.id = p.usuario_id
GROUP BY u.id, u.nombre;

Solución:

-- Crear índice en foreign key
CREATE INDEX idx_pedidos_usuario_id ON pedidos (usuario_id);

-- Ahora será más rápido

Problema: ORDER BY lento

-- Lenta: Sin índice en fecha
EXPLAIN ANALYZE
SELECT * FROM pedidos ORDER BY fecha DESC LIMIT 10;

Solución:

-- Índice en columna de ORDER BY
CREATE INDEX idx_pedidos_fecha_desc ON pedidos (fecha DESC);

El DESC en el índice hace que ORDER BY DESC sea aún más rápido.

Problema: Búsquedas con LIKE

-- Muy lenta: LIKE no usa índices normales
SELECT * FROM usuarios WHERE nombre LIKE '%Garcia%';

Soluciones:

  1. Usar índice trigram (para búsquedas de texto):
-- Habilitar extensión
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- Crear índice GIN
CREATE INDEX idx_usuarios_nombre_trgm ON usuarios USING gin (nombre gin_trgm_ops);

-- Ahora LIKE será más rápido
SELECT * FROM usuarios WHERE nombre LIKE '%Garcia%';
  1. Usar FULL TEXT SEARCH (para búsquedas más complejas):
-- Crear columna de búsqueda
ALTER TABLE usuarios ADD COLUMN nombre_search tsvector;

-- Actualizar con datos
UPDATE usuarios SET nombre_search = to_tsvector('spanish', nombre);

-- Crear índice GIN
CREATE INDEX idx_usuarios_nombre_search ON usuarios USING gin (nombre_search);

-- Buscar
SELECT * FROM usuarios WHERE nombre_search @@ to_tsquery('spanish', 'Garcia');

Problema: Subconsultas lentas

-- Lenta: Subconsulta correlacionada
SELECT nombre, (
    SELECT COUNT(*) FROM pedidos WHERE usuario_id = usuarios.id
) AS total_pedidos
FROM usuarios;

Solución: JOIN en lugar de subconsulta

-- Más rápida: JOIN
SELECT 
    u.nombre,
    COUNT(p.id) AS total_pedidos
FROM usuarios u
LEFT JOIN pedidos p ON u.id = p.usuario_id
GROUP BY u.id, u.nombre;

Problema: IN con muchos valores

-- Puede ser lenta
SELECT * FROM productos WHERE id IN (1, 2, 3, ..., 1000);

Solución: JOIN con tabla temporal

-- Crear tabla temporal con IDs
CREATE TEMP TABLE ids_buscar (id INTEGER);
INSERT INTO ids_buscar VALUES (1), (2), (3), ..., (1000);

-- JOIN en lugar de IN
SELECT p.*
FROM productos p
INNER JOIN ids_buscar i ON p.id = i.id;

11. Casos prácticos de optimización

Escenario 1: Dashboard de ventas lento

Problema: Consulta que genera el dashboard tarda 10 segundos.

-- Consulta original (lenta)
SELECT 
    pr.categoria,
    COUNT(dp.id) AS ventas,
    SUM(dp.cantidad * dp.precio_unitario) AS ingresos
FROM productos pr
INNER JOIN detalle_pedidos dp ON pr.id = dp.producto_id
INNER JOIN pedidos p ON dp.pedido_id = p.id
WHERE p.fecha BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY pr.categoria;

EXPLAIN ANALYZE muestra:

  • Seq Scan en detalle_pedidos (10 millones de filas)
  • Sin índices en foreign keys

Solución:

-- Índices en foreign keys
CREATE INDEX idx_detalle_pedidos_pedido_id ON detalle_pedidos (pedido_id);
CREATE INDEX idx_detalle_pedidos_producto_id ON detalle_pedidos (producto_id);
CREATE INDEX idx_pedidos_fecha ON pedidos (fecha);

-- Ahora tarda 0.5 segundos (20x más rápido)

Escenario 2: Búsqueda de usuarios lenta

Problema: Buscar usuarios por email tarda mucho.

-- Sin índice: 5 segundos en 1M de usuarios
SELECT * FROM usuarios WHERE email = 'juan.martinez@email.com';

Solución:

-- Email ya debería tener UNIQUE (índice automático)
-- Si no lo tiene:
CREATE UNIQUE INDEX idx_usuarios_email ON usuarios (email);

-- Ahora: 0.001 segundos

Escenario 3: Informe mensual muy lento

Problema: Generar informe de ventas mensuales tarda 2 minutos.

-- Lenta: Múltiples JOINs sin optimizar
SELECT 
    DATE_TRUNC('month', p.fecha) AS mes,
    u.pais,
    pr.categoria,
    SUM(dp.cantidad) AS unidades,
    SUM(dp.cantidad * dp.precio_unitario) AS ingresos
FROM pedidos p
JOIN usuarios u ON p.usuario_id = u.id
JOIN detalle_pedidos dp ON p.id = dp.pedido_id
JOIN productos pr ON dp.producto_id = pr.id
WHERE p.fecha >= '2024-01-01'
GROUP BY mes, u.pais, pr.categoria
ORDER BY mes, ingresos DESC;

Solución:

-- 1. Índices necesarios
CREATE INDEX idx_pedidos_fecha ON pedidos (fecha);
CREATE INDEX idx_pedidos_usuario_id ON pedidos (usuario_id);
CREATE INDEX idx_detalle_pedidos_pedido_id ON detalle_pedidos (pedido_id);
CREATE INDEX idx_detalle_pedidos_producto_id ON detalle_pedidos (producto_id);
CREATE INDEX idx_usuarios_pais ON usuarios (pais);
CREATE INDEX idx_productos_categoria ON productos (categoria);

-- 2. VACUUM y ANALYZE
VACUUM ANALYZE pedidos;
VACUUM ANALYZE detalle_pedidos;
VACUUM ANALYZE usuarios;
VACUUM ANALYZE productos;

-- Ahora: 3 segundos (40x más rápido)

12. Herramientas de monitoreo

Ver consultas lentas

-- Habilitar logging de consultas lentas (requiere permisos)
-- En postgresql.conf o con ALTER SYSTEM:
ALTER SYSTEM SET log_min_duration_statement = 1000;  -- Log queries > 1 segundo
SELECT pg_reload_conf();

-- Ver estadísticas de consultas
SELECT 
    query,
    calls,
    total_time,
    mean_time,
    max_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;

Extensión pg_stat_statements

-- Habilitar extensión (si no está)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- Ver consultas más lentas
SELECT 
    substring(query, 1, 50) AS query_short,
    calls,
    ROUND(total_exec_time::numeric, 2) AS total_time_ms,
    ROUND(mean_exec_time::numeric, 2) AS avg_time_ms,
    ROUND((100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 2) AS percentage
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

13. Checklist de optimización

Cuando una consulta es lenta, sigue estos pasos:

Paso 1: Identifica el problema

EXPLAIN ANALYZE tu_consulta_lenta;

Busca:

  • Seq Scan en tablas grandes → Necesitas índice
  • Alto cost → Consulta ineficiente
  • Rows muy diferentes entre estimado y real → Necesitas ANALYZE

Paso 2: Crea índices necesarios

-- Foreign keys (casi siempre)
CREATE INDEX idx_tabla_fk ON tabla (columna_fk);

-- Columnas en WHERE
CREATE INDEX idx_tabla_where ON tabla (columna_where);

-- Columnas en JOIN
CREATE INDEX idx_tabla_join ON tabla (columna_join);

Paso 3: Actualiza estadísticas

ANALYZE tabla;

Paso 4: Reescribe la consulta si es necesario

  • Evita SELECT *
  • Usa JOINs en lugar de subconsultas correlacionadas
  • Limita resultados con LIMIT cuando sea posible
  • Considera materializar resultados en tabla temporal para consultas complejas

Paso 5: Verifica la mejora

EXPLAIN ANALYZE tu_consulta_optimizada;

Compara el tiempo de ejecución antes y después.


14. Errores comunes

Error 1: Crear índices en todo

-- ❌ MAL: Demasiados índices
CREATE INDEX idx1 ON productos (nombre);
CREATE INDEX idx2 ON productos (precio);
CREATE INDEX idx3 ON productos (stock);
CREATE INDEX idx4 ON productos (categoria);
-- ... 10 índices más

Problema: Cada índice ralentiza INSERT, UPDATE, DELETE. Solo crea índices que realmente necesitas.

Error 2: Índice en columna de baja cardinalidad

-- ❌ Poco útil: Solo hay 2 valores posibles
CREATE INDEX idx_productos_disponible ON productos (disponible);  -- TRUE/FALSE

Regla: Si una columna tiene menos del 10% de valores únicos, probablemente no vale la pena indexarla.

Error 3: No actualizar estadísticas

-- Insertas 1 millón de filas
INSERT INTO productos SELECT ...;

-- Consultas lentas porque PostgreSQL usa estadísticas viejas
-- ✅ Solución:
ANALYZE productos;

Error 4: Olvidar índices en foreign keys

-- Tablas relacionadas sin índice en FK = JOINs lentos
CREATE TABLE pedidos (
    usuario_id INTEGER REFERENCES usuarios(id)  -- Sin índice!
);

-- ✅ SIEMPRE crea índice en foreign keys:
CREATE INDEX idx_pedidos_usuario_id ON pedidos (usuario_id);

15. Ejercicios prácticos

  1. Analiza la consulta que lista usuarios de España con EXPLAIN ANALYZE
  2. Crea índices en todas las foreign keys de la base de datos
  3. Compara rendimiento antes y después de crear un índice en productos.categoria
  4. Identifica índices no usados en tu base de datos
  5. Optimiza una consulta que une las 4 tablas (usuarios, pedidos, detalle_pedidos, productos)
  6. Crea un índice compuesto en (categoria, precio) y verifica su uso
  7. Ejecuta VACUUM ANALYZE en todas las tablas
  8. Mide el tamaño de todos los índices de tu base de datos
  9. Reescribe una consulta lenta usando CTE en lugar de subconsultas
  10. Crea un índice parcial para productos en stock > 0

16. Soluciones

Solución Ejercicio 1

EXPLAIN ANALYZE
SELECT * FROM usuarios WHERE pais = 'España';

Observa si usa Seq Scan o Index Scan.

Solución Ejercicio 2

-- Índices en foreign keys
CREATE INDEX IF NOT EXISTS idx_pedidos_usuario_id ON pedidos (usuario_id);
CREATE INDEX IF NOT EXISTS idx_detalle_pedidos_pedido_id ON detalle_pedidos (pedido_id);
CREATE INDEX IF NOT EXISTS idx_detalle_pedidos_producto_id ON detalle_pedidos (producto_id);

Solución Ejercicio 3

-- Antes del índice
EXPLAIN ANALYZE
SELECT * FROM productos WHERE categoria = 'Electronica';
-- Nota el tiempo de ejecución

-- Crear índice
CREATE INDEX idx_productos_categoria ON productos (categoria);

-- Después del índice
EXPLAIN ANALYZE
SELECT * FROM productos WHERE categoria = 'Electronica';
-- Compara el tiempo

Solución Ejercicio 4

-- Índices no usados
SELECT 
    schemaname,
    tablename,
    indexname,
    idx_scan,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;

Solución Ejercicio 5

-- Consulta a optimizar
EXPLAIN ANALYZE
SELECT 
    u.nombre,
    p.fecha,
    pr.nombre AS producto,
    dp.cantidad
FROM usuarios u
JOIN pedidos p ON u.id = p.usuario_id
JOIN detalle_pedidos dp ON p.id = dp.pedido_id
JOIN productos pr ON dp.producto_id = pr.id
WHERE u.pais = 'España';

-- Índices necesarios (si no existen)
CREATE INDEX IF NOT EXISTS idx_usuarios_pais ON usuarios (pais);
CREATE INDEX IF NOT EXISTS idx_pedidos_usuario_id ON pedidos (usuario_id);
CREATE INDEX IF NOT EXISTS idx_detalle_pedidos_pedido_id ON detalle_pedidos (pedido_id);
CREATE INDEX IF NOT EXISTS idx_detalle_pedidos_producto_id ON detalle_pedidos (producto_id);

-- Verificar mejora
EXPLAIN ANALYZE
SELECT 
    u.nombre,
    p.fecha,
    pr.nombre AS producto,
    dp.cantidad
FROM usuarios u
JOIN pedidos p ON u.id = p.usuario_id
JOIN detalle_pedidos dp ON p.id = dp.pedido_id
JOIN productos pr ON dp.producto_id = pr.id
WHERE u.pais = 'España';

Solución Ejercicio 6

-- Crear índice compuesto
CREATE INDEX idx_productos_cat_precio ON productos (categoria, precio);

-- Probar con consulta que usa ambas columnas
EXPLAIN ANALYZE
SELECT nombre, precio
FROM productos
WHERE categoria = 'Electronica' AND precio > 500;
-- Debería usar el índice compuesto

Solución Ejercicio 7

-- Limpiar y actualizar estadísticas
VACUUM ANALYZE usuarios;
VACUUM ANALYZE productos;
VACUUM ANALYZE pedidos;
VACUUM ANALYZE detalle_pedidos;

Solución Ejercicio 8

-- Tamaño de índices por tabla
SELECT 
    schemaname,
    tablename,
    indexname,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;

-- Tamaño total de índices
SELECT 
    pg_size_pretty(SUM(pg_relation_size(indexrelid))) AS total_index_size
FROM pg_stat_user_indexes
WHERE schemaname = 'public';

Solución Ejercicio 9

-- Subconsulta lenta
SELECT 
    nombre,
    (SELECT COUNT(*) FROM pedidos WHERE usuario_id = usuarios.id) AS pedidos
FROM usuarios;

-- Reescrita con CTE (más rápida)
WITH conteo_pedidos AS (
    SELECT 
        usuario_id,
        COUNT(*) AS total_pedidos
    FROM pedidos
    GROUP BY usuario_id
)
SELECT 
    u.nombre,
    COALESCE(cp.total_pedidos, 0) AS pedidos
FROM usuarios u
LEFT JOIN conteo_pedidos cp ON u.id = cp.usuario_id;

Solución Ejercicio 10

-- Índice parcial solo para productos en stock
CREATE INDEX idx_productos_stock_disponible 
ON productos (nombre, categoria)
WHERE stock > 0;

-- Probar que se usa
EXPLAIN ANALYZE
SELECT nombre, categoria, stock
FROM productos
WHERE stock > 0 AND categoria = 'Electronica';

Resumen del Día 7 (y del curso completo)

¡Felicidades! 🎉 Has completado el Mini Tutorial de SQL en 7 Días.

Día 7 – Has aprendido:

Qué son los índices y cómo funcionan
Crear índices (simples, compuestos, parciales, de expresiones)
EXPLAIN y EXPLAIN ANALYZE → Analizar planes de ejecución
Tipos de escaneo (Seq Scan, Index Scan, Bitmap Scan, Index Only Scan)
Cuándo usar índices y cuándo no
Mantenimiento (VACUUM, ANALYZE, REINDEX)
Optimización de consultas comunes
Monitoreo de rendimiento


🎓 Lo que has aprendido en 7 días

Día 1 – Fundamentos

SELECT, FROM, WHERE, ORDER BY, LIMIT

Día 2 – Filtros avanzados

AND, OR, IN, BETWEEN, LIKE, NULL, DISTINCT

Día 3 – Agregaciones

COUNT, SUM, AVG, MIN, MAX, GROUP BY, HAVING

Día 4 – JOINs

INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN

Día 5 – Subconsultas y CTEs

Subconsultas, CTEs, Window Functions, Consultas recursivas

Día 6 – Modificación de datos

INSERT, UPDATE, DELETE, Transacciones, RETURNING

Día 7 – Optimización

Índices, EXPLAIN, Optimización de consultas, Mantenimiento


🚀 Siguientes pasos

Ahora que dominas SQL, puedes:

  1. Practicar con datos reales → Importa datasets de Kaggle o datos públicos
  2. Crear proyectos → API REST con base de datos, dashboard de analytics
  3. Aprender SQL avanzado → Particionamiento, triggers, stored procedures
  4. Explorar otros SGBD → MySQL, SQL Server, SQLite (sintaxis similar)
  5. Integrar con código → Python (SQLAlchemy), Node.js (Sequelize), etc.

Recursos adicionales


¡Felicidades!

Has completado el Mini Tutorial de SQL en 7 Días

Ahora tienes las habilidades fundamentales para:

  • ✅ Consultar datos con confianza
  • ✅ Generar informes y análisis
  • ✅ Modificar datos de forma segura
  • ✅ Optimizar el rendimiento de tu base de datos

SQL es una habilidad que te acompañará toda tu carrera profesional. Prácticamente cualquier aplicación moderna usa bases de datos, y SQL es el lenguaje universal para trabajar con ellas.

¡Gracias por completar este curso!


Página de inicio

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.

ACEPTAR
Aviso de cookies