SQL (Snowflake): chuleta de agregación y funciones de ventana con quiz
Trece patrones de SQL detrás de las pruebas de ingeniería y analítica de datos, en sabor Snowflake: agregación, GROUP BY y HAVING, funciones de ventana, QUALIFY, CTE, JSON y MERGE, con un quiz de práctica de 20 preguntas.
Chuleta de sintaxis
13 patrones. Elige uno en el mapa para ver su sintaxis, cuándo usarlo y la trampa que esconde.
Básicos
SELECT, WHERE, ORDER BY
Filas que entran, filas que salen
- Sintaxis
SELECT order_id, customer_id, amount FROM ORDERS WHERE status = 'completed' AND amount > 100 ORDER BY amount DESC LIMIT 10; SELECT DISTINCT customer_id FROM ORDERS;- Cuándo usarlo
- En cada consulta. WHERE filtra filas individuales antes de cualquier agrupación.
- Qué hace
- Orden lógico de evaluación: FROM, WHERE, GROUP BY, HAVING, SELECT, QUALIFY, DISTINCT, ORDER BY, LIMIT. Ese orden explica qué ve cada cláusula: WHERE ve filas, HAVING ve grupos, QUALIFY ve resultados de ventana.
- Cuidado con
- Un alias definido en SELECT no puede usarse en WHERE (se ejecuta antes). NULL nunca es igual a nada: usa IS NULL. LIMIT sin ORDER BY devuelve un conjunto arbitrario de filas.
- Ejemplo
- SELECT customer_id, amount * 1.21 AS gross FROM ORDERS WHERE amount * 1.21 > 100 -- not: WHERE gross > 100 AND status IS NOT NULL ORDER BY gross DESC;
Agregación
COUNT, SUM, AVG, MIN, MAX
Un número por grupo
- Sintaxis
SELECT COUNT(*) AS orders, COUNT(DISTINCT customer_id) AS customers, SUM(amount) AS revenue, AVG(amount) AS avg_order, MIN(amount), MAX(amount), MEDIAN(amount) FROM ORDERS WHERE status = 'completed';- Cuándo usarlo
- Totales, medias, recuentos y extremos sobre un conjunto de filas, con o sin GROUP BY.
- Qué hace
- Un agregado colapsa muchas filas en un valor. Sin GROUP BY, toda la tabla filtrada es un solo grupo y la consulta devuelve una fila.
- Cuidado con
- COUNT(*) cuenta filas, COUNT(col) omite los NULL, COUNT(DISTINCT col) cuenta valores únicos no nulos. AVG ignora los NULL, así que AVG(col) no es SUM(col) / COUNT(*). MEDIAN resiste mejor los valores extremos que AVG.
- Ejemplo
- SELECT PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY amount) AS p90, APPROX_COUNT_DISTINCT(customer_id) AS est_customers, APPROX_PERCENTILE(amount, 0.95) AS p95_fast FROM ORDERS;
Agregación
GROUP BY y HAVING
Filtrar grupos, no filas
- Sintaxis
SELECT customer_id, SUM(amount) AS total_spent, COUNT(*) AS orders FROM ORDERS WHERE status = 'completed' -- rows, before grouping GROUP BY customer_id HAVING SUM(amount) > 300 -- groups, after aggregation ORDER BY total_spent DESC;- Cuándo usarlo
- Métricas por cliente, por mes o por estado; cualquier pregunta de «clientes con más de N pedidos».
- Qué hace
- GROUP BY produce una fila de salida por cada combinación distinta de las columnas de agrupación. HAVING filtra esas filas usando agregados; WHERE no puede, porque se ejecuta antes de que existan los grupos.
- Cuidado con
- Toda columna del SELECT sin agregar debe estar en GROUP BY (o usa GROUP BY ALL en Snowflake). GROUP BY 1, 2 se refiere a posiciones del SELECT. Una condición HAVING sobre una columna normal suele ser un WHERE disfrazado y debería ir ahí.
- Ejemplo
- SELECT customer_id, status, SUM(amount) AS total FROM ORDERS GROUP BY ALL; -- Snowflake: every non-aggregated column SELECT customer_id, COUNT(DISTINCT product_id) AS products FROM ORDERS GROUP BY customer_id HAVING COUNT(DISTINCT product_id) > 2;
Agregación
Agregación condicional
CASE, IFF, COUNT_IF
- Sintaxis
SELECT customer_id, SUM(IFF(status = 'completed', amount, 0)) AS completed_revenue, SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS pending_revenue, COUNT_IF(status = 'cancelled') AS cancelled_orders, COUNT_IF(status = 'completed') * 100.0 / NULLIF(COUNT(*), 0) AS completion_pct FROM ORDERS GROUP BY customer_id;- Cuándo usarlo
- Varias métricas con filtros distintos en una sola pasada (columnas tipo pivot), tasas y porcentajes.
- Qué hace
- La condición elige qué filas alimentan cada agregado, así que un solo recorrido produce muchos totales filtrados. NULLIF(x, 0) convierte un divisor cero en NULL en vez de un error; COALESCE sustituye un resultado NULL.
- Cuidado con
- SUM(CASE … END) sin ELSE 0 funciona (los NULL se ignoran), pero COUNT(CASE …) cuenta resultados no nulos, no filas. La división entera trunca: multiplica por 100.0, no por 100.
- Ejemplo
- SELECT COUNT(DISTINCT CASE WHEN status = 'completed' THEN customer_id END) AS buyers, COALESCE(SUM(amount), 0) AS revenue FROM ORDERS;
Agregación
Truncar fechas y aritmética
Agrupar por mes
- Sintaxis
DATE_TRUNC('MONTH', order_date) -- 2026-01-01 for any January date YEAR(order_date), MONTH(order_date), DAYNAME(order_date) DATEDIFF(DAY, first_date, last_date) DATEADD(DAY, -7, CURRENT_DATE) order_date::DATE- Cuándo usarlo
- Ingresos mensuales, clientes activos por mes, cohortes, filtros de «últimos 7 días», intervalos entre pedidos.
- Qué hace
- DATE_TRUNC conserva el tipo y baja el valor al inicio de la unidad, así que agrupar y ordenar por él es correcto entre años. DATEDIFF cuenta límites de unidad cruzados; DATEADD desplaza una fecha.
- Cuidado con
- GROUP BY MONTH(order_date) mezcla enero de 2025 con enero de 2026: usa DATE_TRUNC o agrupa por año y mes. DATEDIFF(MONTH, "2026-01-31", "2026-02-01") es 1 porque se cruza un límite de mes.
- Ejemplo
- SELECT DATE_TRUNC('MONTH', order_date) AS month, SUM(amount) AS revenue, COUNT(DISTINCT customer_id) AS active_customers FROM ORDERS WHERE status = 'completed' GROUP BY 1 ORDER BY 1;
Agregación avanzada
ROLLUP, CUBE, GROUPING SETS
Subtotales en una consulta
- Sintaxis
GROUP BY ROLLUP(region, product) -- (region, product), (region), () GROUP BY CUBE(region, product) -- every combination, including () GROUP BY GROUPING SETS ((region), (product), ()) GROUPING(region) -- 1 on the rows where region was aggregated away- Cuándo usarlo
- Informes que necesitan filas de detalle más subtotales y un total general sin UNION ALL de varias consultas.
- Qué hace
- Cada conjunto de agrupación produce sus propias filas agregadas en un solo resultado; una columna agregada aparece como NULL en esa fila. GROUPING() distingue un NULL de subtotal de un NULL real.
- Cuidado con
- ROLLUP es jerárquico (quita columnas por la derecha); CUBE produce 2ⁿ conjuntos, que se disparan con muchas columnas. Ordena con GROUPING() para dejar los totales al final.
- Ejemplo
- SELECT region, product, SUM(amount) AS revenue, GROUPING(product) AS is_subtotal FROM SALES GROUP BY ROLLUP(region, product) ORDER BY region NULLS LAST, is_subtotal, product;
Joins
JOIN y EXISTS
Combinar tablas
- Sintaxis
SELECT c.name, o.amount FROM CUSTOMERS c LEFT JOIN ORDERS o ON o.customer_id = c.customer_id; -- keep every customer SELECT c.customer_id FROM CUSTOMERS c WHERE NOT EXISTS (SELECT 1 FROM ORDERS o WHERE o.customer_id = c.customer_id);- Cuándo usarlo
- INNER solo para coincidencias, LEFT para conservar todas las filas de la tabla izquierda, FULL para ambos lados, CROSS para todas las combinaciones. EXISTS para «tiene al menos uno» y NOT EXISTS para «no tiene ninguno».
- Qué hace
- Un join empareja filas que cumplen la condición ON. LEFT JOIN rellena con NULL las columnas del lado derecho que faltan, así que WHERE o.order_id IS NULL encuentra clientes sin pedidos.
- Cuidado con
- Un filtro sobre la tabla derecha en WHERE convierte un LEFT JOIN en INNER JOIN: ponlo en ON. Hacer join por una clave no única multiplica filas (fan-out), lo que infla SUM y COUNT.
- Ejemplo
- -- customers with no completed order (the filter stays in ON) SELECT c.customer_id FROM CUSTOMERS c LEFT JOIN ORDERS o ON o.customer_id = c.customer_id AND o.status = 'completed' WHERE o.order_id IS NULL;
Estructura de la consulta
CTE y subconsultas
Construir por pasos
- Sintaxis
WITH customer_revenue AS ( SELECT customer_id, SUM(amount) AS revenue FROM ORDERS GROUP BY customer_id ), avg_revenue AS (SELECT AVG(revenue) AS avg_rev FROM customer_revenue) SELECT c.* FROM customer_revenue c, avg_revenue a WHERE c.revenue > a.avg_rev;- Cuándo usarlo
- Lógica en varios pasos (agregar, comparar, clasificar), reutilizar un resultado intermedio, legibilidad en consultas largas.
- Qué hace
- Una CTE da nombre a un conjunto de resultados para el resto de la sentencia; varias se encadenan con comas y las posteriores pueden referirse a las anteriores. Una subconsulta en WHERE devuelve un valor o una lista con la que comparar.
- Cuidado con
- La media de ingresos por cliente no es el importe medio del pedido: agrega por cliente primero y luego promedia. Una CTE referenciada dos veces puede ejecutarse dos veces; materializa las pesadas en una tabla temporal.
- Ejemplo
- SELECT * FROM ORDERS WHERE amount > (SELECT AVG(amount) FROM ORDERS); -- scalar subquery
Funciones de ventana
Funciones de ventana y OVER()
Agregar sin colapsar
- Sintaxis
SELECT employee, department, salary, AVG(salary) OVER (PARTITION BY department) AS dept_avg, -- per group, every row kept AVG(salary) OVER () AS company_avg, -- whole table, on every row SUM(salary) OVER (PARTITION BY department ORDER BY salary) AS running FROM EMPLOYEES;- Cuándo usarlo
- Necesitas un número de grupo junto a cada fila: cuota del total, diferencia con la media del departamento, posición dentro del grupo. GROUP BY colapsaría las filas que aún necesitas.
- Qué hace
- OVER() define la ventana. PARTITION BY divide las filas en grupos independientes, ORDER BY fija el orden dentro de cada uno (necesario para totales acumulados, rankings, LAG y LEAD) y un OVER() vacío abarca todo el resultado. La función se ejecuta una vez por fila sobre la ventana de esa fila, así que el número de filas no cambia.
- Cuidado con
- GROUP BY devuelve una fila por grupo; una función de ventana devuelve todas las filas. Sin PARTITION BY toda la tabla es un solo grupo. Las funciones de ventana se evalúan después de WHERE, GROUP BY y HAVING, así que no pueden ir en WHERE: fíltralas con QUALIFY (Snowflake) o con una subconsulta.
- Ejemplo
- -- 5 employees in, 5 rows out (GROUP BY department would return 2) SELECT employee, department, salary, AVG(salary) OVER (PARTITION BY department) AS avg_salary FROM EMPLOYEES; -- Finance: Andrew 50000 → 40000, Brian 50000 → 40000, Charles 20000 → 40000 -- Sales: Daniel 30000 → 25000, Ethan 20000 → 25000
Funciones de ventana
ROW_NUMBER, RANK y QUALIFY
Última fila por grupo
- Sintaxis
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) -- 1, 2, 3, 4, 5 (always unique) RANK() OVER (ORDER BY salary DESC) -- 1, 1, 3, 4, 4 (gaps after ties) DENSE_RANK() OVER (ORDER BY salary DESC) -- 1, 1, 2, 3, 3 (no gaps) NTILE(4) OVER (ORDER BY salary DESC) -- bucket 1–4 (quartiles) PERCENT_RANK() OVER (ORDER BY salary DESC) -- (rank − 1) / (rows − 1) SELECT * FROM ORDERS QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC, order_id DESC) = 1;- Cuándo usarlo
- Último o primer registro por cliente, top N por grupo, deduplicación de datos de staging, clasificaciones, cuartiles para segmentar.
- Qué hace
- ROW_NUMBER siempre es único; RANK da el mismo número a los empates y salta los siguientes; DENSE_RANK da el mismo número a los empates sin saltar. QUALIFY filtra por el resultado de ventana dentro de la misma consulta, como HAVING filtra agregados, y sustituye a la subconsulta envolvente.
- Cuidado con
- ROW_NUMBER rompe los empates de forma arbitraria salvo que el ORDER BY sea único (añade order_id). GROUP BY customer_id con MAX(order_date) da la fecha, pero no el resto de esa fila. Fuera de Snowflake, envuelve la consulta en una subconsulta y filtra rn = 1 en el WHERE exterior.
- Ejemplo
- -- top 3 customers by revenue SELECT customer_id, SUM(amount) AS revenue FROM ORDERS GROUP BY customer_id QUALIFY ROW_NUMBER() OVER (ORDER BY SUM(amount) DESC) <= 3; -- older duplicate registrations per e-mail (the latest one is rn = 1) SELECT * FROM CUSTOMERS QUALIFY ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) > 1;
Funciones de ventana
Totales acumulados, marcos, LAG y LEAD
Marcos y desplazamientos
- Sintaxis
SUM(amount) OVER (ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) -- running total SUM(amount) OVER (ORDER BY order_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) -- 3-row moving sum LAG(amount) OVER (PARTITION BY customer_id ORDER BY order_date) -- previous row (NULL on the first) LEAD(amount) OVER (PARTITION BY customer_id ORDER BY order_date) -- next row (NULL on the last) FIRST_VALUE(product_id) OVER (PARTITION BY customer_id ORDER BY order_date) -- first purchase amount * 100.0 / SUM(amount) OVER () -- share of the total- Cuándo usarlo
- Ingresos acumulados, medias móviles, variación mes a mes, porcentaje del total, intervalos entre fechas consecutivas, gasto acumulado por cliente.
- Qué hace
- El marco (ROWS BETWEEN inicio AND fin) elige qué filas de la partición ordenada alimentan el agregado de cada fila: UNBOUNDED PRECEDING = desde la primera fila, n PRECEDING / n FOLLOWING = n filas antes o después, CURRENT ROW, UNBOUNDED FOLLOWING = hasta la última fila. ROWS cuenta filas físicas; RANGE agrupa filas con el mismo valor de ORDER BY. LAG y LEAD leen una fila a cierta distancia sin self-join.
- Cuidado con
- ORDER BY sin marco explícito usa RANGE … CURRENT ROW por defecto, así que las filas con la misma fecha se suman juntas: escribe el marco. 2 PRECEDING son tres filas, no tres días (une un calendario primero si faltan días). LAG en la primera fila es NULL: envuelve las divisiones en NULLIF. LAST_VALUE necesita … AND UNBOUNDED FOLLOWING o devuelve la fila actual.
- Ejemplo
- -- amounts 100, 200, 150, 300 → running total 100, 300, 450, 750 WITH m AS (SELECT DATE_TRUNC('MONTH', order_date) AS month, SUM(amount) AS revenue FROM ORDERS GROUP BY 1) SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS previous_month, ROUND((revenue - LAG(revenue) OVER (ORDER BY month)) / NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100, 2) AS growth_pct FROM m;
Snowflake
VARIANT y FLATTEN
De JSON a filas
- Sintaxis
SELECT data:customer_id::NUMBER AS customer_id, f.value:id::NUMBER AS product_id, f.value:price::NUMBER AS price FROM RAW_ORDERS, LATERAL FLATTEN(INPUT => data:products) f; PARSE_JSON(raw_text) -- string → VARIANT ARRAY_AGG(DISTINCT product_id) -- rows → array LISTAGG(product_id, ', ') WITHIN GROUP (ORDER BY product_id) -- rows → string- Cuándo usarlo
- Fuentes semiestructuradas (eventos, respuestas de API) cargadas como VARIANT; construir arrays, objetos o cadenas por grupo para la salida.
- Qué hace
- data:field lee una clave y :: la convierte de tipo. FLATTEN convierte cada elemento del array en una fila unida a su padre (LATERAL), así que se aplican agregados y joins normales. OBJECT_AGG y ARRAY_AGG hacen el camino inverso.
- Cuidado con
- Las claves de la ruta distinguen mayúsculas. Una clave ausente devuelve NULL, no un error: usa COALESCE. FLATTEN sobre un array vacío no produce filas salvo con OUTER => TRUE; RECURSIVE => TRUE recorre los niveles anidados.
- Ejemplo
- SELECT data:customer_id::NUMBER AS customer_id, SUM(f.value:price::NUMBER) AS total FROM RAW_ORDERS, LATERAL FLATTEN(INPUT => data:products) f GROUP BY 1;
Snowflake
MERGE, PIVOT, GENERATOR
Upsert y remodelar
- Sintaxis
MERGE INTO ORDERS t USING (SELECT * FROM STAGING_ORDERS QUALIFY ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY updated_at DESC) = 1) s ON t.order_id = s.order_id WHEN MATCHED THEN UPDATE SET t.amount = s.amount, t.status = s.status WHEN NOT MATCHED THEN INSERT (order_id, amount, status) VALUES (s.order_id, s.amount, s.status); SELECT * FROM (SELECT customer_id, status, amount FROM ORDERS) PIVOT (SUM(amount) FOR status IN ('completed', 'pending', 'cancelled')); SELECT DATEADD(DAY, SEQ4(), '2026-01-01'::DATE) AS day FROM TABLE(GENERATOR(ROWCOUNT => 365));- Cuándo usarlo
- Cargas incrementales (upsert con deduplicación), una columna por estado, tablas de calendario para rellenar días sin datos, muestreo de una tabla grande antes de una ejecución completa.
- Qué hace
- MERGE empareja origen y destino por una clave y actualiza o inserta en una sola sentencia. PIVOT convierte los valores de una columna en columnas; UNPIVOT hace lo contrario. GENERATOR crea filas de la nada; SAMPLE devuelve un subconjunto aleatorio.
- Cuidado con
- Deduplica el origen antes del MERGE o fallará cuando una fila destino coincida con varias filas origen. PIVOT exige la lista de valores escrita (o un pivot dinámico). SAMPLE BERNOULLI(10) devuelve aproximadamente el 10 % de las filas, no exactamente.
- Ejemplo
- WITH calendar AS (SELECT DATEADD(DAY, SEQ4(), '2026-01-01'::DATE) AS day FROM TABLE(GENERATOR(ROWCOUNT => 365))), daily AS (SELECT order_date::DATE AS day, SUM(amount) AS revenue FROM ORDERS GROUP BY 1) SELECT c.day, COALESCE(d.revenue, 0) AS revenue FROM calendar c LEFT JOIN daily d ON d.day = c.day ORDER BY c.day;
Elige un patrón en el mapa para ver su sintaxis, cuándo usarlo, qué hace y con qué tener cuidado.
Quiz de práctica
20 preguntas tipo test elegidas al azar entre las 51 de este tema, una por pantalla, corregidas al final. Cada pregunta remite a un patrón del mapa.
Elige una respuesta, muévete libremente entre las preguntas y termina cuando quieras: la puntuación y la explicación de cada pregunta aparecen al final.
- 20 preguntas
- Sin límite de tiempo
- Corrección al final
El quiz necesita JavaScript. La chuleta de arriba funciona sin él.
Material de práctica para pruebas de Data Engineer y Analytics Engineer en Snowflake. La sintaxis sigue la referencia SQL de Snowflake; las tablas ORDERS (order_id, customer_id, product_id, amount, status, order_date) y EMPLOYEES son ficticias.