Una consulta agrupada devuelve los datos “hacia abajo”: una fila por cliente y mes, por ejemplo. Pero un informe suele pedirlos “hacia el lado”: una fila por cliente y una columna por mes, como una tabla dinámica de Excel. Esa transformación de filas en columnas se llama pivotar.
SQL Server tiene el operador PIVOT para hacerlo. Además existe una alternativa estándar, con CASE dentro de una función agregada, que funciona en cualquier base de datos y que a menudo es más fácil de leer.
El punto de partida
Las ventas agrupadas por cliente y mes, en formato vertical:
| nombre | mes | ventas |
|---|---|---|
| Alejandro | 9 | 899,97 |
| Ana | 7 | 4304,25 |
| Fernando | 6 | 2449,50 |
| Luis | 9 | 4999,00 |
| María | 7 | 2199,80 |
EXTRACT(MONTH FROM fecha) es SQL estándar y funciona en el editor de la página. En SQL Server se escribe MONTH(fecha) o DATEPART(month, fecha).Queremos lo mismo con una columna para junio, otra para julio y otra para septiembre.
Con PIVOT (SQL Server)
SELECT columnasFijas, [valor1], [valor2], ...
FROM (consultaOrigen) AS origen
PIVOT (
funcionAgregada(columnaAAgregar)
FOR columnaQueSeConvierteEnColumnas IN ([valor1], [valor2], ...)
) AS tablaPivotada;
Aplicado a nuestro ejemplo:
SELECT nombre, [6] AS junio, [7] AS julio, [9] AS septiembre
FROM (
SELECT C.nombre,
MONTH(P.fechaPedido) AS mes,
P.totalPrecio
FROM Pedidos P
INNER JOIN Clientes C ON C.idClientes = P.idClientes
WHERE P.fechaPedido >= '2022-06-01'
) AS origen
PIVOT (
SUM(totalPrecio)
FOR mes IN ([6], [7], [9])
) AS ventasPorMes
ORDER BY nombre;
Tres detalles de PIVOT:
- Los valores del
INhay que escribirlos a mano. Si aparece un mes nuevo, la consulta no lo mostrará hasta que lo añadas (o generes la consulta con SQL dinámico). - Todas las columnas de la consulta de origen que no son ni la agregada ni la del
FORse usan para agrupar. Por eso conviene que el origen tenga solo las columnas necesarias. - Las celdas sin datos salen como
NULL.
Sin PIVOT: CASE dentro de SUM
La misma tabla con SQL estándar. Cada columna es una suma que solo cuenta las filas de su mes. Esta sí puedes ejecutarla:
| nombre | junio | julio | septiembre |
|---|---|---|---|
| Alejandro | NULL | NULL | 899,97 |
| Ana | NULL | 4304,25 | NULL |
| Fernando | 2449,50 | NULL | NULL |
| Luis | NULL | NULL | 4999,00 |
| María | NULL | 2199,80 | NULL |
El resultado es idéntico al de PIVOT. Ventajas de esta forma:
- Funciona en SQL Server, PostgreSQL, MySQL, Oracle y SQLite.
- Cada columna puede tener su propia condición o su propia función: una columna con la suma de julio, otra con el número de pedidos de septiembre, otra con el máximo del año…
- Si añades
ELSE 0, las celdas vacías salen como 0 en lugar deNULL.
Otro ejemplo: unidades por familia de producto
Pivotar no tiene por qué ser por fechas. Unidades vendidas a cada cliente, separadas en consolas y el resto:
| nombre | consolas | otros | total |
|---|---|---|---|
| Luis | 0 | 100 | 100 |
| Ana | 0 | 15 | 15 |
| María | 6 | 0 | 6 |
| Alejandro | 3 | 0 | 3 |
| Fernando | 0 | 1 | 1 |
UNPIVOT: de columnas a filas
UNPIVOT hace el camino inverso: convierte columnas en filas. Es útil cuando recibes datos en formato de hoja de cálculo (una columna por mes) y necesitas tenerlos en formato de tabla para poder agruparlos o unirlos.
SELECT nombre, mes, ventas
FROM ventasPorMes
UNPIVOT (
ventas FOR mes IN (junio, julio, septiembre)
) AS filas;
La alternativa estándar es un UNION ALL con una consulta por columna:
SELECT nombre, 'junio' AS mes, junio AS ventas FROM ventasPorMes WHERE junio IS NOT NULL
UNION ALL
SELECT nombre, 'julio', julio FROM ventasPorMes WHERE julio IS NOT NULL
UNION ALL
SELECT nombre, 'septiembre', septiembre FROM ventasPorMes WHERE septiembre IS NOT NULL;
UNPIVOT descarta las celdas con NULL, así que no siempre recupera exactamente las filas originales.Practica
- Añade a la consulta con
CASEuna columna con el total de cada cliente. - Pivota el número de pedidos (no el importe) por mes.
- Haz que las celdas vacías muestren 0 en lugar de
NULL.