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)
谢谢!
编辑具有不同类型的字段,而不仅仅是decimal.
您可以尝试使用sql_variant类型。我从来没有亲自使用过它,但对于您的情况,它可能是一个很好的解决方案。要尝试它只是替换所有[decimal](38, 10)与sql_variant在SQL脚本。查询本身保持原样,不需要显式转换来执行比较。最终结果将有一列包含不同类型的值。最有可能的是,最终您必须以某种方式知道哪种类型在哪个字段中才能在您的应用程序中处理结果,但查询本身应该可以在没有转换的情况下正常工作。
顺便说一句,将日期存储为int.
我不会使用EXCEPT和UNION来计算差异,而是使用FULL JOIN. 就我个人而言,很难遵循背后的逻辑EXCEPT和UNION方法。
我会从取消数据透视开始,而不是最后做(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是过滤到给定的未透视原始数据Version。CTE_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)
这是另一种方法:
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)
这是它的工作原理:
这两个表使用外连接连接,@diffInput位于外侧以匹配您的右连接。
连接的结果使用 CROSS APPLY 有条件地取消透视,其中“有条件地”意味着单独测试每对列并仅在列不同时返回。
每个测试条件的模式
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 有一个非常全面的答案,探讨了两者之间的差异:
简而言之,与临时表相比,表变量的某些属性(例如缺少列统计信息)可能会使它们对您的场景的查询优化器不那么友好。
| 归档时间: |
|
| 查看次数: |
224 次 |
| 最近记录: |