Funciones de ventana
En resumen: Las funciones de ventana con OVER y PARTITION BY calculan sobre un grupo de filas sin colapsarlas: rankings, totales acumulados o la fila anterior con LAG.
Problema
El promedio, pero también las filas
El precio promedio de cada categoría ya lo obtienes con GROUP BY:
Pero la gerencia quiere una tabla con todos los productos y, junto a cada uno, el promedio de su categoría, para ver cuánto se aparta cada uno. GROUP BY ha aplastado los productos: de 25 filas quedan 7. En la lección sobre subconsultas correlacionadas lo hiciste con una subconsulta para cada fila. Hay un camino más corto.
La idea
OVER y PARTITION BY
AVG(price) OVER (PARTITION BY category_id) calcula el promedio sobre el grupo de la fila pero no aplasta las filas: están los 25 productos, cada uno con el promedio de su propia categoría.
OVER (…) dice sobre qué filas se calcula. Ese grupo de filas se llama ventana, y una función con OVER se llama función de ventana. PARTITION BY divide las filas en grupos, como GROUP BY, pero sin fusionarlas.
Una diferencia con la subconsulta correlacionada: para la tarjeta de regalo el promedio es 20 €, no NULL. PARTITION BY mete todos los NULL en un grupo, como hace GROUP BY, mientras que p2.category_id = p.category_id no era verdadero para NULL.
La idea
Un puesto en un ranking
ROW_NUMBER() da el número de la fila en la ventana. Con ORDER BY en OVER indicas cómo se ordenan las filas:
En cada categoría la numeración empieza de nuevo, por el producto más caro: el producto más caro de «Cocina» (categoría 8) es el juego de cuchillos, con el número 1.
El paso natural siguiente es WHERE place = 1, para quedarte solo con los primeros. Pero:
Las funciones de ventana se calculan después de WHERE: primero se eligen las filas y luego se organizan en ventanas. Por eso el ranking va en un paso, y el filtro va fuera.
Pruébalo
El más caro de cada categoría
Muestra para cada categoría su nombre (columna category), su producto más caro y el precio (columnas name y price). El campo ya numera los productos. Ponlo en WITH y toma solo los primeros.
Solución
WITH ranked AS (
SELECT name, category_id, price,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS place
FROM products
)
SELECT c.name AS category, r.name, r.price
FROM ranked r
JOIN categories c ON c.id = r.category_id
WHERE r.place = 1;WITH ranked AS (…), fuera WHERE r.place = 1 y JOIN categories para el nombre. Seis filas, una por subcategoría. La tarjeta de regalo no tiene categoría y JOIN la deja fuera. Este es el patrón para «los primeros N de cada grupo»: numerar en una ventana, filtrar fuera.
Míralo
Puestos compartidos
Ahora un ranking por unidades vendidas en «Pan y bollería» (categoría 3), con las tres funciones una al lado de otra:
ROW_NUMBERda números distintos incluso cuando hay empate. Cuál de los dos va primero lo decide la segunda ordenación (aquí el nombre). Sin ella, no se promete nada.RANKda el mismo puesto a las filas empatadas y luego se salta: 1, 1, 3.DENSE_RANKda el mismo puesto y no se salta nada: 1, 1, 2.
Para «los 3 mejores productos», elige a propósito: con ROW_NUMBER salen exactamente tres, con RANK salen todos los que comparten un puesto.
La idea
Total acumulado
Con ORDER BY en OVER, la ventana de cada fila es «desde el principio hasta aquí». Cuánto hemos vendido desde el 1 de septiembre hasta cada día (sin los pedidos cancelados):
running_total crece fila a fila: el 3 de octubre llega a 415,30 € y el 19 de octubre es de 480,30 €. Esto se llama total acumulado (running total).
Más precisamente, para curiosos
Si dos filas tienen el mismo valor en ORDER BY, por defecto las dos reciben la suma incluyendo a la otra. Aquí cada día es una fila, así que no puede pasar. Si ordenas pedidos por fecha y un día tiene dos pedidos, añade una segunda ordenación, como ORDER BY ordered_at, id, o escribe explícitamente ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.
Pruébalo
Cuántos clientes tuvimos
Muestra para cada cliente el nombre, la fecha de registro y cuántos clientes tenía la tienda en esa fecha, incluyéndolo a él (columna customers_so_far).
Solución
SELECT name, joined_at,
COUNT(*) OVER (ORDER BY joined_at) AS customers_so_far
FROM customers;COUNT(*) OVER (ORDER BY joined_at): de 1 para Maria a 20 para Yavor. Dos clientes con la misma fecha recibirían el mismo número, pero aquí todas las fechas son distintas.
La idea
LAG: la fila anterior
LAG(column) da el valor de la fila anterior de la ventana. ¿Cuántos días pasan entre los pedidos de Maria?
previous es la fecha de su pedido anterior. julianday convierte una fecha en un número de día, así que la diferencia es el número de días entre los dos pedidos. El primer pedido no tiene anterior y recibe NULL. La función contraria es LEAD: la fila siguiente.
Ejercicio
Dos tareas
1. El escaparate del sitio web muestra dos productos de cada categoría, los más caros.
Muestra para cada categoría su nombre (columna category) y sus dos productos más caros con sus precios (columnas name y price).
Solución
WITH ranked AS (
SELECT name, category_id, price,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS place
FROM products
)
SELECT c.name AS category, r.name, r.price
FROM ranked r
JOIN categories c ON c.id = r.category_id
WHERE r.place <= 2;Lo mismo que para el más caro, pero con WHERE r.place <= 2. 12 filas, dos de cada una de las seis subcategorías.
2. Marketing quiere saber cuál es la espera más larga de cada cliente entre dos de sus pedidos.
Muestra para cada cliente con al menos dos pedidos el nombre y la pausa más larga en días entre dos de sus pedidos consecutivos (columna longest_pause).
Solución
WITH gaps AS (
SELECT customer_id,
julianday(ordered_at) - julianday(LAG(ordered_at) OVER (PARTITION BY customer_id ORDER BY ordered_at)) AS days
FROM orders
)
SELECT c.name, MAX(g.days) AS longest_pause
FROM gaps g
JOIN customers c ON c.id = g.customer_id
WHERE g.days IS NOT NULL
GROUP BY c.id, c.name;En un paso: LAG(ordered_at) OVER (PARTITION BY customer_id ORDER BY ordered_at) y la diferencia con julianday. Fuera, MAX(g.days) por cliente, sin los NULL de los primeros pedidos. 17 clientes. Kalin tiene un solo pedido, así que no está. Nikolay esperó más: 314 días.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: JOIN: dos tablas en una consulta · Subconsultas · Subconsultas correlacionadas · Un informe por pasos: WITH
- Continúa en: Flujos de eventos · Bases de datos columnares y series temporales · La panadería, etapa 6: el informe, la copia y DROP
- Ejercicios sobre el tema: Subconsultas y ventanas