QUALIFY y funciones de ventana en Teradata (con ejemplos)
Las funciones de ventana (OLAP) son una de las herramientas más potentes de SQL: permiten calcular rankings, acumulados y comparaciones entre filas sin colapsar el resultado como hace un GROUP BY. Teradata además tiene una cláusula exclusiva, QUALIFY, que simplifica muchísimo los casos de "top-N por grupo". Vamos con ejemplos concretos.
La sintaxis base: OVER (PARTITION BY ... ORDER BY ...)
Toda función de ventana se apoya en la cláusula OVER, que define sobre qué conjunto de filas opera:
funcion_ventana() OVER (
PARTITION BY col_grupo
ORDER BY col_orden
)
- PARTITION BY: define los "grupos" dentro de los cuales se reinicia el cálculo — es conceptualmente parecido a un
GROUP BY, pero sin colapsar las filas: cada fila original se conserva en el resultado. - ORDER BY: define el orden dentro de cada partición, necesario para funciones como
ROW_NUMBER,RANKo acumulados.
ROW_NUMBER, RANK y DENSE_RANK
Las tres numeran filas dentro de una partición, pero se comportan distinto ante empates:
| Función | Comportamiento ante empates |
|---|---|
ROW_NUMBER() | Siempre único, sin importar empates (1, 2, 3, 4...) |
RANK() | Empates comparten posición y se saltan números (1, 2, 2, 4...) |
DENSE_RANK() | Empates comparten posición sin saltos (1, 2, 2, 3...) |
Ejemplo: numerar los pedidos de cada cliente por fecha:
SELECT
customer_id,
order_date,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date) AS rn
FROM orders;
PARTITION BY customer_id reinicia el contador en cada cliente, y ORDER BY order_date define la cronología. ROW_NUMBER() nunca repite un número, incluso si dos pedidos tienen exactamente la misma fecha.
QUALIFY: la cláusula exclusiva de Teradata
Acá está la joya de este nivel. QUALIFY es una cláusula exclusiva de Teradata (no forma parte del estándar SQL ni existe en PostgreSQL o MySQL) que permite filtrar directamente sobre el resultado de una función de ventana, igual que WHERE filtra filas y HAVING filtra grupos agregados.
Sin QUALIFY, para quedarte con el pedido más caro de cada cliente necesitarías un CTE y un filtro externo:
-- Sin QUALIFY: hace falta envolver en un CTE
WITH numerado AS (
SELECT c.*, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY revenue DESC) AS rn
FROM pedidos c
)
SELECT * FROM numerado WHERE rn = 1;
Con QUALIFY, la misma lógica se resuelve en una sola consulta:
-- Con QUALIFY: una sola consulta, sin CTE de filtrado
SELECT c.*, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY revenue DESC) AS rn
FROM pedidos c
QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY revenue DESC) = 1;
Aunque técnicamente es más simple igualmente calcular el ranking en un CTE (WITH) primero y aplicar QUALIFY sobre ese CTE, para mantener la consulta legible cuando hay varias funciones de ventana involucradas.
Top-N por grupo con RANK y QUALIFY
Para quedarte con los 5 clientes de mayor gasto total:
SELECT
customer_id,
RANK() OVER (ORDER BY total_spend DESC) AS rnk
FROM clientes_totales
QUALIFY RANK() OVER (ORDER BY total_spend DESC) <= 5;
O el producto más caro de cada categoría, usando DENSE_RANK para no saltar posiciones ante empates de precio:
SELECT
category,
product_name,
unit_price,
DENSE_RANK() OVER (PARTITION BY category ORDER BY unit_price DESC) AS price_rank
FROM products
QUALIFY DENSE_RANK() OVER (PARTITION BY category ORDER BY unit_price DESC) = 1;
Acumulados: SUM() OVER con ROWS BETWEEN
Las funciones de ventana también sirven para calcular totales acumulados (running totals) sin perder el detalle de cada fila:
SELECT
customer_id,
order_date,
revenue,
SUM(revenue) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM pedidos;
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW le dice al motor "sumá desde la primera fila de la partición hasta la fila actual", que es la definición exacta de un acumulado.
Comparar con la fila anterior o siguiente: LAG y LEAD
LAG y LEAD permiten traer el valor de una fila anterior o posterior dentro de la misma partición, sin necesidad de un self-join:
-- Variación de ingresos respecto al pedido anterior del mismo cliente
SELECT
customer_id,
order_date,
revenue,
revenue - LAG(revenue) OVER (PARTITION BY customer_id ORDER BY order_date, order_id) AS variation
FROM pedidos;
-- Fecha del próximo pedido de cada cliente
SELECT
customer_id,
order_date,
LEAD(order_date) OVER (PARTITION BY customer_id ORDER BY order_date, order_id) AS next_order_date
FROM pedidos;
NTILE: dividir en cuantiles
NTILE(n) reparte las filas de una partición en n grupos de tamaño lo más parejo posible — típicamente para calcular cuartiles o deciles:
SELECT
customer_id,
total_spend,
NTILE(4) OVER (ORDER BY total_spend DESC) AS quartile
FROM clientes_totales
QUALIFY NTILE(4) OVER (ORDER BY total_spend DESC) = 1;
Esta consulta aísla el 25% de clientes con mayor gasto total: el primer cuartil.
Por qué QUALIFY vale la pena aprenderlo bien
Más allá de ahorrar líneas de código, QUALIFY es representativo de cómo Teradata resuelve problemas analíticos comunes de forma más directa que el SQL estándar. Es una de esas cláusulas que, una vez que la interiorizás, cambia cómo pensás las consultas de top-N, deduplicación y ranking.
En el dojo, el Nivel 6 — Funciones OLAP/ventana y QUALIFY tiene ejercicios progresivos que van desde numerar filas simples hasta combinar QUALIFY con múltiples funciones de ventana, todo ejecutable en el navegador.
Seguí aprendiendo
Si todavía no viste cómo Teradata distribuye los datos entre AMPs (clave para entender por qué estas consultas escalan bien), te recomendamos ¿Qué es la arquitectura MPP de Teradata?. Y si venís de otro motor, revisá Teradata vs PostgreSQL y MySQL para ver dónde encaja QUALIFY frente a lo que ya conocés.
Practicá QUALIFY con ejercicios reales que se ejecutan en el navegador. Dojo completo por 5 USD.