我经常读到,当必须检查一行是否存在时,应该始终使用 EXISTS 而不是 COUNT 来完成。
然而,在最近的几个场景中,我测量了使用计数时的性能改进。
模式是这样的:
LEFT JOIN (
SELECT
someID
, COUNT(*)
FROM someTable
GROUP BY someID
) AS Alias ON (
Alias.someID = mainTable.ID
)
Run Code Online (Sandbox Code Playgroud)
我不熟悉判断 SQL Server“内部”发生了什么的方法,所以我想知道 EXISTS 是否存在一个未知的缺陷,这对我所做的测量非常有意义(EXISTS 可能是 RBAR 吗?!)。
你对这种现象有什么解释吗?
编辑:
这是您可以运行的完整脚本:
SET NOCOUNT ON
SET STATISTICS IO OFF
DECLARE @tmp1 TABLE (
ID INT UNIQUE
)
DECLARE @tmp2 TABLE (
ID INT
, X INT IDENTITY
, UNIQUE (ID, X)
)
; WITH T(n) AS (
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master.dbo.spt_values AS S
)
, tally(n) AS (
SELECT
T2.n * 100 + T1.n
FROM T AS T1
CROSS JOIN T AS T2
WHERE T1.n <= 100
AND T2.n <= 100
)
INSERT @tmp1
SELECT n
FROM tally AS T1
WHERE n < 10000
; WITH T(n) AS (
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master.dbo.spt_values AS S
)
, tally(n) AS (
SELECT
T2.n * 100 + T1.n
FROM T AS T1
CROSS JOIN T AS T2
WHERE T1.n <= 100
AND T2.n <= 100
)
INSERT @tmp2
SELECT T1.n
FROM tally AS T1
CROSS JOIN T AS T2
WHERE T1.n < 10000
AND T1.n % 3 <> 0
AND T2.n < 1 + T1.n % 15
PRINT '
COUNT Version:
'
WAITFOR DELAY '00:00:01'
SET STATISTICS IO ON
SET STATISTICS TIME ON
SELECT
T1.ID
, CASE WHEN n > 0 THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
LEFT JOIN (
SELECT
T2.ID
, COUNT(*) AS n
FROM @tmp2 AS T2
GROUP BY T2.ID
) AS T2 ON (
T2.ID = T1.ID
)
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope
SET STATISTICS TIME OFF
PRINT '
EXISTS Version:'
WAITFOR DELAY '00:00:01'
SET STATISTICS TIME ON
SELECT
T1.ID
, CASE WHEN EXISTS (
SELECT 1
FROM @tmp2 AS T2
WHERE T2.ID = T1.ID
) THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope
SET STATISTICS TIME OFF
Run Code Online (Sandbox Code Playgroud)
在 SQL Server 2008R2(7 个 64 位)上,我得到了这个结果
COUNT 版本:
表'#455F344D'。扫描计数 1,逻辑读取 8,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。
表 '#492FC531'。扫描计数 1,逻辑读取 30,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。SQL Server 执行时间:
CPU 时间 = 0 毫秒,已用时间 = 81 毫秒。
EXISTS 版本:
表'#492FC531'。扫描计数 1,逻辑读取 96,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。
表'#455F344D'。扫描计数 1,逻辑读 8,物理读 0,预读 0,lob 逻辑读 0,lob 物理读 0,lob 预读 0。SQL Server 执行时间:
CPU 时间 = 0 毫秒,已用时间 = 76 毫秒。
Pau*_*ite 46
我经常读到,当必须检查一行是否存在时,应该始终使用 EXISTS 而不是 COUNT 来完成。
任何事情都永远正确是非常罕见的,尤其是在数据库方面。有多种方法可以在 SQL 中表达相同的语义。如果有一个有用的经验法则,它可能是使用最自然的可用语法编写查询(并且,是的,这是主观的),并且仅在您获得的查询计划或性能不可接受时才考虑重写。
就其价值而言,我自己对这个问题的看法是存在查询最自然地使用EXISTS. 根据我的经验,比拒绝替代方案EXISTS 更倾向于优化。使用和过滤是另一种选择,它恰好在 SQL Server 查询优化器中有一些支持,但我个人发现这在更复杂的查询中是不可靠的。无论如何,(对我来说)似乎比这两种选择都更自然。OUTER JOINNULLCOUNT(*)=0EXISTS
我想知道 EXISTS 是否存在一个未知的缺陷,它完全符合我所做的测量
您的特定示例很有趣,因为它突出了优化器处理CASE表达式(EXISTS尤其是测试)中子查询的方式。
考虑以下(完全合法的)查询:
DECLARE @Base AS TABLE (a integer NULL);
DECLARE @When AS TABLE (b integer NULL);
DECLARE @Then AS TABLE (c integer NULL);
DECLARE @Else AS TABLE (d integer NULL);
SELECT
CASE
WHEN (SELECT W.b FROM @When AS W) = 1
THEN (SELECT T.c FROM @Then AS T)
ELSE (SELECT E.d FROM @Else AS E)
END
FROM @Base AS B;
Run Code Online (Sandbox Code Playgroud)
的语义CASE是WHEN/ELSE从句通常按文本顺序进行评估。在上面的查询中,如果ELSE子查询返回多于一行,并且WHEN满足子句,则SQL Server 返回错误将是不正确的。为了尊重这些语义,优化器会生成一个使用传递谓词的计划:

嵌套循环连接的内侧仅在传递谓词返回 false 时才计算。总体效果是CASE按顺序测试表达式,并且仅在不满足先前表达式时才评估子查询。
在CASE子查询使用 的情况下EXISTS,逻辑存在性测试作为半连接实现,但通常会被半连接拒绝的行必须保留,以防后面的子句需要它们。流经这种特殊类型的半连接的行获得一个标志,以指示半连接是否找到匹配项。此标志称为探测列。
实现的细节是逻辑子查询被替换为具有探测列的相关连接('apply')。该工作由称为RemoveSubqInPrj(删除投影中的子查询)的查询优化器中的简化规则执行。我们可以使用跟踪标志 8606 查看详细信息:
SELECT
T1.ID,
CASE
WHEN EXISTS
(
SELECT 1
FROM #T2 AS T2
WHERE T2.ID = T1.ID
) THEN 1
ELSE 0
END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8606);
Run Code Online (Sandbox Code Playgroud)
显示EXISTS测试的输入树的一部分如下所示:
ScaOp_Exists
LogOp_Project
LogOp_Select
LogOp_Get TBL: #T2
ScaOp_Comp x_cmpEq
ScaOp_Identifier [T2].ID
ScaOp_Identifier [T1].ID
Run Code Online (Sandbox Code Playgroud)
这被转换为RemoveSubqInPrj以以下为首的结构:
LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)
Run Code Online (Sandbox Code Playgroud)
这是使用前面描述的探针的左半连接应用。此初始转换是迄今为止 SQL Server 查询优化器中唯一可用的转换,如果禁用此转换,编译将会失败。
此查询的一种可能的执行计划形状是该逻辑结构的直接实现:

最终的 Compute ScalarCASE使用探针列值计算表达式的结果:

当优化器为半连接考虑其他物理连接类型时,计划树的基本形状被保留。只有合并连接支持探测列,所以散列半连接虽然在逻辑上是可能的,但不被考虑:

请注意,合并输出一个标记为表达式Expr1008(名称与之前相同是巧合),尽管在计划中的任何运算符上都没有出现它的定义。这又是探针列。和以前一样,最终的计算标量使用这个探测值来评估CASE.
问题是优化器没有完全探索只有合并(或散列)半连接才值得的替代方案。在嵌套循环计划中,T2在每次迭代中检查行是否与范围匹配没有任何好处。使用合并或散列计划,这可能是一个有用的优化。
如果我们在查询中添加一个匹配的BETWEEN谓词 to T2,所发生的一切就是对每一行执行此检查作为合并半连接上的残差(在执行计划中很难发现,但它就在那里):
SELECT
T1.ID,
CASE
WHEN EXISTS
(
SELECT 1
FROM #T2 AS T2
WHERE T2.ID = T1.ID
AND T2.ID BETWEEN 5000 AND 7000 -- New
) THEN 1
ELSE 0
END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;
Run Code Online (Sandbox Code Playgroud)

我们希望BETWEEN谓词被推到T2导致搜索。通常,优化器会考虑这样做(即使查询中没有额外的谓词)。它识别隐含谓词(BETWEENonT1和它们之间的连接谓词T1以及T2它们一起暗示BETWEENon T2),而它们不会出现在原始查询文本中。不幸的是,apply-probe 模式意味着这没有被探索。
有多种方法可以编写查询以在合并半连接的两个输入上生成搜索。一种方法涉及以非常不自然的方式编写查询(打败了我通常更喜欢的原因EXISTS):
WITH T2 AS
(
SELECT TOP (9223372036854775807)
T2.*
FROM #T2 AS T2
WHERE T2.ID BETWEEN 5000 AND 7000
)
SELECT
T1.ID,
DoesExist =
CASE
WHEN EXISTS
(
SELECT
T2.*
FROM T2
WHERE T2.ID = T1.ID
) THEN 1 ELSE 0 END
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;
Run Code Online (Sandbox Code Playgroud)

我不会很高兴在生产环境中编写该查询,这只是为了证明所需的计划形状是可能的。如果您需要编写的实际查询CASE以这种特定方式使用,并且由于在合并半连接的探测端没有搜索而影响性能,您可以考虑使用不同的语法编写查询,以产生正确的结果和更高效的执行计划。
该“COUNT(*)对是否存在”的说法是检查记录是否存在这样做的。例如:
WHERE (SELECT COUNT(*) FROM Table WHERE ID=@ID)>0
Run Code Online (Sandbox Code Playgroud)
对比
WHERE EXISTS(SELECT ID FROM Table WHERE ID=@ID)
Run Code Online (Sandbox Code Playgroud)
您的 SQL 脚本没有COUNT(*)用作记录存在检查,因此我不会说它适用于您的场景。
| 归档时间: |
|
| 查看次数: |
16048 次 |
| 最近记录: |