大量のデータの行間の違いの詳細を照会する


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、2つのテーブルをまとめてグループ化するとPK、変更のタイムラインが作成されます。UpdateDateます。

次の形式で、並べUpdateDate替え、グループ化、行間の違いを詳細に説明するレポートを作成しPKます。

PK,   UpdateDate,  ColumnName,  Old Value,   New Value

Old ValueNew Valueキャストされた関連する列にすることができますVARCHAR(MAX)TEXTまたはBYTE私は任意の後処理値そのものの操作を行う必要はありませんよう、列関与)。

現時点では、クエリをプログラムで生成することに頼ることなく、大量の列に対してこれを行う正しい方法を考えることはできません-これを行う必要があります。

たくさんのアイデアを受け入れるので、2日後に質問に報奨金を追加します。

回答:


15

これは、特に300を超える列とが使用できないことを考えると、見栄えがよくありませんしLAG、非常にうまく機能する可能性もありませんが、まずは次のアプローチを試してみます。

  • UNION 2つのテーブル。
  • 結合されたセットの各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
);

あなたは、自己に参加して新しい、古い値を見つけるために、行と一致することができPKColumnNameかつVersion = Version + 1

それほどきれいではない部分は、もちろん、2つのベーステーブルから一時テーブルに300列のピボットを解除することです。

厄介なものを減らすためのXML

テーブル内のピボットされない実際の列を知る必要なく、XMLを使用してデータのピボットを解除することができます。列名はXMLの要素名として有効である必要があります。そうでない場合、失敗します。

そのアイデアは、その行のすべての値を持つ行ごとに1つの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

次に、XMLを細断しnodes('*') て、列ごとに1行を取得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

別のアプローチをお勧めします。

現在のアプリケーションを変更することはできませんが、データベースの動作を変更できる可能性があります。

可能であれば、2つのTRIGGERSを現在のテーブルに追加します。

dbo.bigtable_archiveの1つの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;
UpdateDate | PK | col1 | col2 | col3
:------------------ | :-| :--- | ---:| :---
02/01/2017 00:00:00 | ABC | C3 | 1 | C1  

UpdateDate | PK | col1 | col2 | col3
:------------------ | :-| :--- | ---:| :---
01/01/2017 00:00:00 | ABC | C1 | 1 | C1  

ここで、bigtable_archiveに、bigtableの保留中のすべてのレコードを挿入する必要があります。

INSERT INTO bigtable_archive
SELECT *
FROM   bigtable
WHERE  UpdateDate >= '20170102';
SELECT * FROM bigtable_archive;
GO
UpdateDate | PK | col1 | col2 | col3
:------------------ | :-| :--- | ---:| :---
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
SELECT * FROM bigtable_archive;
GO
UpdateDate | PK | col1 | col2 | col3
:------------------ | :-| :--- | ---:| :---
01/01/2017 00:00:00 | ABC | C1 | 1 | C1  
02/01/2017 00:00:00 | ABC | C3 | 1 | C1  

明らかに、アーカイブテーブルのみをクエリすることで、変更のタイムラインを取得できます。そして、アプリケーションは、トリガーが密かに隠れて仕事をしていることに気付かないでしょう。

ここに dbfiddle


4

いくつかのサンプルデータを含むワーキングプロポーザルは、rextesterで見つけることができます:bigtable unpivot


操作の要点:

1- syscolumnsおよびxmlを使用して、ピボット解除操作用の列リストを動的に生成します。すべての値はvarchar(max)に変換され、w / nullは文字列 'NULL'に変換されます(これはNULL値をスキップするアンピボットの問題に対処します)

2-#columns一時テーブルにデータをピボット解除する動的クエリを生成します

  • 一時テーブルとCTEの組み合わせ(with with句を使用)が必要な理由 大量のデータの潜在的なパフォーマンスの問題と、使用可能なインデックス/ハッシュスキームのないCTEの自己結合に関する問題。一時テーブルは、自己結合のパフォーマンスを改善するインデックスの作成を可能にします[ 遅いCTE自己結合を参照 ]
  • データはPK + ColName + UpdateDateの順序で#columnsに書き込まれるため、PK / Colname値を隣接する行に格納できます。ID列(rid)を使用すると、rid = rid + 1を介してこれらの連続した行を自己結合できます。

3-#tempテーブルの自己結合を実行して、目的の出力を生成します

rextesterからカットアンドペースト...

サンプルデータと#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
;

そして結果:

ここに画像の説明を入力してください

注:おpびします... rextesterの出力をコードブロックにカットアンドペーストする簡単な方法がわかりませんでした。私は提案を受け入れます。


潜在的な問題/懸念:

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"です。

NB:パフォーマンスをチェックしていません。

注:これは動的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'

間違っていない場合、これは同じ行に行われた複数の変更をキャッチしませんか?
ミカエルエリクソン

そのとおりです。同時に更新された複数の列はキャプチャされません。変更された最初の列のみがキャプチャされます。
ダーメンダークマール 'DK'

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)

ここで、内部選択クエリでは、pをメインテーブル、p1を履歴テーブルと見なします。ピボット解除では、同じタイプに変換することが重要です。

私のスクリプトを理解するために、より少ない列名を持つ他のテーブル名を使用することができます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.