Guía rápida de SQL
Cada construcción de SQL en su sintaxis mínima y la trampa que más se repite. Toca un tema para ver el ejemplo verificado, cómo leerlo y las diferencias entre motores.
Todos los niveles: básico, intermedio y avanzado
39 entradas
Orden de escritura y de ejecución
- 1FROM / JOIN arma las filas de partida
- 2WHERE filtra filas (no ve alias ni agregados)
- 3GROUP BY forma los grupos
- 4HAVING filtra grupos
- 5SELECT calcula columnas, alias y ventanas
- 6DISTINCT quita filas repetidas
- 7ORDER BY ordena (ya ve los alias)
- 8LIMIT / OFFSET corta el resultado
El orden lógico de una consulta
SELECT expr AS alias ... ORDER BY alias -- sí
SELECT expr AS alias ... WHERE alias -- noTrampa: Un alias o una ventana del SELECT no existen en WHERE: repite la expresión o usa una CTE.
SELECT, filtros y orden
SELECT, columnas calculadas y alias
SELECT a, b - c AS neto, d AS "Monto neto"
FROM tabla AS t;Trampa: El alias va sin comillas o con comillas dobles; las simples son texto.
DISTINCT: valores y combinaciones únicas
SELECT DISTINCT pais, ciudad FROM clientes;
count(DISTINCT cliente_id)Trampa: DISTINCT mira la fila completa; DISTINCT(col) no cambia nada.
WHERE, comparaciones, AND, OR y NOT
WHERE a = 1
AND (b = 2 OR c = 3)
AND NOT dTrampa: AND se evalúa antes que OR: si mezclas los dos, usa paréntesis.
IN, BETWEEN, LIKE e ILIKE
WHERE x IN ('a', 'b')
AND n BETWEEN 10 AND 20 -- incluye extremos
AND t LIKE 'ab%' -- ILIKE: sin mayúsculasTrampa: BETWEEN sobre fecha y hora pierde el último día: usa >= inicio AND < fin.
ORDER BY con varias claves, NULLS LAST y LIMIT
ORDER BY rating DESC NULLS LAST, id
LIMIT 10;Trampa: En PostgreSQL NULL es el mayor: con DESC sale primero si no pones NULLS LAST.
Paginación: OFFSET o keyset
LIMIT 10 OFFSET 20 -- simple, lento
WHERE (f, id) < (:f, :id) -- por clave
ORDER BY f DESC, id DESC LIMIT 10Trampa: Sin un desempate único en el ORDER BY, las filas cambian de página.
NULL
Comparar con NULL: IS NULL e IS DISTINCT FROM
WHERE col IS NULL
WHERE (col <> 5 OR col IS NULL)Trampa: col = NULL nunca es verdadero: devuelve 0 filas sin error.
NULL en count, sum y avg
count(*) count(col) avg(col)
coalesce(sum(col), 0)Trampa: count(col) y avg ignoran NULL; sum sin valores da NULL, no 0.
COALESCE y NULLIF
coalesce(col, 'sin dato')
a / NULLIF(b, 0)Trampa: Los argumentos de COALESCE deben ser del mismo tipo.
CASE, tipos, fechas y texto
CASE WHEN: decidir dentro de la consulta
CASE WHEN x < 150 THEN 'corta'
WHEN x < 240 THEN 'media'
ELSE 'larga' ENDTrampa: Gana el primer WHEN que se cumple; sin ELSE devuelve NULL.
Conversión de tipos, división entera y redondeo
col::numeric CAST(col AS numeric)
round(a::numeric / b, 2)Trampa: Entre enteros, 5 / 2 da 2: convierte a numeric antes de dividir.
Fechas: date_trunc, extract y to_char
date_trunc('month', ts)
extract(year FROM ts)
to_char(ts, 'YYYY-MM')Trampa: Agrupar por extract(month …) junta enero de años distintos.
Rangos de fechas, intervalos y zona horaria
WHERE ts >= '2025-08-01'
AND ts < '2025-09-01'
ts + INTERVAL '1 month'Trampa: Rango semiabierto siempre; el «día» local necesita AT TIME ZONE.
Funciones de texto: limpiar, cortar y unir
lower(trim(t)) length(t)
split_part(email, '@', 2)
concat(a, ' ', b) a || ' ' || bTrampa: a || NULL da NULL; concat ignora los NULL.
Agregación, GROUP BY y HAVING
COUNT, SUM, AVG, MIN y MAX
count(*) count(DISTINCT c)
sum(x) avg(x) min(x) max(x)Trampa: No sumes importes de monedas distintas: filtra o convierte antes.
GROUP BY: una fila por grupo
SELECT date_trunc('month', ts) AS mes,
count(*)
FROM t
GROUP BY date_trunc('month', ts);Trampa: Toda columna del SELECT que no es agregado va en el GROUP BY.
HAVING: filtrar grupos
WHERE status = 'x' -- filas
GROUP BY g
HAVING count(*) >= 2 -- gruposTrampa: Condiciones sobre filas en WHERE; sobre agregados en HAVING.
Agregación condicional: FILTER y CASE
count(*) FILTER (WHERE s = 'x')
sum(CASE WHEN s = 'x' THEN 1 ELSE 0 END)Trampa: Si filtras en WHERE pierdes el total y la tasa sale 100 %.
Joins y cardinalidad
INNER JOIN y LEFT JOIN
FROM a
LEFT JOIN b ON b.a_id = a.id
AND b.status = 'x'Trampa: Filtrar b en el WHERE convierte el LEFT JOIN en INNER JOIN.
Cardinalidad: cuando el join multiplica filas
SELECT count(*), count(DISTINCT o.id)
FROM orders o
JOIN payments p ON p.order_id = o.idTrampa: Uno a muchos repite el lado «uno»: agrega la tabla hija antes de unir.
Anti join: filas sin pareja
WHERE NOT EXISTS (
SELECT 1 FROM b WHERE b.a_id = a.id
)Trampa: NOT IN con un NULL en la subconsulta devuelve 0 filas.
Self join: unir una tabla consigo misma
FROM categories h
LEFT JOIN categories p ON p.id = h.parent_id
-- pares: ON a.g = b.g AND a.id < b.idTrampa: a.id <> b.id cuenta cada par dos veces: (A, B) y (B, A).
Subconsultas, CTE y conjuntos
Subconsultas: escalares, derivadas y correlacionadas
WHERE x > (SELECT avg(x) FROM t)
FROM (SELECT ... GROUP BY ...) AS d
WHERE EXISTS (SELECT 1 FROM h
WHERE h.p_id = p.id)Trampa: Una subconsulta escalar que devuelve dos filas corta la consulta con error.
CTE: WITH para escribir en pasos
WITH paso_1 AS (...),
paso_2 AS (SELECT ... FROM paso_1)
SELECT ... FROM paso_2;Trampa: Pasos con nombre en lugar de subconsultas anidadas: se prueban por separado.
UNION, INTERSECT y EXCEPT
SELECT c FROM a
UNION ALL -- UNION quita repetidos
SELECT c FROM b -- también INTERSECT, EXCEPTTrampa: UNION borra filas repetidas: para eventos, UNION ALL.
Funciones de ventana
ROW_NUMBER, RANK y DENSE_RANK
row_number() OVER (PARTITION BY g
ORDER BY x DESC, id)
rank(): 1, 1, 3 dense_rank(): 1, 1, 2Trampa: row_number sin desempate único elige un ganador distinto en cada ejecución.
PARTITION BY: agregados sin colapsar filas
avg(x) OVER (PARTITION BY g)
x / sum(x) OVER () -- participaciónTrampa: OVER no colapsa filas; GROUP BY sí.
LAG y LEAD: la fila anterior y la siguiente
lag(x) OVER (PARTITION BY g ORDER BY fecha)
lead(x, 1, 0) OVER (...)Trampa: Sin PARTITION BY, el primer valor de un grupo toma el último del anterior.
Totales acumulados y el marco de la ventana
sum(x) OVER (PARTITION BY g
ORDER BY fecha, id
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW)Trampa: Sin ROWS, las filas empatadas reciben el mismo acumulado.
Patrones de entrevista
Top-N por grupo
WITH r AS (SELECT *, rank() OVER (
PARTITION BY g ORDER BY m DESC) AS p
FROM t)
SELECT * FROM r WHERE p <= 3;Trampa: LIMIT corta el total, no cada grupo; decide qué hacer con los empates.
Deduplicar: quedarse con una fila por clave
row_number() OVER (PARTITION BY clave
ORDER BY criterio, id) AS n -- n = 1
-- PostgreSQL: DISTINCT ON (clave)Trampa: SELECT DISTINCT * no deduplica: el id es distinto en cada copia.
El último registro de cada entidad
row_number() OVER (PARTITION BY entidad
ORDER BY fecha DESC, id DESC) AS n
-- quedarse con n = 1Trampa: max(fecha) y volver a unir duplica la entidad si la fecha empata.
Promedio móvil de 7 días
avg(x) OVER (ORDER BY dia
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)
-- sobre generate_series(ini, fin, '1 day')Trampa: ROWS cuenta filas, no días: completa el calendario con 0 antes.
Porcentajes y participación sobre el total
round(100.0 * count(*)
/ sum(count(*)) OVER (), 1)
100.0 * a / NULLIF(b, 0)Trampa: count / count es división entera: multiplica primero por 100.0.
Cohortes y retención del mes 1
date_trunc('month', signup_at) AS cohorte
-- LEFT JOIN a la actividad del mes 1
-- activos / tamaño de la cohorteTrampa: Un INNER JOIN a la actividad achica el denominador: 100 % falso.
Rendimiento
Filtros sargables: el rango sobre la columna cruda
WHERE ts >= '2025-08-01'
AND ts < '2025-09-01' -- usa el índice
WHERE date_trunc('month', ts) = ... -- noTrampa: Una función aplicada a la columna impide usar su índice.
Índices, selectividad y planes de ejecución
CREATE INDEX ON orders (restaurant_id,
placed_at);
EXPLAIN ANALYZE SELECT ...;Trampa: Un índice ayuda si el filtro es selectivo; si deja pasar casi todo, se ignora.
Reducir el trabajo: agregar antes de unir
WITH r AS (SELECT k, count(*) AS n
FROM grande GROUP BY k)
SELECT d.nombre, r.n
FROM r JOIN dim d ON d.id = r.k;Trampa: JOIN + DISTINCT para saber si hubo actividad: mejor EXISTS.
Equivalencias entre motores
| Qué necesitas | PostgreSQL | MySQL | SQL Server | BigQuery | Snowflake |
|---|---|---|---|---|---|
| Primeras n filas | LIMIT n | LIMIT n | TOP (n) | LIMIT n | LIMIT n |
| Valor por defecto | COALESCE | IFNULL | ISNULL | IFNULL | NVL |
| Inicio del mes | date_trunc('month', ts) | DATE_FORMAT(ts, '%Y-%m-01') | DATETRUNC(month, ts) (2022+) | TIMESTAMP_TRUNC(ts, MONTH) | DATE_TRUNC('month', ts) |
| Sumar un mes | ts + INTERVAL '1 month' | DATE_ADD(ts, INTERVAL 1 MONTH) | DATEADD(month, 1, ts) | DATE_ADD(d, INTERVAL 1 MONTH) | DATEADD(month, 1, ts) |
| Filtrar por ventana | subconsulta | subconsulta | subconsulta | QUALIFY | QUALIFY |
| Conversión segura | — | — | TRY_CAST | SAFE_CAST | TRY_CAST |
SQL Server también acepta OFFSET … FETCH. «—»: no hay una función equivalente.
Antes de decir «listo»
- ¿Qué representa una fila del resultado (el grano)?
- Después de cada join, ¿
count(*)coincide concount(DISTINCT clave)? - ¿Qué pasa con los
NULLen filtros,NOT INy promedios? - ¿Los rangos de fecha son semiabiertos y en qué zona horaria?
- ¿Cómo se resuelven los empates del ranking o del orden?
- ¿Hay importes en monedas distintas?