Pregunta 1Dos empleados están empatados en el 5° lugar con el mismo total. ¿Qué rango obtiene la siguiente persona bajo RANK() vs. DENSE_RANK()?
Funciones de ventana ③ — Rangos, funciones analíticas y orden de ejecución
Recorre cómo ROW_NUMBER / RANK / DENSE_RANK manejan los empates, cómo LAG / LEAD traen valores de la fila anterior / siguiente, cómo NTILE(4) divide las filas en n grupos, y por qué tienes que usar una subconsulta para filtrar por un rango, todo con datos de ventas.
Rangos — ROW_NUMBER / RANK / DENSE_RANK
Las funciones de rango dedicadas son ROW_NUMBER() / RANK() / DENSE_RANK().
Las tres numeran filas en el orden dado por OVER (ORDER BY ...), pero manejan los empates (valores iguales) de forma distinta.
| Función | Cómo se manejan los empates (valores iguales) |
|---|---|
ROW_NUMBER() | Fuerza numeración consecutiva incluso con empates (siempre 1, 2, 3, ...) |
RANK() | Los empates comparten el mismo rango, luego el siguiente rango salta por cuantos se hayan empatado (2 empatados en 5° → el siguiente es 7) |
DENSE_RANK() | Los empates comparten el mismo rango, pero el siguiente rango no salta (la fila después de un empate en 5° es 6) |
El SQL de abajo, y el diagrama que visualiza su resultado, recorren un caso concreto con totales empatados fila a fila.
-- Calcula el total de ventas de cada empleado y luego clasifica en orden descendente de total de 3 formas
SELECT emp_id,
SUM(amount) AS total,
ROW_NUMBER() OVER (ORDER BY SUM(amount) DESC) AS rn,
RANK() OVER (ORDER BY SUM(amount) DESC) AS rk,
DENSE_RANK() OVER (ORDER BY SUM(amount) DESC) AS dr
FROM sales
GROUP BY emp_id
ORDER BY total DESC;
-- Puedes agregar primero con GROUP BY y luego clasificar esas agregaciones
LAG / LEAD — trae valores de la fila anterior / siguiente
LAG(column) trae el valor de la fila anterior a la fila actual, y LEAD(column) trae el de la fila siguiente (lag = quedarse atrás, lead = ir adelante).
OVER (PARTITION BY ... ORDER BY ...) decide qué significan "anterior" y "siguiente" estableciendo el orden.
Úsalas para diferencias periodo contra periodo (vs. la venta anterior), deltas vs. el último mes, o comparaciones contra el siguiente evento agendado.
LAG(amount) devuelve NULL en la primera fila de cada partición (no hay fila anterior); de manera similar LEAD es NULL en la última fila.
El segundo argumento opcional controla el desplazamiento ("cuántas filas atrás") y el tercero establece un valor por defecto cuando la fila objetivo falta: por ejemplo, LAG(amount, 1, 0).
Resta con amount - LAG(amount) OVER (...) para obtener directamente la diferencia vs. la fila anterior.
-- Muestra la venta anterior (LAG) y la diferencia vs. la venta anterior
SELECT emp_id, sale_date, amount,
LAG(amount) OVER (PARTITION BY emp_id ORDER BY sale_date) AS prev_amount,
amount - LAG(amount) OVER (PARTITION BY emp_id ORDER BY sale_date) AS diff_prev
FROM sales
ORDER BY emp_id, sale_date;
-- Usa LEAD si quieres mostrar la siguiente venta junto a la fila actual
SELECT emp_id, sale_date, amount,
LEAD(amount) OVER (PARTITION BY emp_id ORDER BY sale_date) AS next_amount
FROM sales
ORDER BY emp_id, sale_date;
NTILE — divide filas ordenadas en n grupos del mismo tamaño
NTILE(n) divide las filas ordenadas por OVER (ORDER BY ...) en n grupos del tamaño más parecido posible, etiquetando cada fila con un número de grupo de 1 a n.
Úsalo para cuartiles, quintiles, "top 25%" y agrupamientos similares.
Cuando el número de filas no es divisible por n, los grupos anteriores reciben una fila extra cada uno (por ejemplo, dividir 5 filas en 4 grupos da 2 / 1 / 1 / 1).
Combínalo con PARTITION BY para hacer cosas como "dividir las ventas de cada empleado en cubos superiores / inferiores dentro de ese empleado".
El diagrama de abajo muestra qué sucede cuando las 5 ventas de emp_id=11 se ordenan por amount desc y se dividen con NTILE(4).
-- Por empleado, divide las ventas de ese empleado en 4 grupos por amount desc
SELECT emp_id, sale_date, amount,
NTILE(4) OVER (PARTITION BY emp_id ORDER BY amount DESC) AS quartile
FROM sales
ORDER BY emp_id, amount DESC;
-- Divide todas las ventas en 4 cubos por amount (el top 25% tiene quartile = 1)
SELECT sale_id, emp_id, amount,
NTILE(4) OVER (ORDER BY amount DESC) AS quartile
FROM sales
ORDER BY amount DESC;
Orden de ejecución de SQL — por qué no puedes poner una función de ventana en WHERE
Te puedes preguntar: "¿no puedo simplemente filtrar por el rango que calculé, como WHERE rk = 1?"
Obtendrías un error. La razón es el orden lógico de ejecución de SQL.
El orden en que escribes las cláusulas (SELECT → FROM → WHERE ...) no es el orden en que se evalúan.
La evaluación va aproximadamente: FROM / JOIN (ensamblar tablas) → WHERE (filtrar filas) → GROUP BY (agrupar) → HAVING (filtrar grupos) → SELECT (calcular columnas: aquí corren las funciones de ventana) → ORDER BY (ordenar) → LIMIT (truncar).
Como WHERE corre antes de SELECT, no puede ver el rango que SELECT está a punto de calcular.
Para filtrar por un rango, empuja la consulta que calcula la función de ventana a una subconsulta (o CTE) y aplica WHERE por fuera.
El SELECT interno termina de calcular la función de ventana, y la consulta externa puede entonces tratar ese resultado como una columna más.
-- Esto da error: WHERE corre antes de SELECT, así que rk aún no existe
-- SELECT emp_id, RANK() OVER (ORDER BY SUM(amount) DESC) AS rk
-- FROM sales GROUP BY emp_id WHERE rk <= 3;
-- Correcto: calcula el rango en una subconsulta interna, filtra por fuera
SELECT * FROM (
SELECT emp_id,
SUM(amount) AS total,
RANK() OVER (ORDER BY SUM(amount) DESC) AS rk
FROM sales
GROUP BY emp_id
) AS ranked
WHERE rk <= 3
ORDER BY rk, emp_id;
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2¿Qué devuelve LAG(amount) OVER (PARTITION BY emp_id ORDER BY sale_date) en la primera fila de venta de cada empleado?
Pregunta 3¿Por qué SELECT emp_id, RANK() OVER (ORDER BY SUM(amount) DESC) AS rk FROM sales GROUP BY emp_id WHERE rk <= 3; no funciona?