差分を生成する最も効率的な方法


8

SQLサーバーに次のようなテーブルがあります。

Id    |Version  |Name    |date    |fieldA   |fieldB ..|fieldZ
1     |1        |Foo     |20120101|23       |       ..|25334123
2     |2        |Foo     |20120101|23       |NULL   ..|NULL
3     |2        |Bar     |20120303|24       |123......|NULL
4     |2        |Bee     |20120303|34       |-34......|NULL

入力データとバージョン番号を取得するdiffのストアドプロシージャに取り組んでいます。入力データには、名前からfieldZまでの列があります。ほとんどのフィールド列はNULLであることが期待されています。つまり、通常、各行には最初のいくつかのフィールドのデータのみが含まれ、残りはNULLです。名前、日付、およびバージョンは、テーブルに対する一意の制約を形成します。

特定のバージョンについて、このテーブルに関して入力されたデータを比較する必要があります。各行を比較する必要があります。行は名前、日付、バージョンで識別され、フィールドの列の値を変更すると、比較結果に表示されるようになります。

更新:すべてのフィールドが10進数である必要はありません。それらのいくつかはnvarcharである可能性があります。タイプを変換せずにdiffを実行したいのですが、diff出力はすべてをnvarcharに変換できるため、それは表示目的でのみ使用されるためです。

入力が次のようであり、要求されたバージョンが2であるとします。

Name    |date    |fieldA   |fieldB|..|fieldZ
Foo     |20120101|25       |NULL  |.. |NULL
Foo     |20120102|26       |27    |.. |NULL
Bar     |20120303|24       |126   |.. |NULL
Baz     |20120101|15       |NULL  |.. |NULL

差分は次の形式にする必要があります。

name    |date    |field    |oldValue    |newValue
Foo     |20120101|FieldA   |23          |25
Foo     |20120102|FieldA   |NULL        |26
Foo     |20120102|FieldB   |NULL        |27
Bar     |20120303|FieldB   |123         |126
Baz     |20120101|FieldA   |NULL        |15

これまでの私の解決策は、まずEXCEPTとUNIONを使用して差分を生成することです。次に、JOINとCROSS APPLYを使用して、diffを目的の出力形式に変換します。これは機能しているようですが、これを行うためのよりクリーンで効率的な方法があるかどうか疑問に思っています。フィールドの数は100に近く、コード内の...がある各場所は実際には多数の行です。入力テーブルと既存のテーブルの両方が、時間の経過とともにかなり大きくなることが予想されます。私はSQLを初めて使用しますが、まだパフォーマンスチューニングを習得しようとしています。

以下がそのSQLです。

CREATE TABLE #diff
(   [change] [nvarchar](50) NOT NULL,
    [name] [nvarchar](50) NOT NULL,
    [date] [int] NOT NULL,
    [FieldA] [decimal](38, 10) NULL,
    [FieldB] [decimal](38, 10) NULL,
    .....
    [FieldZ] [decimal](38, 10) NULL
)

--Generate the diff in a temporary table
INSERT INTO #diff
SELECT * FROM
(

(
    SELECT
        'old' as change,
        name,
        date,
        FieldA,
        FieldB,
        ...,
        FieldZ
    FROM 
        myTable mt 
    WHERE 
        version = @version
        AND mt.name + '_' + CAST(mt.date AS VARCHAR) IN (SELECT name + '_' + CAST(date AS VARCHAR) FROM @diffInput) 
    EXCEPT
    SELECT 'old' as change,* FROM @diffInput
)
UNION

(
    SELECT 'new' as change, * FROM @diffInput
    EXCEPT
    SELECT
        'new' as change,
        name,
        date,
        FieldA, 
        FieldB,
        ...,
        FieldZ
    FROM 
        myTable mt 
    WHERE 
        version = @version 
        AND mt.name + '_' + CAST(mt.date AS VARCHAR) IN (SELECT name + '_' + CAST(date AS VARCHAR) FROM @diffInput) 
) 
) AS myDiff

SELECT 
d3.name, d3.date, CrossApplied.field, CrossApplied.oldValue, CrossApplied.newValue
FROM
(
    SELECT 
        d2.name, d2.date, 
        d1.FieldA AS oldFieldA, d2.FieldA AS newFieldA, 
        d1.FieldB AS oldFieldB, d2.FieldB AS newFieldB,
        ...
        d1.FieldZ AS oldFieldZ, d2.FieldZ AS newFieldZ,
    FROM #diff AS d1
    RIGHT OUTER JOIN #diff AS d2
    ON 
        d1.name = d2.name
        AND d1.date = d2.date
        AND d1.change = 'old'
    WHERE d2.change = 'new'
) AS d3
CROSS APPLY (VALUES ('FieldA', oldFieldA, newFieldA), 
                ('FieldB', oldFieldB, newFieldB),
                ...
                ('FieldZ', oldFieldZ, newFieldZ))
                CrossApplied (field, oldValue, newValue)
WHERE 
    crossApplied.oldValue != crossApplied.newValue 
    OR (crossApplied.oldValue IS NULL AND crossApplied.newValue IS NOT NULL) 
    OR (crossApplied.oldValue IS NOT NULL AND crossApplied.newValue IS NULL)  

ありがとうございました!

回答:


5

ここに別のアプローチがあります:

SELECT
  di.name,
  di.date,
  x.field,
  x.oldValue,
  x.newValue
FROM
  @diffInput AS di
  LEFT JOIN dbo.myTable AS mt ON
    mt.version = @version
    AND mt.name = di.name
    AND mt.date = di.date
  CROSS APPLY
  (
    SELECT
      'fieldA',
      mt.fieldA,
      di.fieldA
    WHERE
      NOT EXISTS (SELECT mt.fieldA INTERSECT SELECT di.fieldA)

    UNION ALL

    SELECT
      'fieldB',
      mt.fieldB,
      di.fieldB
    WHERE
      NOT EXISTS (SELECT mt.fieldB INTERSECT SELECT di.fieldB)

    UNION ALL

    SELECT
      'fieldC',
      mt.fieldC,
      di.fieldC
    WHERE
      NOT EXISTS (SELECT mt.fieldC INTERSECT SELECT di.fieldC)

    UNION ALL

    ...
  ) AS x (field, oldValue, newValue)
;

これはどのように機能するかです:

  1. 2つのテーブルは、外部結合を使用して結合@diffInputされ、右側に結合するために外側になります。

  2. 結合の結果はCROSS APPLYを使用して条件付きでアンピボットされます。「条件付き」とは、列の各ペアが個別にテストされ、列が異なる場合にのみ返されることを意味します。

  3. 各試験条件のパターン

    NOT EXISTS (SELECT oldValue INTERSECT SELECT newValue)

    あなたと同等です

    oldValue != newValue
    OR (oldValue IS NULL AND newValue IS NOT NULL)
    OR (oldValue IS NOT NULL AND newValue IS NULL)
    

    より簡潔なだけです。このINTERSECTの使用の詳細については、Paul Whiteの記事「Undocumented Query Plans:Equality Comparisons」を参照してください

別の言い方をすると、あなたが言っているので、

入力テーブルと既存のテーブルの両方が、時間とともにかなり大きくなることが予想されます

入力テーブルに使用しているテーブル変数を一時テーブルに置き換えることを検討してください。両者の違いを探るマーティン・スミスによる非常に包括的な答えがあります:

つまり、列統計の欠如など、テーブル変数の特定のプロパティは、一時テーブルよりもシナリオのクエリオプティマイザーに適さない可能性があります。


フィールドAZのデータ型が同じでない場合、selectステートメントの2つのフィールドをvarcharに変換する必要があります。そうしないと、unionステートメントが機能しません。
Andre

5

だけでなく、さまざまなタイプのフィールドに関する編集decimal

sql_variantタイプを使用してみることができます。私はそれを個人的に使用したことはありませんが、あなたの場合には良い解決策かもしれません。それを試すには、SQLスクリプトですべて[decimal](38, 10)を置き換えてくださいsql_variant。クエリ自体はそのままです。比較を実行するために明示的な変換は必要ありません。最終結果には、さまざまなタイプの値を含む列が含まれます。ほとんどの場合、最終的には、アプリケーションで結果を処理するために、どの型がどのフィールドにあるかを何らかの方法で知る必要がありますが、クエリ自体は変換なしで正常に機能するはずです。


ちなみに、日付をとして保存することはお勧めできませんint

とを使用EXCEPTUNIONて差分を計算する代わりに、を使用しますFULL JOIN。私にとって、個人的には、背後にある論理EXCEPTUNIONアプローチに従うことは困難です。

データを最後に実行するのではなく、アンピボットすることから始めCROSS APPLY(VALUES)ます。入力のピボット解除を事前に行う場合は、呼び出し側で取り除くことができます。

でのみ100列すべてをリストする必要がありますCROSS APPLY(VALUES)

最後のクエリはかなり単純なので、一時テーブルは実際には必要ありません。私はあなたのバージョンよりも書きやすく、保守しやすいと思います。これがSQL Fiddleです。

サンプルデータを設定する

DECLARE @TMain TABLE (
    [ID] [int] NOT NULL,
    [Version] [int] NOT NULL,
    [Name] [nvarchar](50) NOT NULL,
    [dt] [date] NOT NULL,
    [FieldA] [decimal](38, 10) NULL,
    [FieldB] [decimal](38, 10) NULL,
    [FieldZ] [decimal](38, 10) NULL
);

INSERT INTO @TMain ([ID],[Version],[Name],[dt],[FieldA],[FieldB],[FieldZ]) VALUES
(1,1,'Foo','20120101',23,23  ,25334123),
(2,2,'Foo','20120101',23,NULL,NULL),
(3,2,'Bar','20120303',24,123 ,NULL),
(4,2,'Bee','20120303',34,-34 ,NULL);

DECLARE @TInput TABLE (
    [Name] [nvarchar](50) NOT NULL,
    [dt] [date] NOT NULL,
    [FieldA] [decimal](38, 10) NULL,
    [FieldB] [decimal](38, 10) NULL,
    [FieldZ] [decimal](38, 10) NULL
);

INSERT INTO @TInput ([Name],[dt],[FieldA],[FieldB],[FieldZ]) VALUES
('Foo','20120101',25,NULL,NULL),
('Foo','20120102',26,27  ,NULL),
('Bar','20120303',24,126 ,NULL),
('Baz','20120101',15,NULL,NULL);

DECLARE @VarVersion int = 2;

メインクエリ

CTE_Mainは、指定されたにフィルタリングされたピボットされていない元のデータVersionです。CTE_Inputは、このフォーマットですでに提供されている可能性のある入力テーブルです。メインクエリはを使用しFULL JOIN、結果行にを追加しますBee。それらは返される必要があると思いますが、見たくない場合は、の代わりに追加AND CTE_Input.FieldValue IS NOT NULLまたは使用することで除外できます。返されるべきだと思うので、詳細は調べませんでした。LEFT JOINFULL JOIN

WITH
CTE_Main
AS
(
    SELECT
        Main.ID
        ,Main.Version
        ,Main.Name
        ,Main.dt
        ,FieldName
        ,FieldValue
    FROM
        @TMain AS Main
        CROSS APPLY
        (
            VALUES
                ('FieldA', Main.FieldA),
                ('FieldB', Main.FieldB),
                ('FieldZ', Main.FieldZ)
        ) AS CA(FieldName, FieldValue)
    WHERE
        Main.Version = @VarVersion
)
,CTE_Input
AS
(
    SELECT
        Input.Name
        ,Input.dt
        ,FieldName
        ,FieldValue
    FROM
        @TInput AS Input
        CROSS APPLY
        (
            VALUES
                ('FieldA', Input.FieldA),
                ('FieldB', Input.FieldB),
                ('FieldZ', Input.FieldZ)
        ) AS CA(FieldName, FieldValue)
)

SELECT
    ISNULL(CTE_Main.Name, CTE_Input.Name) AS FullName
    ,ISNULL(CTE_Main.dt, CTE_Input.dt) AS FullDate
    ,ISNULL(CTE_Main.FieldName, CTE_Input.FieldName) AS FullFieldName
    ,CTE_Main.FieldValue AS OldValue
    ,CTE_Input.FieldValue AS NewValue
FROM
    CTE_Main
    FULL JOIN CTE_Input ON 
        CTE_Input.Name = CTE_Main.Name
        AND CTE_Input.dt = CTE_Main.dt
        AND CTE_Input.FieldName = CTE_Main.FieldName
WHERE
    (CTE_Main.FieldValue <> CTE_Input.FieldValue)
    OR (CTE_Main.FieldValue IS NULL AND CTE_Input.FieldValue IS NOT NULL)
    OR (CTE_Main.FieldValue IS NOT NULL AND CTE_Input.FieldValue IS NULL)
--ORDER BY FullName, FullDate, FullFieldName;

結果

FullName    FullDate    FullFieldName   OldValue        NewValue
Foo         2012-01-01  FieldA          23.0000000000   25.0000000000
Foo         2012-01-02  FieldA          NULL            26.0000000000
Foo         2012-01-02  FieldB          NULL            27.0000000000
Bar         2012-03-03  FieldB          123.0000000000  126.0000000000
Baz         2012-01-01  FieldA          NULL            15.0000000000
Bee         2012-03-03  FieldB          -34.0000000000  NULL
Bee         2012-03-03  FieldA          34.0000000000   NULL
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.