La idea
GROUP BY responde una pregunta sobre un grupo y te da una fila por grupo. Una función de
ventana responde el mismo tipo de pregunta pero pone la respuesta en cada fila del grupo. Mira un
conjunto de filas relacionadas, calcula algo y deja las filas donde están.
La cláusula OVER (…) define ese conjunto de filas, la ventana. Tres partes hacen el trabajo:
PARTITION BY divide las filas en grupos, ORDER BY pone cada grupo en secuencia, y el marco
dice cuáles de esas filas están a la vista para la fila actual.
Figura 1
GROUP BY pliega las filas; OVER las conserva todas
| order_item_id | order_id | line_total |
|---|---|---|
| 139 | 1049 | 378.00 |
| 140 | 1049 | 107.98 |
| 141 | 1049 | 1137.00 |
| 601 | 1231 | 93.56 |
| 602 | 1231 | 294.30 |
| order_id | order_total |
|---|---|
| 1049 | 1622.98 |
| 1231 | 387.86 |
| order_item_id | line_total | order_total |
|---|---|---|
| 139 | 378.00 | 1622.98 |
| 140 | 107.98 | 1622.98 |
| 141 | 1137.00 | 1622.98 |
| 601 | 93.56 | 387.86 |
| 602 | 294.30 | 387.86 |
- Cinco líneas de pedido que pertenecen a dos pedidos. Queremos el total de cada pedido.
GROUP BY order_idpliega las líneas de cada pedido en una fila. Los totales son correctos, pero el detalle de las líneas desaparece: cinco filas se volvieron dos.OVER (PARTITION BY order_id)también agrupa por pedido, pero solo para calcular la suma. La partición del pedido 1049 son sus tres líneas, que suman 1,622.98.- La respuesta se escribe en cada fila de la partición. Las cinco filas sobreviven, así que aún puedes mostrar la línea, el total de su pedido y, con un paso más, la parte que representa.
Por qué existe
Algunas preguntas necesitan el detalle y el grupo a la vez. "Cada producto junto al mejor precio de su
categoría." "Cada línea de pedido como parte de su pedido." "Los pedidos de este mes junto a los del
mes pasado." Solo con GROUP BY agregas en una consulta y unes el resultado de vuelta al detalle en
otra. Funciona, pero es largo, y es fácil equivocarse con la clave del join.
Otras preguntas son sobre el orden, que GROUP BY ignora: totales acumulados, rankings, "el segundo
pedido del cliente", la distancia entre un pedido y el anterior. Una función de ventana hace esto en
una sola pasada sobre las filas, y por eso las entrevistas de analista insisten tanto en ellas.
Cómo funciona por dentro
Para cada fila, la base de datos hace las mismas cuatro cosas.
- Particionar. Busca las filas del mismo grupo
PARTITION BYque esta fila. SinPARTITION BY, todo el resultado es una sola partición. - Ordenar. Ordena esa partición con el
ORDER BYde la ventana. - Marco. Elige qué filas alrededor de la actual están a la vista.
- Calcular. Ejecuta la función sobre las filas del marco y escribe la respuesta en la fila actual.
El marco es donde vive la mayor parte del poder, y de la confusión.
Figura 2
El marco se desliza por las filas ordenadas
| month | orders | running_total | avg_3_months |
|---|---|---|---|
| 2023-01 | 192 | 192 | 192.0 |
| 2023-02 | 157 | 349 | 174.5 |
| 2023-03 | 191 | 540 | 180.0 |
| 2023-04 | 216 | 756 | 188.0 |
| 2023-05 | 218 | 974 | 208.3 |
| 2023-06 | 189 | 1163 | 207.7 |
- Los meses en orden. Para cada fila la ventana decide qué filas están a la vista, el marco, y la función trabaja solo con ellas.
- Total acumulado:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. En marzo el marco va de enero a marzo, y 192 + 157 + 191 = 540. - Una fila después, el mismo marco suma abril: 540 + 216 = 756. El inicio queda fijo y el final se mueve con la fila actual.
- Promedio móvil:
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW. En mayo el marco son solo marzo, abril y mayo, y (191 + 216 + 218) / 3 = 208.3. - En junio el marco avanza: febrero sale y junio entra. Se mueven los dos extremos, así que la ventana mantiene su tamaño de tres filas.
Si la ventana tiene ORDER BY y no tiene un marco explícito, el marco por defecto va desde el inicio
de la partición hasta la fila actual (e incluye las filas empatadas con ella). Por eso SUM(x) OVER (ORDER BY month) da un total acumulado, mientras que SUM(x) OVER () da el total general en cada
fila. Para un promedio móvil, escribe tú el marco: ROWS BETWEEN 6 PRECEDING AND CURRENT ROW es una
ventana de 7 filas. Ten en cuenta que cuenta filas, no días, así que un día que falte estira la
ventana sin avisar.
Leer una ventana en una frase
Di la cláusula OVER en voz alta y se vuelve una frase. SUM(line_total) OVER (PARTITION BY order_id) se lee "suma los totales de línea, dentro de cada pedido". LAG(orders) OVER (PARTITION BY channel ORDER BY month) se lee "los pedidos del mes anterior, dentro de cada canal". Si no puedes
decir quién es ese "dentro de cada", a la ventana le falta un PARTITION BY, y los números van a
mezclar grupos que deberían estar separados.
Por qué el marco cuenta filas y no tiempo
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW significa "esta fila y las dos anteriores", sean cuales
sean. En el diagrama son tres meses solo porque cada mes tiene una fila. Si un mes no tuviera pedidos
y ninguna fila, el mismo marco llegaría cuatro meses atrás y promediaría en silencio el periodo
equivocado. La solución habitual es construir primero un calendario completo, con una fila por cada
mes o día, y unir tus datos a él antes de aplicar la ventana.
Los porcentajes salen solos. La parte que una línea representa de su pedido es line_total / SUM(line_total) OVER (PARTITION BY order_id): el detalle arriba, el total de la partición abajo, sin
necesidad de volver a unir.
Tres familias de funciones
Agregados sobre una ventana: SUM, AVG, COUNT, MIN, MAX. Las mismas funciones que ya
conoces, ahora con marco.
Ranking: ROW_NUMBER, RANK, DENSE_RANK, NTILE. Dependen solo del orden, y los empates son
lo que las distingue.
Figura 3
Tres rankings, un empate
| name | unit_price | row_number | rank | dense_rank |
|---|---|---|---|---|
| Pacifica Black Metal Table Lamp | 409.00 | 1 | 1 | 1 |
| Fern Rattan Desk Lamp | 379.00 | 2 | 2 | 2 |
| Marlow Glass Wall Sconce | 349.00 | 3 | 3 | 3 |
| Ashford Linen Pendant Light | 349.00 | 4 | 3 | 3 |
| Quarry Ceramic Picture Light | 329.00 | 5 | 5 | 4 |
- Cinco productos de Lighting ordenados por precio. Dos de ellos, el aplique y la lámpara colgante, cuestan exactamente $349.
ROW_NUMBERnunca empata. Numera las filas del 1 al 5, y cuál de los productos empatados recibe el 3 y cuál el 4 es arbitrario si no agregas un desempate en elORDER BY.RANKda el mismo número a las filas empatadas y luego salta: 3, 3 y después 5. Un "top 3" conRANKdevuelve aquí cuatro productos.DENSE_RANKda el mismo número a las filas empatadas y no salta: 3, 3 y después 4. Elige según lo que la pregunta diga sobre los empates.
Navegación: LAG y LEAD leen un valor de la fila anterior o siguiente de la partición.
LAG(revenue) OVER (ORDER BY month) es el ingreso del mes pasado junto al de este mes, y el primer
mes recibe NULL porque no hay nada antes. FIRST_VALUE y LAST_VALUE leen los extremos del marco.
Dónde se ejecuta la ventana
Las funciones de ventana se ejecutan después de WHERE, GROUP BY y HAVING, y antes del ORDER BY
final y del LIMIT del SELECT. Dos consecuencias. No puedes filtrar por el resultado de una ventana
en el WHERE de la misma consulta, así que "top 3 por categoría" es un CTE o subconsulta que calcula
ROW_NUMBER y una consulta externa que conserva rn <= 3. Y una ventana sobre una consulta agregada
ve las filas ya agrupadas: SUM(SUM(revenue)) OVER (ORDER BY month) es un total acumulado de totales
mensuales.
Errores comunes
"OVER (PARTITION BY x) es lo mismo que GROUP BY x"
El agrupamiento es el mismo, el resultado no. GROUP BY devuelve una fila por grupo. Una ventana
devuelve todas las filas de entrada. Si tu resultado tiene menos filas de las que esperabas, algo las
colapsó; si tiene el mismo número, tu ventana las dejó en paz.
"LAG toma la fila anterior, así que funciona sin PARTITION BY"
Toma la fila anterior en el orden de la ventana, entre todo lo que haya en la partición. Con varios
clientes en un mismo resultado y sin PARTITION BY customer_id, el primer pedido de un cliente se
compara con el último pedido del cliente anterior. Cuando compares con "antes", pregunta antes para
quién.
"Los empates no importan si uso ROW_NUMBER"
Importan porque ROW_NUMBER los esconde: rompe el empate de forma arbitraria, y el resultado puede
cambiar entre ejecuciones. Agrega una columna de desempate al ORDER BY para obtener una respuesta
estable, o usa RANK o DENSE_RANK cuando las filas empatadas deban compartir lugar.
Ponte a prueba
Una pregunta. Al responderla, la lección queda marcada en el mapa; nunca bloquea un nivel.
No del todo. Eso es GROUP BY. Una función de ventana nunca elimina filas; agrega un valor a cada fila existente.Prueba otra respuesta.
Correcto. La partición es el pedido 1231, así que la suma cubre sus dos líneas (93.56 + 294.30) y se escribe en ambas filas.
No del todo. 93.56 sería la suma si cada fila fuera su propia partición. PARTITION BY order_id junta las líneas de un mismo pedido.Prueba otra respuesta.
No del todo. Eso requeriría no usar PARTITION BY. Con PARTITION BY order_id la suma se reinicia en cada pedido.Prueba otra respuesta.
Lección hecha: ya está marcada en el mapa.