即使第一个参数不为 NULL,SQL Server 是否会读取所有 COALESCE 函数?

Cur*_*urt 102 null sql-server coalesce

我正在使用 T-SQLCOALESCE函数,其中第一个参数在大约 95% 的运行时间不会为空。如果第一个参数是NULL,第二个参数是一个相当漫长的过程:

SELECT COALESCE(c.FirstName
                ,(SELECT TOP 1 b.FirstName
                  FROM TableA a 
                  JOIN TableB b ON .....)
                )
Run Code Online (Sandbox Code Playgroud)

例如,如果 ,c.FirstName = 'John'SQL Server 仍会运行子查询吗?

我知道使用 VB.NETIIF()函数,如果第二个参数为 True,代码仍会读取第三个参数(即使不会使用它)。

JNK*_*JNK 98

。这是一个简单的测试:

SELECT COALESCE(1, (SELECT 1/0)) -- runs fine
SELECT COALESCE(NULL, (SELECT 1/0)) -- throws error
Run Code Online (Sandbox Code Playgroud)

如果计算第二个条件,则被零除会引发异常。

根据MSDN 文档,这与COALESCE解释器如何查看有关 - 这只是编写CASE语句的一种简单方法。

CASE 众所周知,它是 SQL Server 中唯一(大部分)可靠地短路的函数之一。

与 Aaron Bertrand 在另一个答案中所示的标量变量和聚合进行比较时有一些例外(这将适用于CASECOALESCE):

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;
Run Code Online (Sandbox Code Playgroud)

将产生除以零误差。

这应该被视为一个错误,并且通常COALESCE会从左到右解析。


Aar*_*and 75

这个怎么样 - 正如 Itzik Ben-Gan 向我报告的那样,Jaime Lafargue 告诉了他这件事

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;
Run Code Online (Sandbox Code Playgroud)

结果:

Msg 8134, Level 16, State 1, Line 2
Divide by zero error encountered.
Run Code Online (Sandbox Code Playgroud)

当然有一些微不足道的解决方法,但重点仍然是CASE并不总是保证从左到右的评估/短路。我在这里报告了这个错误,它被关闭为“设计使然”。Paul White 随后提交了这个 Connect 项目,并以 Fixed 方式关闭。不是因为它本身是固定的,而是因为他们更新了联机丛书,更准确地描述了聚合可以更改CASE表达式的计算顺序的场景。我最近在这里写了更多关于这个的博客

编辑只是一个附录,虽然我同意这些是边缘情况,大多数时候你可以依靠从左到右的评估和短路,而且这些是与文档相矛盾的错误,最终可能会被修复(这不是确定的 - 请参阅Bart Duncan 的博客文章中的后续对话以了解原因),当人们说某些事情总是正确的时,即使存在一个反驳它的边缘情况,我也必须不同意。如果 Itzik 和其他人能找到这样的孤立 bug,那么至少在存在其他 bug 的可能性范围内。而且由于我们不知道 OP 的其余查询,我们不能肯定地说他会依赖这种短路,但最终会被它咬住。所以对我来说,更安全的答案是:

虽然您通常可以依靠CASE评估从左到右和短路,如文档中所述,但不能准确地说您总是可以这样做。此页面上有两个不正确的演示案例,并且在任何公开可用的 SQL Server 版本中都没有修复任何错误。

编辑 这里是另一种情况(我需要停止这样做)CASE,即使不涉及聚合,表达式也不会按照您期望的顺序进行计算。

  • 看起来 `CASE` 有另一个问题 [已悄悄修复](http://support.microsoft.com/kb/2894305) (2认同)

Pau*_*ite 39

该文档合理清楚地表明目的是为了CASE短路。正如Aaron 所提到的,有几个报告的实例表明这并不总是正确的。到目前为止,其中大部分已被确认为错误并已修复。

在使用副作用函数或子查询的情况下CASE(因此COALESCE)还有其他问题。考虑:

SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);
SELECT ISNULL((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);
Run Code Online (Sandbox Code Playgroud)

COALESCE表单通常返回 null,如Hugo Kornelis的错误报告中所述。

优化器转换和通用表达式跟踪所证明的问题意味着不可能保证CASE在所有情况下都会短路。

我认为您可以有理由相信这CASE会在一般情况下短路(特别是如果有合理技能的人检查执行计划,并且该执行计划是通过计划指南或提示“强制执行”的)但如果您需要绝对保证,您必须编写完全不包含表达式的 SQL。


Mar*_*ith 21

我遇到了另一种情况,其中CASE/COALESCE不要短路。如果1作为参数传递,以下 TVF 将引发 PK 违规。

CREATE FUNCTION F (@P INT)
RETURNS @T TABLE (
  C INT PRIMARY KEY)
AS
  BEGIN
      INSERT INTO @T
      VALUES      (1),
                  (@P)

      RETURN
  END
Run Code Online (Sandbox Code Playgroud)

如果如下调用

DECLARE @Number INT = 1

SELECT COALESCE(@Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = @Number), 
                         (SELECT TOP (1)  C
                          FROM   F(@Number))) 
Run Code Online (Sandbox Code Playgroud)

或者作为

DECLARE @Number INT = 1

SELECT CASE
         WHEN @Number = 1 THEN @Number
         ELSE (SELECT TOP (1) C
               FROM   F(@Number))
       END 
Run Code Online (Sandbox Code Playgroud)

两者都给出结果

违反 PRIMARY KEY 约束“PK__F__3BD019A800551192”。无法在对象“dbo.@T”中插入重复键。重复的键值为 (1)。

表明SELECT(或至少是表变量人口)仍在执行并引发错误,即使永远不应到达该语句的分支。该COALESCE版本的计划如下。

计划

查询的这种重写似乎避免了这个问题

SELECT COALESCE(Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = Number), 
                         (SELECT TOP (1)  C
                          FROM   F(Number))) 
FROM (VALUES(1)) V(Number)   
Run Code Online (Sandbox Code Playgroud)

这给出了计划

计划2


Mar*_*ith 10

另一个例子

CREATE TABLE T1 (C INT PRIMARY KEY)

CREATE TABLE T2 (C INT PRIMARY KEY)

INSERT INTO T1 
OUTPUT inserted.* INTO T2
VALUES (1),(2),(3);
Run Code Online (Sandbox Code Playgroud)

查询

SET STATISTICS IO ON;

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (LOOP JOIN)
Run Code Online (Sandbox Code Playgroud)

根本不显示任何读取T2

的查找T2是在传递谓词下,并且永远不会执行运算符。但

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (MERGE JOIN)
Run Code Online (Sandbox Code Playgroud)

是否表明T2被读取。即使T2实际上不需要任何价值。

当然,这并不令人惊讶,但我认为值得添加到反例存储库中,因为它引发了短路甚至在基于集合的声明性语言中意味着什么的问题。


Eri*_*ikE 8

我只是想提一个你可能没有考虑过的策略。它在这里可能不匹配,但有时确实派上用场。看看这个修改是否能给你带来更好的性能:

SELECT COALESCE(c.FirstName
            ,(SELECT TOP 1 b.FirstName
              FROM TableA a 
              JOIN TableB b ON .....
              WHERE C.FirstName IS NULL) -- this is the changed part
            )
Run Code Online (Sandbox Code Playgroud)

另一种方法可能是这样(基本上等效,但允许您在必要时从其他查询访问更多列):

SELECT COALESCE(c.FirstName, x.FirstName)
FROM
   TableC c
   OUTER APPLY (
      SELECT TOP 1 b.FirstName
      FROM
         TableA a 
         JOIN TableB b ON ...
      WHERE
         c.FirstName IS NULL -- the important part
   ) x
Run Code Online (Sandbox Code Playgroud)

基本上这是一种“硬”连接表的技术,但包括何时应该连接任何行的条件。根据我的经验,这有时确实有助于执行计划。