查询大量数据行之间的详细差异

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)(不涉及TEXTBYTE涉及列),因为我不需要对值本身进行任何后处理。

目前,我想不出一种对大量列执行此操作的明智方法,而无需以编程方式生成查询 - 我可能必须这样做。

对很多想法持开放态度,所以我会在 2 天后为这个问题添加一个赏金。

And*_*y M 15

这看起来不会很漂亮,尤其是考虑到 300 多列和不可用的情况下LAG,它也不太可能表现得非常好,但作为开始,我会尝试以下方法:

  • UNION 两张桌子。
  • 对于组合集中的每个 PK,从存档表中获取其先前的“化身”(下面的实现使用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)

您可以匹配行以查找新值和旧值PKColumnName并在、和上进行自连接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)


McN*_*ets 6

我建议你另一种方法。

尽管您无法更改当前应用程序,但您可能可以更改数据库行为。

如果可能,我会在当前表中添加两个触发器。

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)

好的,我在这里用这个初始值设置了一个小例子:

SELECT * FROM bigtable;
SELECT * FROM bigtable_archive;
Run Code Online (Sandbox Code Playgroud)
更新日期 | 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)
SELECT * FROM bigtable_archive;
GO
Run Code Online (Sandbox Code Playgroud)
更新日期 | 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 表上插入记录时,触发器将检测它是否存在,并避免插入。

INSERT INTO dbo.bigtable_archive VALUES('20170102', 'ABC', 'C3', 1, 'C1');
GO
Run Code Online (Sandbox Code Playgroud)
SELECT * FROM bigtable_archive;
GO
Run Code Online (Sandbox Code Playgroud)
更新日期 | 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在这里