生成差异的最有效方法

ame*_*ame 8 join sql-server union cross-apply

我在 SQL Server 中有一个表,如下所示:

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
Run Code Online (Sandbox Code Playgroud)

我正在研究一个存储过程来区分,它需要输入数据和版本号。输入数据包含从 Name up 到 fieldZ 的列。大多数字段列预计为NULL,即每行通常只有前几个字段的数据,其余为NULL。名称、日期和版本构成了表上的唯一约束。

对于给定的版本,我需要对输入的与该表相关的数据进行比较。每一行都需要区分——一行由名称、日期和版本标识,字段列中任何值的任何更改都需要显示在差异中。

更新:所有字段都不需要是十进制类型。其中一些可能是 nvarchars。我更希望 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
Run Code Online (Sandbox Code Playgroud)

差异需要采用以下格式:

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
Run Code Online (Sandbox Code Playgroud)

到目前为止,我的解决方案是首先使用 EXCEPT 和 UNION 生成差异。然后使用 JOIN 和 CROSS APPLY 将差异转换为所需的输出格式。虽然这似乎有效,但我想知道是否有更清洁、更有效的方法来做到这一点。字段数接近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)  
Run Code Online (Sandbox Code Playgroud)

谢谢!

Vla*_*nov 5

编辑具有不同类型的字段,而不仅仅是decimal.

您可以尝试使用sql_variant类型。我从来没有亲自使用过它,但对于您的情况,它可能是一个很好的解决方案。要尝试它只是替换所有[decimal](38, 10)sql_variant在SQL脚本。查询本身保持原样,不需要显式转换来执行比较。最终结果将有一列包含不同类型的值。最有可能的是,最终您必须以某种方式知道哪种类型在哪个字段中才能在您的应用程序中处理结果,但查询本身应该可以在没有转换的情况下正常工作。


顺便说一句,将日期存储为int.

我不会使用EXCEPTUNION来计算差异,而是使用FULL JOIN. 就我个人而言,很难遵循背后的逻辑EXCEPTUNION方法。

我会从取消数据透视开始,而不是最后做(CROSS APPLY(VALUES)像你一样使用)。如果您提前在调用方端执行此操作,您可以摆脱输入的逆透视。

您必须仅在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;
Run Code Online (Sandbox Code Playgroud)

主要查询

CTE_Main是过滤到给定的未透视原始数据VersionCTE_Input是输入表,它可以已经以这种格式提供。主查询使用FULL JOIN,它添加到结果行中Bee。我认为它们应该被退回,但如果你不想看到它们,你可以通过添加AND CTE_Input.FieldValue IS NOT NULL或使用LEFT JOIN代替来过滤掉它们FULL 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;
Run Code Online (Sandbox Code Playgroud)

结果

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
Run Code Online (Sandbox Code Playgroud)


And*_*y M 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)
;
Run Code Online (Sandbox Code Playgroud)

这是它的工作原理:

  1. 这两个表使用外连接连接,@diffInput位于外侧以匹配您的右连接。

  2. 连接的结果使用 CROSS APPLY 有条件地取消透视,其中“有条件地”意味着单独测试每对列并仅在列不同时返回。

  3. 每个测试条件的模式

    NOT EXISTS (SELECT oldValue INTERSECT SELECT newValue)
    
    Run Code Online (Sandbox Code Playgroud)

    相当于你的

    oldValue != newValue
    OR (oldValue IS NULL AND newValue IS NOT NULL)
    OR (oldValue IS NOT NULL AND newValue IS NULL)
    
    Run Code Online (Sandbox Code Playgroud)

    只会更简洁。您可以在 Paul White 的文章Undocumented Query Plans: Equality Comparisons中详细了解 INTERSECT 的这种用法。

另一方面,既然你说,

随着时间的推移,输入表和现有表预计会相当大

您可能需要考虑用临时表替换用于输入表的表变量。Martin Smith 有一个非常全面的答案,探讨了两者之间的差异:

简而言之,与临时表相比,表变量的某些属性(例如缺少列统计信息)可能会使它们对您的场景的查询优化器不那么友好。