DavidGadyan

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.

  1. 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;
  2. 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;
  3. 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;
  4. 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;
  5. 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;
  6. 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;
  7. 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;
  8. 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
  9. 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
  10. 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;
  11. 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;
  12. 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;
  13. 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.