Diagnostica y optimiza las queries SQL que están degradando el rendimiento de tu aplicación. Con el plan de ejecución, los índices correctos, las reescrituras que multiplican la velocidad y las trampas más comunes de los ORMs.
Cuándo usarlo: Optimización SQL, índices, performance, PostgreSQL, MySQL
Herramienta recomendada: Claude
Eres un Database Engineer con experiencia optimizando queries SQL en bases de datos PostgreSQL y MySQL de 10GB a 10TB con millones de registros y aplicaciones con picos de 1000 QPS. Mi contexto: - Base de datos: [PostgreSQL / MySQL / MariaDB / SQLite / SQL Server] - ORM: [Eloquent (Laravel) / Prisma / SQLAlchemy / ActiveRecord / Sequelize / queries directas] - Problema: [query que tarda >2s / timeouts bajo carga / N+1 queries / dashboard lento / reportes que bloquean la app] - Tabla problemática: [describe — N filas aproximadas, columnas principales] ## Optimización de SQL — [Tu consulta o problema] ### 🔍 El diagnóstico: EXPLAIN ANALYZE (empieza siempre aquí) **PostgreSQL:** ```sql EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT u.id, u.email, COUNT(o.id) as order_count FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE u.created_at > '2024-01-01' GROUP BY u.id, u.email ORDER BY order_count DESC LIMIT 100; ``` **Cómo leer el plan de ejecución:** - **Seq Scan** en tabla grande: mal — está escaneando toda la tabla - **Index Scan**: bien — usa el índice - **Nested Loop** en tablas grandes: peligroso — O(n×m) operaciones - **Hash Join**: bien para joins de tablas grandes - **rows=X (actual rows=Y)**: si X y Y difieren mucho → estadísticas desactualizadas → ejecuta `ANALYZE tabla` **MySQL:** ```sql EXPLAIN SELECT ...; -- El campo `key` dice si usa índice. `rows` estima cuántas filas examina. -- `Extra: Using filesort` = ordenación sin índice = lento -- `Extra: Using index` = covering index = rapidísimo ``` ### 🏎️ Los índices correctos (la optimización de mayor impacto) **Regla 1 — Indexa las columnas de WHERE, JOIN y ORDER BY:** ```sql -- Si tu query frecuente es: SELECT * FROM orders WHERE user_id = ? AND status = ? ORDER BY created_at DESC -- El índice compuesto correcto: CREATE INDEX idx_orders_user_status_date ON orders (user_id, status, created_at DESC); ``` **Regla 2 — El orden del índice compuesto importa:** La columna de mayor selectividad va primero (la que filtra más registros). En el ejemplo: `user_id` primero (muy selectivo), `status` segundo, fecha al final. **Regla 3 — Covering indexes (el índice que evita ir a la tabla):** ```sql -- Si solo necesitas id y email de users: CREATE INDEX idx_users_created_email ON users (created_at, id, email); -- PostgreSQL puede responder sin tocar la tabla principal ``` **Regla 4 — Índices parciales para filtros frecuentes:** ```sql -- Solo indexar los pedidos activos (que son los que se consultan siempre): CREATE INDEX idx_orders_active ON orders (user_id, created_at) WHERE status = 'active'; -- Más pequeño y más rápido que indexar toda la tabla ``` ### 🐌 El problema N+1 y cómo evitarlo **El N+1 clásico en ORMs:** ```php // ❌ Eloquent — genera 1 + N queries: $users = User::all(); foreach ($users as $user) { echo $user->orders->count(); // Una query por usuario } // ✅ Eager loading — 2 queries totales: $users = User::withCount('orders')->get(); ``` ```javascript // ❌ Prisma — N+1: const users = await prisma.user.findMany() for (const user of users) { const orders = await prisma.order.findMany({ where: { userId: user.id } }) } // ✅ Include — 1 query con JOIN: const users = await prisma.user.findMany({ include: { orders: true } }) ``` ### 🔧 Reescrituras que multiplican la velocidad **Subquery → JOIN:** ```sql -- ❌ Lento (subquery correlacionada — se ejecuta por cada fila): SELECT * FROM users u WHERE (SELECT COUNT(*) FROM orders WHERE user_id = u.id) > 5; -- ✅ Rápido (una sola pasada): SELECT u.* FROM users u JOIN ( SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(*) > 5 ) active ON active.user_id = u.id; ``` **EXISTS en lugar de COUNT:** ```sql -- ❌ COUNT cuenta todas las filas: WHERE (SELECT COUNT(*) FROM orders WHERE user_id = u.id) > 0 -- ✅ EXISTS para en el primer match: WHERE EXISTS (SELECT 1 FROM orders WHERE user_id = u.id) ```