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


15

我有很多大表,每个表都有> 300列。我正在使用的应用程序通过在辅助表中复制当前行来创建已更改行的“归档”。

考虑一个简单的例子:

CREATE TABLE dbo.bigtable
(
  UpdateDate datetime,
  PK varchar(12) PRIMARY KEY,
  col1 varchar(100),
  col2 int,
  col3 varchar(20),
  .
  .
  .
  colN datetime
);

存档表:

CREATE TABLE dbo.bigtable_archive
(
  UpdateDate datetime,
  PK varchar(12) NOT NULL,
  col1 varchar(100),
  col2 int,
  col3 varchar(20),
  .
  .
  .
  colN datetime
);

在上执行任何更新之前dbo.bigtable,会在中创建该行的副本dbo.bigtable_archive,然后dbo.bigtable.UpdateDate使用当前日期进行更新。

因此,按排序时,UNION将两个表放在一起并按分组可以PK创建更改的时间表UpdateDate

我希望创建一个报告,详细说明行之间的差异UpdateDate,按PK以下格式排序,按,分组,

PK,   UpdateDate,  ColumnName,  Old Value,   New Value

Old Value并且New Value可以转换为相应的列VARCHAR(MAX)(没有TEXTBYTE列参与),因为我不需要做值本身的任何后期处理。

目前,我想不出一种合理的方法来对大量列进行此操作,而不用依靠编程方式生成查询-我可能必须这样做。

开放的想法很多,所以我将在两天后向这个问题添加赏金。

Answers:


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
;

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
);

你可以匹配的行寻找新的和旧值以自连接上PKColumnNameVersion = 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;
<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>

elements xsinil是否可以使用创建列元素NULL

然后可以使用nodes('*') 来粉碎XML ,以便为每一列获取一行,并用于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)

完整解决方案如下。注意这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;

6

我建议您使用另一种方法。

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

如果可能的话,我将在当前表中添加两个TRIGGERS。

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

而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

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

SELECT * FROM bigtable;
SELECT * FROM bigtable_archive;
更新日期| PK | col1 | col2 | col3
:------------------ | :-| :--- | ---::---
2017年2月1日00:00:00 | ABC | C3 | 1 | C1  

更新日期| PK | col1 | col2 | col3
:------------------ | :-| :--- | ---::---
01/01/2017 00:00:00 | ABC | C1 | 1 | C1  

现在,您应该将bigtable中所有未决的记录插入bigtable_archive中。

INSERT INTO bigtable_archive
SELECT *
FROM   bigtable
WHERE  UpdateDate >= '20170102';
SELECT * FROM bigtable_archive;
GO
更新日期| PK | col1 | col2 | col3
:------------------ | :-| :--- | ---::---
01/01/2017 00:00:00 | ABC | C1 | 1 | C1  
2017年2月1日00:00:00 | ABC | C3 | 1 | C1  

现在,下次应用程序尝试在bigtable_archive表上插入记录时,触发器将检测到该记录是否存在,并避免插入。

INSERT INTO dbo.bigtable_archive VALUES('20170102', 'ABC', 'C3', 1, 'C1');
GO
SELECT * FROM bigtable_archive;
GO
更新日期| PK | col1 | col2 | col3
:------------------ | :-| :--- | ---::---
01/01/2017 00:00:00 | ABC | C1 | 1 | C1  
2017年2月1日00:00:00 | ABC | C3 | 1 | C1  

显然,现在您可以通过仅查询存档表来获得更改的时间表。而且应用程序永远也不会意识到触发程序正在悄悄地从事这项工作。

dbfiddle 在这里


4

可在rextester上找到带有一些示例数据的工作建议:bigtable unpivot


操作要点:

1-使用syscolumnsfor xml为unpivot操作动态生成我们的列列表;所有值都将被转换为varchar(max),而将NULL转换为字符串'NULL'(此地址将解决无误跳过NULL值的问题)

2-生成动态查询以将数据取消透视化到#columns临时表中

  • 为什么要使用临时表vs CTE(通过with子句)?涉及大量数据的潜在性能问题以及没有可用索引/哈希方案的CTE自联接;临时表允许创建索引,该索引应提高自连接的性能[请参见慢速CTE自连接 ]
  • 数据以PK + ColName + UpdateDate的顺序写入#列,从而使我们可以在相邻行中存储PK / Colname值;标识列(rid)允许我们通过rid = rid + 1自联接这些连续的行

3-执行#temp表的自联接以生成所需的输出

从右旋糖膏中切出...

创建一些示例数据和我们的#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)
);

解决方案的胆量:

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
;

结果:

在此处输入图片说明

注意:道歉...无法找出将nextester输出剪切-粘贴到代码块中的简单方法。我愿意提出建议。


潜在的问题/担忧:

1-将数据转换为通用varchar(max)可能会导致数据精度下降,这又意味着我们错过了一些数据更改;考虑以下日期时间和浮点对,当它们转换/广播为通用“ varchar(max)”时,它们会失去精度(即,转换后的值相同):

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

虽然可以保持数据精度,但需要更多的编码(例如,基于源列数据类型的转换);现在,我选择按照OP的建议坚持使用通用的varchar(max)(并假设OP非常了解数据,以至于我们不会遇到任何数据精度损失的问题)。

2-对于非常大的数据集,我们冒着耗尽一些服务器资源(无论是tempdb空间和/或缓存/内存)的风险;主要问题来自于取消透视期间发生的数据爆炸(例如,我们从1行和302条数据变为300行和1200-1500条数据,包括PK和UpdateDate列的300个副本,300个列的名称)


1

这种方法使用动态查询来生成sql以获取更改。SP采用表和架构名称,并提供所需的输出。

假设所有表中都存在PK和UpdateDate列。并且所有存档表的格式均为originalTableName +“ _archive”。

注意:我尚未检查其性能。

注意:由于它使用动态SQL,因此我应该添加有关安全性/ SQL注入的警告。限制对SP的访问并添加其他验证以防止sql注入。

    CREATE proc getTableChanges
    @schemaname  varchar(255),
    @tableName varchar(255)
    as

    declare @strg nvarchar(max), @colNameStrg nvarchar(max)='', @oldValueString nvarchar(max)='', @newValueString nvarchar(max)=''

    set @strg = '
    with cte as (

    SELECT  * , ROW_NUMBER() OVER(partition by PK ORDER BY UpdateDate) as RowNbr
    FROM    (

        SELECT  *
        FROM    [' + @schemaname + '].[' + @tableName + ']

        UNION

        SELECT  *
        FROM    [' + @schemaname + '].[' + @tableName + '_archive]

        ) a

    )
    '


    SET @strg = @strg + '

    SELECT  a.pk, a.updateDate, 
    CASE '

    DECLARE @colName varchar(255)
    DECLARE cur CURSOR FOR
        SELECT  COLUMN_NAME
        FROM    INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_SCHEMA = @schemaname
        AND TABLE_NAME = @tableName
        AND COLUMN_NAME NOT IN ('PK', 'Updatedate')

    OPEN cur
    FETCH NEXT FROM cur INTO @colName 

    WHILE @@FETCH_STATUS = 0
    BEGIN

        SET @colNameStrg  = @colNameStrg  + ' when a.' + @colName + ' <> b.' + @colName + ' then ''' + @colName + ''' '
        SET @oldValueString = @oldValueString + ' when a.' + @colName + ' <> b.' + @colName + ' then cast(a.' + @colName + ' as varchar(max))'
        SET @newValueString = @newValueString + ' when a.' + @colName + ' <> b.' + @colName + ' then cast(b.' + @colName + ' as varchar(max))'


    FETCH NEXT FROM cur INTO @colName 
    END

    CLOSE cur
    DEALLOCATE cur


    SET @colNameStrg = @colNameStrg  + '    END as ColumnChanges '
    SET @oldValueString = 'CASE ' + @oldValueString + ' END as OldValue'
    SET @newValueString = 'CASE ' + @newValueString + ' END as NewValue'

    SET @strg = @strg + @colNameStrg + ',' + @oldValueString + ',' + @newValueString

    SET @strg = @strg + '
        FROM    cte a join cte b on a.PK = b.PK and a.RowNbr + 1 = b.RowNbr 
        ORDER BY  a.pk, a.UpdateDate
    '

    print @strg

    execute sp_executesql @strg


    go

样品电话:

exec getTableChanges 'dbo', 'bigTable'

如果我没记错的话,这不会捕获对同一行所做的多个更改,对吗?
Mikael Eriksson

没错。不会同时捕获多个更新的列。仅捕获具有更改的第一列。
Dharmendar Kumar'DK'17年

1

我在示例中使用AdventureWorks2012`,Production.ProductCostHistory和Production.ProductListPriceHistory。这可能不是完美的历史记录表示例,“但脚本可以将期望的输出和正确的输出放在一起”。

     DECLARE @sql NVARCHAR(MAX)
    ,@columns NVARCHAR(Max)
    ,@table VARCHAR(200) = 'ProductCostHistory'
    ,@Schema VARCHAR(200) = 'Production'
    ,@Archivecolumns NVARCHAR(Max)
    ,@ColForUnpivot NVARCHAR(Max)
    ,@ArchiveColForUnpivot NVARCHAR(Max)
    ,@PKCol VARCHAR(200) = 'ProductID'
    ,@UpdatedCol VARCHAR(200) = 'modifiedDate'
    ,@Histtable VARCHAR(200) = 'ProductListPriceHistory'
SELECT @columns = STUFF((
            SELECT ',CAST(p.' + QUOTENAME(column_name) + ' AS VARCHAR(MAX)) AS ' + QUOTENAME(column_name)
            FROM information_schema.columns
            WHERE table_name = @table
                AND column_name NOT IN (
                    @PKCol
                    ,@UpdatedCol
                    )
            ORDER BY ORDINAL_POSITION
            FOR XML PATH('')
            ), 1, 1, '')
    ,@Archivecolumns = STUFF((
            SELECT ',CAST(p1.' + QUOTENAME(column_name) + ' AS VARCHAR(MAX)) AS ' + QUOTENAME('A_' + column_name)
            FROM information_schema.columns
            WHERE table_name = @Histtable
                AND column_name NOT IN (
                    @PKCol
                    ,@UpdatedCol
                    )
            ORDER BY ORDINAL_POSITION
            FOR XML PATH('')
            ), 1, 1, '')
    ,@ColForUnpivot = STUFF((
            SELECT ',' + QUOTENAME(column_name)
            FROM information_schema.columns
            WHERE table_name = @table
                AND column_name NOT IN (
                    @PKCol
                    ,@UpdatedCol
                    )
            ORDER BY ORDINAL_POSITION
            FOR XML PATH('')
            ), 1, 1, '')
    ,@ArchiveColForUnpivot = STUFF((
            SELECT ',' + QUOTENAME('A_' + column_name)
            FROM information_schema.columns
            WHERE table_name = @Histtable
                AND column_name NOT IN (
                    @PKCol
                    ,@UpdatedCol
                    )
            ORDER BY ORDINAL_POSITION
            FOR XML PATH('')
            ), 1, 1, '')

--SELECT @columns   ,@Archivecolumns    ,@ColForUnpivot
SET @sql = N' 
    SELECT ' + @PKCol + ', ColumnName,
            OldValue,NewValue,' + @UpdatedCol + '
    FROM    (  
    SELECT p.' + @PKCol + '
        ,p.' + @UpdatedCol + '
        ,' + @columns + '
        ,' + @Archivecolumns + '
    FROM ' + @Schema + '.' + @table + ' p
    left JOIN ' + @Schema + '.' + @Histtable + ' p1 ON p.' + @PKCol + ' = p1.' + @PKCol + '

  ) t
    UNPIVOT (
        OldValue
        FOR ColumnName in (' + @ColForUnpivot + ')
    ) up

     UNPIVOT (
        NewValue
        FOR ColumnName1 in (' + @ArchiveColForUnpivot + ')
    ) up1

--print @sql
EXEC (@sql)

在内部Select查询中,将p视为Main Table,将p1视为History表。在unpivot中,将其转换为相同类型很重要。

您可以使用其他任何具有较少列名称的表名来理解我的脚本。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.