La propuesta de trabajo, con algunos datos de muestra, se puede encontrar en @ rextester: bigtable unpivot
La esencia de la operación:
1 - Use syscolumns y for xml para generar dinámicamente nuestras listas de columnas para la operación de univivot; todos los valores se convertirán a varchar (max), con los valores NULL convertidos a la cadena 'NULL' (esto soluciona el problema con los valores NULL de omisión no dinámica)
2 - Genere una consulta dinámica para desenredar datos en la tabla temporal #columns
- ¿Por qué una tabla temporal vs CTE (a través de una cláusula)? preocupado por un posible problema de rendimiento para un gran volumen de datos y una unión automática CTE sin esquema de índice / hash utilizable; una tabla temporal permite la creación de un índice que debería mejorar el rendimiento en la autounión [ver autounión CTE lenta ]
- Los datos se escriben en #columnas en el orden PK + ColName + UpdateDate, lo que nos permite almacenar valores PK / Colname en filas adyacentes; una columna de identidad ( rid ) nos permite unir estas filas consecutivas mediante rid = rid + 1
3 - Realice una autounión de la tabla #temp para generar la salida deseada
Cortar y pegar desde rextester ...
Cree algunos datos de muestra y nuestra tabla #columns:
CREATE TABLE dbo.bigtable
(UpdateDate datetime not null
,PK varchar(12) not null
,col1 varchar(100) null
,col2 int null
,col3 varchar(20) null
,col4 datetime null
,col5 char(20) null
,PRIMARY KEY (PK)
);
CREATE TABLE dbo.bigtable_archive
(UpdateDate datetime not null
,PK varchar(12) not null
,col1 varchar(100) null
,col2 int null
,col3 varchar(20) null
,col4 datetime null
,col5 char(20) null
,PRIMARY KEY (PK, UpdateDate)
);
insert into dbo.bigtable values ('20170512', 'ABC', NULL, 6, 'C1', '20161223', 'closed')
insert into dbo.bigtable_archive values ('20170427', 'ABC', NULL, 6, 'C1', '20160820', 'open')
insert into dbo.bigtable_archive values ('20170315', 'ABC', NULL, 5, 'C1', '20160820', 'open')
insert into dbo.bigtable_archive values ('20170212', 'ABC', 'C1', 1, 'C1', '20160820', 'open')
insert into dbo.bigtable_archive values ('20170109', 'ABC', 'C1', 1, 'C1', '20160513', 'open')
insert into dbo.bigtable values ('20170526', 'XYZ', 'sue', 23, 'C1', '20161223', 're-open')
insert into dbo.bigtable_archive values ('20170401', 'XYZ', 'max', 12, 'C1', '20160825', 'cancel')
insert into dbo.bigtable_archive values ('20170307', 'XYZ', 'bob', 12, 'C1', '20160825', 'cancel')
insert into dbo.bigtable_archive values ('20170223', 'XYZ', 'bob', 12, 'C1', '20160820', 'open')
insert into dbo.bigtable_archive values ('20170214', 'XYZ', 'bob', 12, 'C1', '20160513', 'open')
;
create table #columns
(rid int identity(1,1)
,PK varchar(12) not null
,UpdateDate datetime not null
,ColName varchar(128) not null
,ColValue varchar(max) null
,PRIMARY KEY (rid, PK, UpdateDate, ColName)
);
Las entrañas de la solución:
declare @columns_max varchar(max),
@columns_raw varchar(max),
@cmd varchar(max)
select @columns_max = stuff((select ',isnull(convert(varchar(max),'+name+'),''NULL'') as '+name
from syscolumns
where id = object_id('dbo.bigtable')
and name not in ('PK','UpdateDate')
order by name
for xml path(''))
,1,1,''),
@columns_raw = stuff((select ','+name
from syscolumns
where id = object_id('dbo.bigtable')
and name not in ('PK','UpdateDate')
order by name
for xml path(''))
,1,1,'')
select @cmd = '
insert #columns (PK, UpdateDate, ColName, ColValue)
select PK,UpdateDate,ColName,ColValue
from
(select PK,UpdateDate,'+@columns_max+' from bigtable
union all
select PK,UpdateDate,'+@columns_max+' from bigtable_archive
) p
unpivot
(ColValue for ColName in ('+@columns_raw+')
) as unpvt
order by PK, ColName, UpdateDate'
--select @cmd
execute(@cmd)
--select * from #columns order by rid
;
select c2.PK, c2.UpdateDate, c2.ColName as ColumnName, c1.ColValue as 'Old Value', c2.ColValue as 'New Value'
from #columns c1,
#columns c2
where c2.rid = c1.rid + 1
and c2.PK = c1.PK
and c2.ColName = c1.ColName
and isnull(c2.ColValue,'xxx') != isnull(c1.ColValue,'xxx')
order by c2.UpdateDate, c2.PK, c2.ColName
;
Y los resultados:
Nota: disculpas ... no pude encontrar una manera fácil de cortar y pegar la salida del rextester en un bloque de código. Estoy abierto a sugerencias.
Posibles problemas / preocupaciones:
1 - la conversión de datos a un genérico varchar (max) puede conducir a la pérdida de precisión de los datos, lo que a su vez puede significar que perdemos algunos cambios de datos; considere los siguientes pares de fecha y hora que, cuando se convierten / convierten al genérico 'varchar (max)', pierden su precisión (es decir, los valores convertidos son los mismos):
original value varchar(max)
------------------- -------------------
06/10/2017 10:27:15 Jun 10 2017 10:27AM
06/10/2017 10:27:18 Jun 10 2017 10:27AM
234.23844444 234.238
234.23855555 234.238
29333488.888 2.93335e+007
29333499.999 2.93335e+007
Si bien se podría mantener la precisión de los datos, requeriría un poco más de codificación (por ejemplo, conversión basada en los tipos de datos de la columna fuente); por ahora he optado por seguir con el varchar genérico (max) según la recomendación del OP (y suponiendo que el OP conoce los datos lo suficientemente bien como para saber que no nos encontraremos con ningún problema de pérdida de precisión de datos).
2: para conjuntos de datos realmente grandes, corremos el riesgo de eliminar algunos recursos del servidor, ya sea espacio temporal o caché / memoria; El problema principal proviene de la explosión de datos que ocurre durante una desconexión (por ejemplo, pasamos de 1 fila y 302 piezas de datos a 300 filas y 1200-1500 piezas de datos, incluidas 300 copias de las columnas PK y UpdateDate, 300 nombres de columnas)