生成差异的最有效方法


8

我在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

我正在研究要比较的存储过程,该过程需要输入数据和版本号。输入数据具有“名称”更新字段Z中的列。预期大多数字段列为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

差异必须采用以下格式:

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)  

谢谢!

Answers:


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. 这两个表使用外部联接进行联接,该联接@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)

    只有更加简洁。您可以在Paul White的文章未记录的查询计划:平等比较中详细了解有关INTERSECT的用法。

换句话说,既然你说,

随着时间的推移,输入表和现有表都将非常大

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

简而言之,表变量的某些属性(例如,缺少列统计信息)可能会使它们对您的方案的查询更不友好,而不是临时表。


如果字段AZ的数据类型不同,则select语句中的2个字段需要转换为varchar,否则union语句将不起作用。
安德烈(Andre)

5

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

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


顺便说一句,将日期存储为并不是一个好主意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是原始数据过滤到给定的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;

结果

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
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.