Roboczą propozycję, z pewnymi przykładowymi danymi, można znaleźć @ rextester: bigtable unpivot
Istota operacji:
1 - Użyj syscolumns i xml do dynamicznego generowania naszych list kolumn dla operacji unpivot; wszystkie wartości zostaną przekonwertowane na varchar (maks.), w / NULL zostaną przekonwertowane na ciąg „NULL” (rozwiązuje to problem z pomijaniem wartości NULL przez unpivot)
2 - Wygeneruj zapytanie dynamiczne, aby rozdzielić dane w tabeli tymczasowej #columns
- Dlaczego tabela temp vs CTE (przez z klauzulą)? zaniepokojony potencjalnym problemem związanym z wydajnością dużej ilości danych i samozłączeniem CTE bez użytecznego indeksu / schematu mieszania; tabela temp pozwala na utworzenie indeksu, który powinien poprawić wydajność samozłączenia [patrz powolne samodzielne łączenie CTE ]
- Dane są zapisywane do # kolumn w PK + ColName + UpdateDate, co pozwala nam przechowywać wartości PK / Colname w sąsiednich wierszach; kolumna tożsamości ( rid ) pozwala nam łączyć się z tymi kolejnymi wierszami poprzez rid = rid + 1
3 - Wykonaj samodzielne połączenie w tabeli #temp, aby wygenerować pożądany wynik
Wycinanie i wklejanie z rextestera ...
Utwórz przykładowe dane i naszą tabelę #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)
);
Odwaga rozwiązania:
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
;
A wyniki:
Uwaga: przepraszam ... nie mogłem znaleźć prostego sposobu na wycięcie i wklejenie wyniku rextestera do bloku kodu. Jestem otwarty na sugestie.
Potencjalne problemy / obawy:
1 - konwersja danych na ogólny varchar (max) może prowadzić do utraty dokładności danych, co z kolei może oznaczać, że przegapimy pewne zmiany danych; rozważ następujące pary dat i liczb zmiennoprzecinkowych, które po przekonwertowaniu / rzutowaniu na ogólny „varchar (max)” tracą swoją precyzję (tzn. przekonwertowane wartości są takie same):
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
Chociaż można by zachować precyzję danych, wymagałoby to nieco więcej kodowania (np. Rzutowania w oparciu o typy danych w kolumnie źródłowej); na razie zdecydowałem się trzymać ogólnego varchara (maks.) zgodnie z zaleceniem PO (i założenie, że OP zna dane wystarczająco dobrze, aby wiedzieć, że nie napotkamy żadnych problemów z utratą precyzji danych).
2 - w przypadku naprawdę dużych zestawów danych istnieje ryzyko wysadzenia niektórych zasobów serwera, bez względu na to, czy jest to przestrzeń tempdb i / lub pamięć podręczna / pamięć; główny problem pochodzi z eksplozji danych, która ma miejsce podczas rozpadu (np. przechodzimy z 1 wiersza i 302 kawałków danych do 300 wierszy i 1200-1500 kawałków danych, w tym 300 kopii kolumn PK i UpdateDate, 300 nazw kolumn)
Używam AdventureWorks2012`, Production.ProductCostHistory i Production.ProductListPriceHistory w moim przykładzie. Może to nie być doskonały przykład tabeli historii, „ale skrypt jest w stanie zebrać pożądane dane wyjściowe i prawidłowe dane wyjściowe”.
Możesz wziąć dowolną inną nazwę tabeli o mniejszej nazwie kolumny, aby zrozumieć mój skrypt. Każde wyjaśnienie musi mnie pingować.
źródło