SQL*_*Guy 5 sql-server t-sql unique-constraint
下面是一个脚本,可用于重现我面临的问题。基本上问题是这样的:为什么(ID1 = 6,ID2 = 7)的记录没有填充到最终的#tt2表中?我可以手动插入它,所以它不会违反 UNIQUE 约束,但它不会作为 INSERT/SELECT 的一部分进行填充。
USE [tempdb]
GO
IF OBJECT_ID('tempdb..#t1') IS NOT NULL DROP TABLE #t1
CREATE TABLE [#t1] ([ID] INT , [V] VARCHAR(10))
INSERT INTO #t1 (ID, V)
VALUES
(1,'A'),
(2,'B'),
(3,'B'),
(4,'C'),
(5,'E'),
(6,'E')
IF OBJECT_ID('tempdb..#t2') IS NOT NULL DROP TABLE #t2
CREATE TABLE [#t2] ([ID] INT , [V] VARCHAR(10))
INSERT INTO #t2 (ID, V)
VALUES
(1,'A'),
(2,'B'),
(3,'C'),
(4,'C'),
(5,'D'),
(6,'E'),
(7,'E')
IF OBJECT_ID('tempdb..#tt') IS NOT NULL DROP TABLE #tt
SELECT t1.ID AS ID1, t2.ID AS ID2, t1.V AS V
INTO #tt
FROM #t1 t1
JOIN #t2 t2 ON t1.V=t2.V
--SELECT * FROM #tt
IF OBJECT_ID('tempdb..#tt2') IS NOT NULL DROP TABLE #tt2
CREATE TABLE #tt2 (ID1 INT, ID2 INT, V VARCHAR(10))
CREATE UNIQUE INDEX IDX_TT_ID1 ON #tt2 (ID1) WITH (IGNORE_DUP_KEY = ON)
CREATE UNIQUE INDEX IDX_TT_ID2 ON #tt2 (ID2) WITH (IGNORE_DUP_KEY = ON)
-- Query 1 - this SELECT returns 9 records.
SELECT ID1, ID2, V
FROM #tt
ORDER BY ID1, ID2
-- Query 2 - this INSERT populates 4 records, but I would expect 5
INSERT INTO #tt2 (ID1, ID2, V)
SELECT ID1, ID2, V
FROM #tt
ORDER BY ID1, ID2
-- Why are there only 4 records in this table? (I would expect 5, but the record with ID1 = 6, ID2 = 7 is missing)
SELECT * FROM #tt2
-- I can INSERT the missing record manually:
INSERT INTO #tt2 (ID1, ID2, V)
VALUES (6, 7, 'E')
Run Code Online (Sandbox Code Playgroud)
我认为之所以会出现混乱,是因为索引在 ID1 和 ID2 上分别定义为单列。这意味着“忽略重复项”检查是对每个单独执行的,并且只有当一行通过两项检查时才会将其插入到最终的 #tt2 表中。我将逐步介绍并展示工作原理。
系统检查重复项的一种方法是对行进行排序,然后按顺序处理它们。这就是我的本地(2017)实例上发生的情况。那么让我们看看#tt的内容,有序:
Row ID1 ID2 V
i 1 1 A
ii 2 2 B
iii 3 2 B
iv 4 3 C
v 4 4 C
vi 5 6 E
vii 5 7 E
iix 6 6 E
ix 6 7 E
Run Code Online (Sandbox Code Playgroud)
从第 i 行开始,系统询问“我已经看到 ID1=1 了吗?或者我已经看到 ID2 = 1 了吗?” 由于两者都是假,所以行 i 被传递到输出。与第二行类似。然而,对于第 iii 行,ID2=2 与已经看到的第 ii 行匹配。所以第三行被拒绝。第 iv 行被接受。行 v 被拒绝,因为它与行 iv 具有相同的 ID1 值。第 vi 行被接受。第 vii 行被拒绝 - ID1=5 与第 vi 行匹配。由于 ID2=6 与 vi 行匹配,因此 iix 行被拒绝。最后,第 ix 行在 ID1(匹配 iix)和 ID2(匹配 vii)中都被拒绝。
输出是 i、ii、iv、vi,这些被插入到 #tt2 中。
Row ID1 ID2 V
i 1 1 A
ii 2 2 B
iv 4 3 C
vi 5 6 E
Run Code Online (Sandbox Code Playgroud)
之后可以插入 {6,7,E},因为它与通过过滤并实际插入到 #tt2 中的任何行都不冲突。所有 ID1=6 的行因其 ID2 值而被消除,所有 ID2=7 的行因其 ID1 值而被消除。
以上是对产生观察到的输出的逻辑算法的解释。正如在实际执行计划中观察到的,该算法的物理实现是不同但等效的。计划如下:
图 1 - INSERT 的实际执行计划
物理计划按 ID1(图中的 A)排序,并为每个值 (B) 选取第一行。然后,其输出按 ID2 (C) 进行排序,并且再次保留每个值的第一行 (D)。无论哪一行通过两个过滤器,都会插入到#tt2。
这里棘手的部分是“第一”这个词。关系数据库表没有固有的顺序。查询优化器 (QO) 可以按照它选择的任何顺序自由处理行,只要其结果在逻辑上与提交的 SQL 相同。这让我想知道是否可以在不更改查询的情况下从相同的数据产生不同的结果。我可以。
排序的成本很高,因此 QO 通常会选择其他选项(如果有的话)。其中一种选择是读取索引。我发现使用 #tt.ID1 上的索引,QO 会使用它并跳过排序。SQL Server 的排序是保留顺序的,因此如果我可以以备用顺序显示 ID2,将为每个 ID1 值选择不同的“第一”行,并且总体输出将会不同。我创建了一个与 #tt2 相同的新目标表 #tt3 并运行它(ORDER BY 不会影响此 INSERT)
CREATE CLUSTERED INDEX CIX ON #tt(ID1 ASC, ID2 DESC);
INSERT INTO #tt3 (ID1, ID2, V)
SELECT ID1, ID2, V
FROM #tt;
Run Code Online (Sandbox Code Playgroud)
制作了这个
Row ID1 ID2 V
i 1 1 A
ii 2 2 B
v 4 4 C
vii 5 7 E
Run Code Online (Sandbox Code Playgroud)
与之前的结果 i、ii、iv、vi 不同。
您会注意到,如果您按 ID1 asc、ID1 desc .. 对#tt 的内容进行排序。
Row ID1 ID2 V
i 1 1 A
ii 2 2 B
iii 3 2 B
v 4 4 C
iv 4 3 C
vii 5 7 E
vi 5 6 E
ix 6 7 E
iix 6 6 E
Run Code Online (Sandbox Code Playgroud)
..并应用逻辑“我见过”算法获得此替代结果。
问题仍然存在,为什么 QO 先处理 ID1,然后再处理 ID2?我相信这是由定义约束的顺序驱动的。我预计将 #tt 聚集索引定义为 (ID2 asc, ID1 desc) 会导致首先处理 ID2,因为这会避免排序。它不是。它仍然首先处理 ID1,并带回排序运算符。事实上,在第五列中创建另一个约束将其相应地添加到计划中。似乎按 sys.indexes.index_id 顺序进行处理可能会硬编码在 QO 中?
总而言之,我认为建议谨慎。物理层的变化会影响该查询的结果。如果目标表的约束被删除并重新创建,我们最好希望它们按照之前的顺序返回。如果源表经过优化(例如使用索引),则可能会影响访问路径以及哪一行“首先”出现。这没有考虑分配顺序扫描或旋转木马扫描。
如果涉及更多行,QO 可能会改用散列连接。天知道这会如何影响事情。
归档时间: |
|
查看次数: |
367 次 |
最近记录: |