En ingeniería de datos, tarde o temprano aparece la necesidad de transformar filas en columnas, especialmente cuando trabajamos con datos normalizados que el negocio quiere ver “aplanados”. En este artículo muestro cómo resolver este patrón en SQL Server usando dos enfoques distintos, ambos basados en un CTE para que el lector pueda reproducir el ejemplo sin usar tablas físicas.
CTE de ejemplo: correos electrónicos por tipo
Usaremos correos electrónicos porque es un caso más común hoy en día: un cliente puede tener un correo personal, uno laboral y uno alternativo. El objetivo es obtener una sola fila por cliente.
-- [ES] CTE con datos de prueba
-- [EN] CTE with sample data
with Emails as (
select 101 as CustomerID, 'Personal' as EmailType, 'a.personal@sqlenvivo.com' as Email union all
select 101, 'Work', 'a.work@sqlenvivo.com' union all
select 101, 'Extra', 'a.extra@sqlenvivo.com' union all
select 202, 'Personal', 'b.personal@sqlenvivo.com' union all
select 202, 'Work', 'b.work@sqlenvivo.com' union all
select 303, 'Personal', 'c.personal@sqlenvivo.com'
)
Resultado esperado:
| CustomerID | Personal | Work | Extra |
|---|---|---|---|
| 101 | a.personal@sqlenvivo.com | a.work@sqlenvivo.com | a.extra@sqlenvivo.com |
| 202 | b.personal@sqlenvivo.com | b.work@sqlenvivo.com | NULL |
| 303 | c.personal@sqlenvivo.com | NULL | NULL |
Transformar filas en columnas con PIVOT (SQL Server)
Este es el enfoque nativo de SQL Server para transformar filas en columnas cuando se sabe de antemano los valores que se convertirán en columnas.
-- [ES] CTE con datos de prueba
-- [EN] CTE with sample data
with Emails as (
select 101 as CustomerID, 'Personal' as EmailType, 'a.personal@sqlenvivo.com' as Email union all
select 101, 'Work', 'a.work@sqlenvivo.com' union all
select 101, 'Extra', 'a.extra@sqlenvivo.com' union all
select 202, 'Personal', 'b.personal@sqlenvivo.com' union all
select 202, 'Work', 'b.work@sqlenvivo.com' union all
select 303, 'Personal', 'c.personal@sqlenvivo.com'
)
select
CustomerID,
[Personal],
[Work],
[Extra]
from Emails
pivot (
max(Email)
for EmailType in ([Personal], [Work], [Extra])
) as p;
¿Por qué funciona?
PIVOTtoma los valores deEmailTypey los convierte en columnas.- El
PIVOTnecesita una función de agregación para resolver qué valor colocar en cada celda. En este caso solo hay un correo por tipo, así queMAX()no está “calculando” nada, sino que toma ese único valor y lo coloca en la columna correspondiente (también podría haberse usadoMIN(), porque solo existe un valor por combinación). - Si un cliente no tiene un tipo de correo, la columna queda en NULL, que es lo correcto.
- Es limpio, declarativo y legible.
Transformar filas en columnas con CASE + MAX() (solución universal)
Este patrón funciona en cualquier motor SQL, no solo en SQL Server.
-- [ES] CTE con los mismos datos de prueba
-- [EN] CTE with the same sample data
with Emails as (
select 101 as CustomerID, 'Personal' as EmailType, 'a.personal@sqlenvivo.com' as Email union all
select 101, 'Work', 'a.work@sqlenvivo.com' union all
select 101, 'Extra', 'a.extra@sqlenvivo.com' union all
select 202, 'Personal', 'b.personal@sqlenvivo.com' union all
select 202, 'Work', 'b.work@sqlenvivo.com' union all
select 303, 'Personal', 'c.personal@sqlenvivo.com'
)
-- [ES] Transformación universal usando CASE + MAX()
-- [EN] Universal transformation using CASE + MAX()
select
CustomerID,
max(case when EmailType = 'Personal' then Email end) as Personal,
max(case when EmailType = 'Work' then Email end) as Work,
max(case when EmailType = 'Extra' then Email end) as Extra
from Emails
group by CustomerID;
¿Por qué funciona esto para transformar filas en columnas?
- Cada
CASEfiltra el correo del tipo correspondiente. MAX()toma ese único valor y lo convierte en una columna (también podría haberse usado MIN(), porque solo existe un valor por combinación).- Si no existe ese tipo, el
CASEdevuelve NULL, yMAX(NULL)es NULL. - Es explícito, robusto y universal.
¿Cuál usar?
- SQL Server puro:
PIVOTes elegante y directo. - Entornos mixtos:
CASE + MAX()es la opción segura. - Tipos dinámicos: Hace falta SQL dinámico (tema para otro artículo).
El script que soluciona el problema puede descargarse en este enlace.
Imagen de Barbara Jackson – Pixabay
