Inicio / TypeScript / Conceptos de Backend / SQL: Consultas y Optimización

SQL: Consultas y Optimización

JOINs, subconsultas, CTEs, window functions y estrategias de optimización.

🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

SQL: Consultas y Optimización

SQL (Structured Query Language) es el lenguaje estándar para interactuar con bases de datos relacionales. Dominar sus construcciones avanzadas es esencial para escribir código backend eficiente.


SELECT: Fundamentos

-- Proyección y filtrado
SELECT id, name, email
FROM   users
WHERE  role = 'admin'
  AND  created_at >= '2024-01-01'
  AND  deleted_at IS NULL
ORDER  BY created_at DESC
LIMIT  10 OFFSET 20;  -- Paginación: página 3, 10 por página

-- Alias y expresiones
SELECT
    id,
    UPPER(name)                              AS name_upper,
    DATE_TRUNC('month', created_at)          AS month,
    EXTRACT(YEAR FROM created_at)            AS year,
    CASE
        WHEN total_cents > 10000 THEN 'VIP'
        WHEN total_cents > 5000  THEN 'Premium'
        ELSE                          'Standard'
    END AS customer_tier
FROM users u
JOIN orders o ON o.user_id = u.id;

JOINs

-- INNER JOIN: solo coincidencias en ambas tablas
SELECT u.name, o.id AS order_id, o.total_cents
FROM   users  u
JOIN   orders o ON o.user_id = u.id
WHERE  o.status = 'shipped';

-- LEFT JOIN: todos los usuarios, hayan pedido o no
SELECT u.name, COUNT(o.id) AS order_count
FROM   users  u
LEFT JOIN orders o ON o.user_id = u.id AND o.deleted_at IS NULL
GROUP BY u.id, u.name
ORDER BY order_count DESC;

-- JOIN múltiple con tabla intermedia N:M
SELECT
    o.id AS order_id,
    u.name AS customer,
    p.name AS product,
    oi.quantity,
    oi.unit_price
FROM orders      o
JOIN users       u  ON u.id  = o.user_id
JOIN order_items oi ON oi.order_id = o.id
JOIN products    p  ON p.id  = oi.product_id
WHERE o.id = 42;

-- SELF JOIN: empleado y su manager
SELECT
    e.name AS employee,
    m.name AS manager
FROM   employees e
LEFT JOIN employees m ON m.id = e.manager_id;

Funciones de agregación y GROUP BY

-- Estadísticas por usuario
SELECT
    u.id,
    u.name,
    COUNT(o.id)              AS total_orders,
    SUM(o.total_cents)       AS lifetime_value,
    AVG(o.total_cents)       AS avg_order,
    MAX(o.total_cents)       AS max_order,
    MIN(o.created_at)        AS first_order
FROM users  u
JOIN orders o ON o.user_id = u.id
WHERE o.status != 'cancelled'
GROUP BY u.id, u.name
HAVING COUNT(o.id) >= 3          -- filtrar DESPUÉS de agrupar
ORDER BY lifetime_value DESC;

-- Productos más vendidos
SELECT
    p.name,
    SUM(oi.quantity)         AS units_sold,
    SUM(oi.quantity * oi.unit_price) AS revenue
FROM   products    p
JOIN   order_items oi ON oi.product_id = p.id
JOIN   orders      o  ON o.id = oi.order_id
WHERE  o.status = 'completed'
  AND  o.created_at >= NOW() - INTERVAL '30 days'
GROUP  BY p.id, p.name
ORDER  BY revenue DESC
LIMIT  10;

Subconsultas

-- Subconsulta en WHERE
SELECT name, email
FROM   users
WHERE  id IN (
    SELECT DISTINCT user_id
    FROM   orders
    WHERE  total_cents > 50000
);

-- Subconsulta correlacionada (referencia tabla exterior)
SELECT
    u.name,
    (
        SELECT COUNT(*)
        FROM   orders o
        WHERE  o.user_id = u.id
          AND  o.status  = 'completed'
    ) AS completed_orders
FROM users u;

-- Subconsulta en FROM (tabla derivada)
SELECT
    month,
    SUM(revenue) AS monthly_revenue
FROM (
    SELECT
        DATE_TRUNC('month', created_at) AS month,
        total_cents AS revenue
    FROM orders
    WHERE status = 'completed'
) AS monthly_orders
GROUP BY month
ORDER BY month;

Common Table Expressions (CTE)

-- CTE: consultas más legibles y reutilizables
WITH active_users AS (
    SELECT id, name, email
    FROM   users
    WHERE  deleted_at IS NULL AND role = 'user'
),
user_stats AS (
    SELECT
        u.id,
        u.name,
        COUNT(o.id)        AS order_count,
        COALESCE(SUM(o.total_cents), 0) AS total_spent
    FROM active_users u
    LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'completed'
    GROUP BY u.id, u.name
)
SELECT
    name,
    order_count,
    total_spent,
    RANK() OVER (ORDER BY total_spent DESC) AS spending_rank
FROM user_stats
WHERE order_count > 0
ORDER BY spending_rank;

-- CTE recursiva: árbol de categorías
WITH RECURSIVE category_tree AS (
    -- Caso base: categorías raíz
    SELECT id, name, parent_id, 0 AS depth, name AS path
    FROM categories
    WHERE parent_id IS NULL

    UNION ALL

    -- Caso recursivo: hijos
    SELECT c.id, c.name, c.parent_id, ct.depth + 1, ct.path || ' > ' || c.name
    FROM categories c
    JOIN category_tree ct ON ct.id = c.parent_id
)
SELECT * FROM category_tree ORDER BY path;

Window Functions

-- Ranking dentro de grupos
SELECT
    u.name,
    o.total_cents,
    RANK()       OVER (ORDER BY o.total_cents DESC) AS global_rank,
    ROW_NUMBER() OVER (PARTITION BY o.status ORDER BY o.created_at) AS rank_in_status,
    LAG(o.total_cents)  OVER (PARTITION BY o.user_id ORDER BY o.created_at) AS prev_order,
    LEAD(o.total_cents) OVER (PARTITION BY o.user_id ORDER BY o.created_at) AS next_order,
    SUM(o.total_cents)  OVER (PARTITION BY o.user_id ORDER BY o.created_at
                              ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
                             ) AS running_total
FROM orders o
JOIN users  u ON u.id = o.user_id
ORDER BY u.name, o.created_at;

Optimización de consultas

-- EXPLAIN ANALYZE: ver el plan de ejecución
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 1 AND status = 'pending';

-- Resultado típico (sin índice):
-- Seq Scan on orders  (cost=0.00..150.00 rows=5 width=50)
--   Filter: (user_id = 1 AND status = 'pending')

-- Con índice:
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- Index Scan using idx_orders_user_status on orders  (cost=0.15..8.17 rows=5)

Patrones de optimización

-- ❌ Evita funciones en columnas indexadas (rompe el índice)
SELECT * FROM users WHERE LOWER(email) = 'ana@test.com';

-- ✅ Usa índices funcionales o normaliza los datos al insertar
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
-- o bien: guarda el email en minúsculas

-- ❌ SELECT * (trae columnas innecesarias)
SELECT * FROM users WHERE id = 1;

-- ✅ Proyecta solo lo que necesitas
SELECT id, name, email FROM users WHERE id = 1;

-- ❌ N+1 problem: una query por cada usuario
-- for user in users: SELECT * FROM orders WHERE user_id = user.id

-- ✅ Una sola query con JOIN
SELECT u.id, u.name, o.id AS order_id, o.total_cents
FROM users  u
JOIN orders o ON o.user_id = u.id
WHERE u.id = ANY(ARRAY[1, 2, 3, 4, 5]);

-- ❌ OFFSET grande es lento (escanea todas las filas anteriores)
SELECT * FROM orders ORDER BY created_at LIMIT 10 OFFSET 100000;

-- ✅ Paginación por cursor (keyset pagination)
SELECT * FROM orders
WHERE created_at < '2024-06-15 10:30:00'  -- último valor de la página anterior
ORDER BY created_at DESC
LIMIT 10;

Buenas prácticas

Práctica Descripción
Usa parámetros, nunca concatenes Previene SQL injection
Pagina con cursor Más eficiente que OFFSET en tablas grandes
EXPLAIN antes de optimizar No asumas, mide el plan real
Índices en FK PostgreSQL no los crea automáticamente
Evita columnas nullables en índices Pueden no usarse en algunos SGBD
Usa transacciones Para mantener consistencia en múltiples operaciones
🔒

Ejercicio práctico disponible

Operaciones tipo SQL en Arrays

Desbloquear ejercicios
// Operaciones tipo SQL en Arrays
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes