Phi*_*lᵀᴹ 15 sql-server sql-server-2008-r2
我有许多大表,每个表都有 > 300 列。我正在使用的应用程序通过在辅助表中复制当前行来创建更改行的“档案”。
考虑一个简单的例子:
CREATE TABLE dbo.bigtable
(
UpdateDate datetime,
PK varchar(12) PRIMARY KEY,
col1 varchar(100),
col2 int,
col3 varchar(20),
.
.
.
colN datetime
);
Run Code Online (Sandbox Code Playgroud)
存档表:
CREATE TABLE dbo.bigtable_archive
(
UpdateDate datetime,
PK varchar(12) NOT NULL,
col1 varchar(100),
col2 int,
col3 varchar(20),
.
.
.
colN datetime
);
Run Code Online (Sandbox Code Playgroud)
在 上执行任何更新之前dbo.bigtable
,将在 中创建该行的副本dbo.bigtable_archive
,然后dbo.bigtable.UpdateDate
使用当前日期进行更新。
因此,UNION
将两个表放在一起并分组按PK
创建更改的时间线,按 排序时UpdateDate
。
我希望创建一个报告,详细说明行之间的差异,ordered by UpdateDate
, grouped by PK
,格式如下:
PK, UpdateDate, ColumnName, Old Value, New Value
Run Code Online (Sandbox Code Playgroud)
Old Value
并且New Value
可以是转换为 a 的相关列VARCHAR(MAX)
(不涉及TEXT
或BYTE
涉及列),因为我不需要对值本身进行任何后处理。
目前,我想不出一种对大量列执行此操作的明智方法,而无需以编程方式生成查询 - 我可能必须这样做。
对很多想法持开放态度,所以我会在 2 天后为这个问题添加一个赏金。
And*_*y M 15
这看起来不会很漂亮,尤其是考虑到 300 多列和不可用的情况下LAG
,它也不太可能表现得非常好,但作为开始,我会尝试以下方法:
UNION
两张桌子。OUTER APPLY
+TOP (1)
作为穷人的LAG
)。varchar(max)
为成对并将它们反旋转,即当前值和前一个值(CROSS APPLY (VALUES ...)
适用于此操作)。我所看到的上述 Transact-SQL:
WITH
Combined AS
(
SELECT * FROM dbo.bigtable
UNION ALL
SELECT * FROM dbo.bigtable_archive
) AS derived,
OldAndNew AS
(
SELECT
this.*,
OldCol1 = last.Col1,
OldCol2 = last.Col2,
...
FROM
Combined AS this
OUTER APPLY
(
SELECT TOP (1)
*
FROM
dbo.bigtable_archive
WHERE
PK = this.PK
AND UpdateDate < this.UpdateDate
ORDER BY
UpdateDate DESC
) AS last
)
SELECT
t.PK,
t.UpdateDate,
x.ColumnName,
x.OldValue,
x.NewValue
FROM
OldAndNew AS t
CROSS APPLY
(
VALUES
('Col1', CAST(t.OldCol1 AS varchar(max), CAST(t.Col1 AS varchar(max))),
('Col2', CAST(t.OldCol2 AS varchar(max), CAST(t.Col2 AS varchar(max))),
...
) AS x (ColumnName, OldValue, NewValue)
WHERE
NOT EXISTS (SELECT x.OldValue INTERSECT x.NewValue)
ORDER BY
t.PK,
t.UpdateDate,
x.ColumnName
;
Run Code Online (Sandbox Code Playgroud)
Mik*_*son 13
如果将数据逆透视到临时表
create table #T
(
PK varchar(12) not null,
UpdateDate datetime not null,
ColumnName nvarchar(128) not null,
Value varchar(max),
Version int not null
);
Run Code Online (Sandbox Code Playgroud)
您可以匹配行以查找新值和旧值PK
,ColumnName
并在、和上进行自连接Version = Version + 1
。
当然,不太漂亮的部分是将 300 列从两个基表转入临时表。
XML 的帮助使事情变得不那么尴尬。
可以使用 XML 对数据进行反透视,而无需知道表中哪些实际将被反透视。列名称必须与 XML 中的元素名称一样有效,否则将失败。
这个想法是为每一行创建一个 XML,其中包含该行的所有值。
select bt.PK,
bt.UpdateDate,
(select bt.* for xml path(''), elements xsinil, type) as X
from dbo.bigtable as bt;
Run Code Online (Sandbox Code Playgroud)
<UpdateDate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2001-01-03T00:00:00</UpdateDate>
<PK xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">PK1</PK>
<col1 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">c1_1_3</col1>
<col2 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">3</col2>
<col3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true" />
<colN xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2001-01-03T00:00:00</colN>
Run Code Online (Sandbox Code Playgroud)
elements xsinil
是否可以为带有NULL
.
然后可以将 XML 分解nodes('*')
为每列一行,并用于local-name(.)
获取元素名称和text()
值。
select C1.PK,
C1.UpdateDate,
T.X.value('local-name(.)', 'nvarchar(128)') as ColumnName,
T.X.value('text()[1]', 'varchar(max)') as Value
from C1
cross apply C1.X.nodes('row/*') as T(X)
Run Code Online (Sandbox Code Playgroud)
完整解决方案如下。注意Version
是反的。0 = 最新版本。
create table #X
(
PK varchar(12) not null,
UpdateDate datetime not null,
Version int not null,
RowData xml not null
);
create table #T
(
PK varchar(12) not null,
UpdateDate datetime not null,
ColumnName nvarchar(128) not null,
Value varchar(max),
Version int not null
);
insert into #X(PK, UpdateDate, Version, RowData)
select bt.PK,
bt.UpdateDate,
0,
(select bt.* for xml path(''), elements xsinil, type)
from dbo.bigtable as bt
union all
select bt.PK,
bt.UpdateDate,
row_number() over(partition by bt.PK order by bt.UpdateDate desc),
(select bt.* for xml path(''), elements xsinil, type)
from dbo.bigtable_archive as bt;
with C as
(
select X.PK,
X.UpdateDate,
X.Version,
T.C.value('local-name(.)', 'nvarchar(128)') as ColumnName,
T.C.value('text()[1]', 'varchar(max)') as Value
from #X as X
cross apply X.RowData.nodes('*') as T(C)
)
insert into #T (PK, UpdateDate, ColumnName, Value, Version)
select C.PK,
C.UpdateDate,
C.ColumnName,
C.Value,
C.Version
from C
where C.ColumnName not in (N'PK', N'UpdateDate');
/*
option (querytraceon 8649);
The above query might need some trick to go parallel.
For the testdata I had on my machine exection time is 16 seconds vs 2 seconds
https://sqlkiwi.blogspot.com/2011/12/forcing-a-parallel-query-execution-plan.html
http://dataeducation.com/next-level-parallel-plan-forcing-an-alternative-to-8649/
*/
select New.PK,
New.UpdateDate,
New.ColumnName,
Old.Value as OldValue,
New.Value as NewValue
from #T as New
left outer join #T as Old
on Old.PK = New.PK and
Old.ColumnName = New.ColumnName and
Old.Version = New.Version + 1;
Run Code Online (Sandbox Code Playgroud)
我建议你另一种方法。
尽管您无法更改当前应用程序,但您可能可以更改数据库行为。
如果可能,我会在当前表中添加两个触发器。
dbo.bigtable_archive 上的一个 INSTEAD OF INSERT 仅在新记录当前不存在时才添加它。
CREATE TRIGGER dbo.IoI_BTA
ON dbo.bigtable_archive
INSTEAD OF INSERT
AS
BEGIN
IF NOT EXISTs(SELECT 1
FROM dbo.bigtable_archive bta
INNER JOIN inserted i
ON bta.PK = i.PK
AND bta.UpdateDate = i.UpdateDate)
BEGIN
INSERT INTO dbo.bigtable_archive
SELECT * FROM inserted;
END
END
Run Code Online (Sandbox Code Playgroud)
bigtable 上的 AFTER INSERT 触发器执行完全相同的工作,但使用 bigtable 的数据。
CREATE TRIGGER dbo.IoI_BT
ON dbo.bigtable
AFTER INSERT
AS
BEGIN
IF NOT EXISTS(SELECT 1
FROM dbo.bigtable_archive bta
INNER JOIN inserted i
ON bta.PK = i.PK
AND bta.UpdateDate = i.UpdateDate)
BEGIN
INSERT INTO dbo.bigtable_archive
SELECT * FROM inserted;
END
END
Run Code Online (Sandbox Code Playgroud)
好的,我在这里用这个初始值设置了一个小例子:
Run Code Online (Sandbox Code Playgroud)SELECT * FROM bigtable; SELECT * FROM bigtable_archive;
更新日期 | PK | 列 1 | col2 | 第 3 列 :------------------ | :-- | :--- | ---: | :--- 02/01/2017 00:00:00 | ABC | C3 | 1 | C1 更新日期 | PK | 列 1 | col2 | 第 3 列 :------------------ | :-- | :--- | ---: | :--- 01/01/2017 00:00:00 | ABC | C1 | 1 | C1
现在您应该将 bigtable 中的所有待处理记录插入到 bigtable_archive 中。
INSERT INTO bigtable_archive
SELECT *
FROM bigtable
WHERE UpdateDate >= '20170102';
Run Code Online (Sandbox Code Playgroud)
Run Code Online (Sandbox Code Playgroud)SELECT * FROM bigtable_archive; GO
更新日期 | PK | 列 1 | col2 | 第 3 列 :------------------ | :-- | :--- | ---: | :--- 01/01/2017 00:00:00 | ABC | C1 | 1 | C1 02/01/2017 00:00:00 | ABC | C3 | 1 | C1
现在,下次应用程序尝试在 bigtable_archive 表上插入记录时,触发器将检测它是否存在,并避免插入。
Run Code Online (Sandbox Code Playgroud)INSERT INTO dbo.bigtable_archive VALUES('20170102', 'ABC', 'C3', 1, 'C1'); GO
Run Code Online (Sandbox Code Playgroud)SELECT * FROM bigtable_archive; GO
更新日期 | PK | 列 1 | col2 | 第 3 列 :------------------ | :-- | :--- | ---: | :--- 01/01/2017 00:00:00 | ABC | C1 | 1 | C1 02/01/2017 00:00:00 | ABC | C3 | 1 | C1
显然,现在您可以通过仅查询存档表来获取更改的时间线。并且应用程序永远不会意识到触发器正在悄悄地在幕后工作。
dbfiddle在这里
归档时间: |
|
查看次数: |
637 次 |
最近记录: |