Proposition de travail, avec quelques exemples de données, peut être trouvée @ rextester: bigtable unpivot
L'essentiel de l'opération:
1 - Utilisez syscolumns et for xml pour générer dynamiquement nos listes de colonnes pour l'opération de pivotement; toutes les valeurs seront converties en varchar (max), w / NULLs étant convertis en la chaîne 'NULL' (ceci résout le problème avec unpivot sautant les valeurs NULL)
2 - Générez une requête dynamique pour annuler le pivotement des données dans la table temporaire #columns
- Pourquoi une table temporaire vs CTE (via avec clause)? préoccupé par un problème de performances potentiel pour un grand volume de données et une auto-jointure CTE sans index / schéma de hachage utilisable; une table temporaire permet de créer un index qui devrait améliorer les performances de l'auto-jointure [voir auto-jointure CTE lente ]
- Les données sont écrites dans #colonnes dans l'ordre PK + ColName + UpdateDate, ce qui nous permet de stocker les valeurs PK / Colname dans des lignes adjacentes; une colonne d'identité ( rid ) nous permet d'auto-joindre ces lignes consécutives via rid = rid + 1
3 - Effectuez une auto-jointure de la table #temp pour générer la sortie souhaitée
Couper-coller du rextester ...
Créez des exemples de données et notre table #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)
);
Les tripes de la solution:
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
;
Et les résultats:
Remarque: les excuses ... n'ont pas pu trouver un moyen facile de couper-coller la sortie du rextester dans un bloc de code. Je suis ouvert aux suggestions.
Problèmes / préoccupations potentiels:
1 - la conversion des données en un varchar générique (max) peut entraîner une perte de précision des données, ce qui peut signifier que nous manquons certains changements de données; considérons les paires datetime et float suivantes qui, lorsqu'elles sont converties / transtypées en 'varchar (max)' générique, perdent leur précision (c'est-à-dire que les valeurs converties sont les mêmes):
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
Bien que la précision des données puisse être maintenue, elle nécessiterait un peu plus de codage (par exemple, un cast basé sur les types de données de la colonne source); pour l'instant, j'ai choisi de m'en tenir au varchar générique (max) selon la recommandation de l'OP (et en supposant que l'OP connaît suffisamment les données pour savoir que nous ne rencontrerons aucun problème de perte de précision des données).
2 - pour de très grands ensembles de données, nous courons le risque de faire sauter certaines ressources du serveur, qu'il s'agisse d'espace tempdb et / ou de cache / mémoire; le principal problème provient de l'explosion des données qui se produit pendant un pivot (par exemple, nous passons de 1 ligne et 302 éléments de données à 300 lignes et 1 200 à 1 500 éléments de données, dont 300 copies des colonnes PK et UpdateDate, 300 noms de colonne)