SQL Cheat Sheet: Consultas Clave, Joins y Optimización Básica

Una referencia directa y estructurada para el trabajo diario con bases de datos relacionales. Cubre desde operaciones fundamentales y cruces de tablas hasta optimización con índices y diagnósticos rápidos de rendimiento.


1. Orden de Ejecución de una Consulta

Para depurar y optimizar consultas complejas es clave entender que SQL no ejecuta las cláusulas en el orden en que se escriben:

  1. FROM / JOIN: Se identifican y cruzan las tablas origen.
  2. WHERE: Se filtran filas a nivel individual.
  3. GROUP BY: Se agrupan los registros resultantes.
  4. HAVING: Se filtran los grupos creados.
  5. SELECT: Se evalúan expresiones y proyecciones.
  6. DISTINCT: Se eliminan filas duplicadas.
  7. ORDER BY: Se ordenan los resultados.
  8. LIMIT / OFFSET: Se pagina el conjunto devuelto.

2. Tipos de JOIN

Considera dos entidades: usuarios (id, nombre) y pedidos (id, usuario_id, total).

INNER JOIN

Retorna únicamente los registros que coinciden en ambas tablas.

sql
SELECT u.nombre, p.total
FROM usuarios u
INNER JOIN pedidos p ON u.id = p.usuario_id;

LEFT JOIN (LEFT OUTER JOIN)

Retorna todos los registros de la tabla izquierda y las coincidencias de la derecha (o NULL si no hay relación).

sql
SELECT u.nombre, p.total
FROM usuarios u
LEFT JOIN pedidos p ON u.id = p.usuario_id;

Tip práctico: Para encontrar "usuarios sin pedidos":

sql
> SELECT u.nombre
> FROM usuarios u
> LEFT JOIN pedidos p ON u.id = p.usuario_id
> WHERE p.id IS NULL;
>

RIGHT JOIN

Retorna todos los registros de la tabla derecha y las coincidencias de la izquierda. Rara vez se usa; suele reescribirse como LEFT JOIN invirtiendo el orden de las tablas por claridad.

FULL OUTER JOIN

Retorna todos los registros cuando hay coincidencia en cualquiera de las dos tablas (rellena con NULL donde falte el par).

sql
-- Soportado nativamente en PostgreSQL:
SELECT u.nombre, p.total
FROM usuarios u
FULL OUTER JOIN pedidos p ON u.id = p.usuario_id;


3. Subconsultas y CTEs (Common Table Expressions)

Subconsulta en WHERE (con IN / EXISTS)

Prefiere EXISTS sobre IN cuando la subconsulta maneje grandes volúmenes de datos, ya que el motor puede detener la búsqueda al encontrar la primera coincidencia:

sql
SELECT nombre
FROM usuarios u
WHERE EXISTS (
    SELECT 1 
    FROM pedidos p 
    WHERE p.usuario_id = u.id AND p.total > 1000
);

CTEs (WITH): Código más legible

Las CTEs reemplazan subconsultas anidadas difíciles de mantener por bloques reutilizables:

sql
WITH pedidos_recientes AS (
    SELECT usuario_id, SUM(total) AS total_gastado
    FROM pedidos
    WHERE fecha >= CURRENT_DATE - INTERVAL '30 days' -- PostgreSQL
    GROUP BY usuario_id
)
SELECT u.nombre, pr.total_gastado
FROM usuarios u
INNER JOIN pedidos_recientes pr ON u.id = pr.usuario_id
WHERE pr.total_gastado > 5000;


4. Agregación y Window Functions

Agrupamiento y filtro de grupos

WHERE filtra filas antes de agrupar; HAVING filtra después de agrupar:

sql
SELECT usuario_id, COUNT(*) AS total_pedidos, AVG(total) AS promedio
FROM pedidos
WHERE estado = 'COMPLETADO'
GROUP BY usuario_id
HAVING COUNT(*) >= 5;

Funciones de Ventana (ROW_NUMBER, RANK)

Permiten calcular sobre particiones sin colapsar las filas resultantes:

sql
-- Obtener el pedido más reciente por cada usuario
WITH ranking_pedidos AS (
    SELECT 
        id, 
        usuario_id, 
        total,
        ROW_NUMBER() OVER (PARTITION BY usuario_id ORDER BY fecha DESC) as rn
    FROM pedidos
)
SELECT id, usuario_id, total
FROM ranking_pedidos
WHERE rn = 1;


5. Estrategia e Índices

B-Tree (Índice Estándar)

Ideal para comparaciones de igualdad (=), rangos (<, <=, >, >=) y ordenamiento (ORDER BY):

sql
-- PostgreSQL / MySQL
CREATE INDEX idx_pedidos_usuario_fecha ON pedidos (usuario_id, fecha DESC);

Regla de oro del índice compuesto (Leftmost Prefix):
Un índice en (colA, colB) acelera filtros en colA o en colA AND colB, pero no acelerará un filtro que consulte únicamente colB.

Índices Parciales / Filtrados (PostgreSQL)

Ahorran espacio en disco y memoria RAM si solo consultas un subconjunto específico:

sql
-- Solo indexa pedidos activos, ignorando el historial completado o cancelado
CREATE INDEX idx_pedidos_pendientes 
ON pedidos (fecha) 
WHERE estado = 'PENDIENTE';


6. Diagnóstico y Optimización de Consultas

Analizar planes de ejecución

Nunca asumas cómo ejecutará la base de datos tu consulta; inspecciona el costo:

En PostgreSQL:

sql
-- EXPLAIN ANALYZE ejecuta la consulta y muestra tiempos reales
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM pedidos WHERE usuario_id = 42 ORDER BY fecha DESC;

  • Seq Scan (Sequential Scan): Recorre toda la tabla fila por fila. Si la tabla es grande, evalúa crear un índice.
  • Index Scan / Index Only Scan: Acceso eficiente mediante árbol de índice.

En MySQL:

sql
EXPLAIN FORMAT=TREE
SELECT * FROM pedidos WHERE usuario_id = 42;

  • Revisa la columna type: ALL (escaneo completo de tabla, evitar si es grande), ref o const (búsqueda rápida por índice).

7. Mantenimiento Rápido y Salud de Tablas

Con el paso del tiempo, las inserciones, actualizaciones (UPDATE) y borrados (DELETE) dejan espacio muerto o estadísticas desactualizadas que confunden al optimizador del motor.

PostgreSQL

sql
-- Actualiza estadísticas para que el optimizador elija el mejor plan:
ANALYZE VERBOSE pedidos;

-- Recupera espacio de tuplas muertas y recalcula estadísticas:
VACUUM ANALYZE pedidos;

-- Ver tamaño de tablas e índices:
SELECT 
    relname AS tabla,
    pg_size_pretty(pg_total_relation_size(relid)) AS tamano_total
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC;

MySQL / MariaDB

sql
-- Actualiza estadísticas de índices:
ANALYZE TABLE pedidos;

-- Desfragmenta y recupera espacio no utilizado:
OPTIMIZE TABLE pedidos;

-- Ver procesos en ejecución y queries lentas activas:
SHOW PROCESSLIST;


Buenas Prácticas Rápidas

  1. Evita SELECT * en producción: Solicita únicamente las columnas requeridas para reducir uso de red y permitir Index Only Scans.
  2. Cuidado con funciones sobre columnas indexadas: WHERE YEAR(fecha) = 2026 invalida el índice en fecha. Usa rangos: WHERE fecha >= '2026-01-01' AND fecha < '2027-01-01'.
  3. Limita siempre queries exploratorias: Evita bloqueos y consumo accidental de memoria añadiendo LIMIT 50 en entornos vivos.