Un informe por pasos: WITH
En resumen: WITH (common table expression, CTE) le da un nombre a cada paso de una consulta para que se lea de arriba abajo y un paso pueda usarse más de una vez.
Problema
Un informe de dentro hacia fuera
El dueño quiere una lista de los clientes que han gastado más de 100 € (sin los pedidos cancelados), con su cantidad de pedidos. Con subconsultas en FROM ya puedes hacerlo:
El resultado es correcto. Pero intenta decir qué hace la consulta: tienes que empezar por los paréntesis más internos y avanzar hacia fuera. Dentro de un mes ni tú mismo la reconocerás.
La idea
WITH
WITH le da un nombre a un paso, antes de la consulta principal:
order_totals es el valor de cada pedido que no está cancelado: 53 filas. Después de WITH, el nombre se usa como una tabla. Un paso con nombre así se llama CTE (common table expression).
Puede haber varios pasos, separados por comas. Cada uno ve los pasos anteriores:
El mismo informe del principio, pero se lee de arriba abajo: el valor de cada pedido, luego un total por cada cliente, luego solo los que pasan de 100 €. Salen siete clientes, encabezados por Maria (375,70 € en 12 pedidos).
Pruébalo
Pedidos por encima del promedio
Muestra el número y el valor de los pedidos (sin los cancelados) cuyo valor está por encima del valor promedio de un pedido. El campo ya tiene el paso order_totals.
Solución
WITH order_totals AS (
SELECT o.id, o.customer_id, SUM(oi.quantity * oi.unit_price) AS total
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status <> 'cancelled'
GROUP BY o.id, o.customer_id
)
SELECT id, ROUND(total, 2) AS total
FROM order_totals
WHERE total > (SELECT AVG(total) FROM order_totals);WHERE total > (SELECT AVG(total) FROM order_totals). Salen 22 pedidos. Aquí ves una ventaja más: order_totals se usa dos veces, una en FROM y otra en la subconsulta. Con una subconsulta en FROM lo habrías escrito dos veces.
Míralo
Cuánto vive el nombre
Un nombre de WITH vale solo para la consulta de la que forma parte. Aquí hay dos consultas: la primera cuenta los pedidos, la segunda vuelve a pedir order_totals:
WITH no crea una tabla en la base de datos. Si necesitas un paso en muchos informes, para eso están las vistas (VIEW), que conoceremos en el módulo sobre el esquema.
La idea
Cuándo WITH, cuándo una subconsulta
El resultado es el mismo. La diferencia es para quien lee:
- una condición corta como
price > (SELECT AVG(price) FROM products)es más clara en su sitio, como subconsulta; - un informe de varios pasos es más claro con
WITH: cada paso tiene un nombre que dice qué es; - un paso que necesitas dos veces se escribe una vez.
También ayuda al buscar un error: cambia el último SELECT por SELECT * FROM order_totals y verás qué da cada paso.
Más precisamente, para curiosos
Si la base de datos ejecuta cada paso por separado o los fusiona en una sola consulta lo decide la base de datos. En PostgreSQL antes de la versión 12 cada WITH se ejecutaba por separado, y desde la versión 12 la base puede fusionarlo con la consulta principal. Si alguna vez una consulta con WITH es más lenta que la misma con subconsultas, la razón suele estar ahí.
Ejercicio
Tres tareas
1. El equipo de ventas quiere los ingresos por ciudad, para decidir dónde anunciarse.
Muestra la ciudad y los ingresos de los pedidos de sus clientes (sin los cancelados), redondeados a dos decimales. Empieza por el paso order_totals.
Solución
WITH order_totals AS (
SELECT o.id, o.customer_id, SUM(oi.quantity * oi.unit_price) AS total
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status <> 'cancelled'
GROUP BY o.id, o.customer_id
)
SELECT c.city, ROUND(SUM(ot.total), 2) AS revenue
FROM order_totals ot
JOIN customers c ON c.id = ot.customer_id
GROUP BY c.city;JOIN customers con order_totals y GROUP BY c.city. Siete filas: Sofía encabeza con 658,70 €. Boris no tiene ciudad, así que una fila tiene NULL como ciudad (66,60 €). No la pierdas: es dinero de verdad.
2. ¿Qué categorías han aportado más de 200 € (sin los pedidos cancelados)?
Muestra el nombre de la categoría y sus ingresos (la suma de cantidad por precio unitario), solo para las categorías con ingresos de más de 200 €. Haz un paso con los ingresos por category_id y luego únelo con categories.
Solución
WITH category_sales AS (
SELECT p.category_id, SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
WHERE o.status <> 'cancelled'
GROUP BY p.category_id
)
SELECT c.name, ROUND(cs.revenue, 2) AS revenue
FROM category_sales cs
JOIN categories c ON c.id = cs.category_id
WHERE cs.revenue > 200;El paso une order_items con orders (para el estado) y products (para la categoría), y la consulta principal añade el nombre y filtra. Tres categorías: «Cocina» (516,90 €), «Lácteos» (315,50 €) y «Bebidas» (231,20 €).
3. En el módulo sobre unir tablas hiciste con CROSS JOIN la cuadrícula de cada ciudad y cada subcategoría. Ahora rellénala: los ingresos de cada par, con 0 donde no hay ventas.
Muestra la ciudad, el nombre de la subcategoría (columna category) y los ingresos (columna revenue) de los clientes de esa ciudad en esa subcategoría (sin los pedidos cancelados), redondeados a dos decimales. Para los pares sin ventas los ingresos son 0. El campo ya tiene la cuadrícula.
Solución
WITH grid AS (
SELECT DISTINCT cu.city, cat.id AS category_id, cat.name AS category
FROM customers cu
CROSS JOIN categories cat
WHERE cu.city IS NOT NULL AND cat.parent_id IS NOT NULL
),
sales AS (
SELECT cu.city, p.category_id, SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN customers cu ON cu.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status <> 'cancelled'
GROUP BY cu.city, p.category_id
)
SELECT g.city, g.category, COALESCE(ROUND(s.revenue, 2), 0) AS revenue
FROM grid g
LEFT JOIN sales s ON s.city = g.city AND s.category_id = g.category_id;Dos pasos: grid es la cuadrícula, sales son los ingresos por ciudad y category_id. Luego grid LEFT JOIN sales por las dos columnas y COALESCE(…, 0). 36 filas, cuatro de ellas con cero: Burgas en «Cocina», «Lácteos» y «Bebidas», y Ruse en «Limpieza». La cuadrícula está a la izquierda, así que ningún par se pierde.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Muchas tablas, y una tabla unida consigo misma · UNION, INTERSECT, EXCEPT y CROSS JOIN · Subconsultas · Subconsultas correlacionadas
- Continúa en: Árboles y secuencias: WITH RECURSIVE · Funciones de ventana · Buscar por significado · La panadería, etapa 3: las preguntas
- Ejercicios sobre el tema: Subconsultas y ventanas