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


98

我正在使用T-SQL COALESCE函数,其中第一个参数在运行的大约95%的时间内不会为null。如果第一个参数为NULL,则第二个参数将是一个漫长的过程:

SELECT COALESCE(c.FirstName
                ,(SELECT TOP 1 b.FirstName
                  FROM TableA a 
                  JOIN TableB b ON .....)
                )

例如,c.FirstName = 'John'如果SQL Server仍然可以运行子查询?

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

Answers:


95

都能跟得上。这是一个简单的测试:

SELECT COALESCE(1, (SELECT 1/0)) -- runs fine
SELECT COALESCE(NULL, (SELECT 1/0)) -- throws error

如果对第二个条件求值,则会抛出一个除以零的异常。

根据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;

将产生除以零的误差。

应该将其视为错误,并且通常COALESCE会从左到右进行解析。


6
@JNK请查看我的答案,以了解一个不成立的非常简单的情况(我担心的是,还有更多但尚未发现的场景-很难同意CASE始终评估从左到右并总是短路)。
阿龙贝特朗

4
其他有趣的行为@SQLKiwi指向我:SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 1 END), 1);-重复多次。您NULL有时会得到。与再试一次ISNULL-你永远不会NULL...
阿龙贝特朗


@马丁是的,我相信。但是,除非他们听说(或被)这个问题困扰,否则大多数用户不会发现这些行为是直观的。
阿龙贝特朗

73

由Jaime Lafargue告诉我的Itzik Ben-Gan向我报告的那个怎么样?

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;

结果:

Msg 8134, Level 16, State 1, Line 2
Divide by zero error encountered.

当然,有很简单的解决方法,但重点仍然是CASE不能始终保证从左到右的评估/短路。我在这里报告了该错误,并将其关闭为“设计使然”。保罗·怀特(Paul White)随后提交了此Connect项目,并且已按固定关闭。不是因为它本身是固定的,而是因为他们用更准确的描述更新了联机丛书,在这种情况下,聚合可以更改CASE表达式的评估顺序。我最近在这里写了更多有关此的博客

编辑只是一个附录,尽管我同意这些是极端情况,但大多数时候您可以依靠从左到右的评估和短路,并且这些是与文档相矛盾的错误,并且最终可能会得到修复(这是不确定的-请参阅Bart Duncan的博客文章中的后续对话,以了解原因),当人们说某事总是对的,即使有一个极端的情况不能证明这一点,我也不得不不同意。如果Itzik和其他人可以找到这样的单独错误,那么至少在可能的范围内,还存在其他错误。而且由于我们不了解OP的其余查询,因此不能确定地说他会依靠这种短路,但最终会被其咬伤。所以对我来说,更安全的答案是:

如文档中所述,虽然通常可以依靠它CASE来评估从左到右和短路,但是说总是可以这样做并不太准确。在此页面上有两种已证明的情况,这是不正确的,并且在任何公共可用的SQL Server版本中均未修复任何错误。

编辑 这里是另一种情况(我需要停止这样做),其中CASE表达不你所期望的顺序计算,即使没有聚集的参与。


2
而且看起来好像有另一个问题CASE 是静静地固定
马丁·史密斯

IMO不能证明不能保证CASE表达式的评估,因为合计值是在选择之前计算出来的(以便可以在having内部使用)。
Salman A

1
@SalmanA我不知道这还能做什么,除了确切证明不能保证CASE表达式中的求值顺序。我们之所以遇到一个例外,是因为首先要计算汇总,即使该汇总位于ELSE子句中(如果您按文档进行操作)也绝不能达到。
亚伦·伯特兰

@AaronBertrand聚合是 CASE语句之前计算的(它们应该是IMO)。修订的文档正是指出这一点,即在评估CASE 之前发生了错误。
Salman A

@SalmanA它仍然向临时开发人员表明CASE表达式不能按其编写的顺序求值-如果您要尝试做的只是了解为什么错误来自CASE分支而不应该这样做,那么底层机制就无关紧要了。还没到。您是否也反对此页面上的所有其他示例?
亚伦·伯特兰

37

我对此的看法是,该文档清楚地表明,目的是使CASE发生短路。正如亚伦(Aaron)所提到的,在许多情况下(ha!),这并不总是正确的。

到目前为止,所有这些都已被确认为错误并已修复-尽管不一定在您今天可以购买和修补的SQL Server版本中存在(不断折叠的错误尚未纳入累积更新AFAIK)。最新的潜在错误(最初由Itzik Ben-Gan报告)尚待调查(或者Aaron或我不久将其添加到Connect中)。

与原始问题有关,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);

COALESCE表单通常返回NULL,有关更多详细信息,访问https://connect.microsoft.com/SQLServer/feedback/details/546437/coalesce-subquery-1-may-return-null

优化器转换和公共表达跟踪所表现出的问题意味着不可能保证CASE在所有情况下都会短路。我可以设想一些情况,甚至可能无法通过检查公共表演计划的输出来预测行为,尽管今天我对此没有任何评论。

总而言之,我认为您可以肯定CASE总体上会发生短路(尤其是如果一个熟练的技术人员检查了执行计划,并且执行计划被计划指南或提示“强制执行了”),但是如果您需要绝对保证,您必须编写完全不包含表达式的SQL。

我想这不是非常令人满意的状况。


18

我遇到了另一种情况,其中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

如果如下调用

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

或作为

DECLARE @Number INT = 1

SELECT CASE
         WHEN @Number = 1 THEN @Number
         ELSE (SELECT TOP (1) C
               FROM   F(@Number))
       END 

两者都给出结果

违反主键约束'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)   

给出计划

计划2


8

另一个例子

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);

查询

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)

完全不显示任何读取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)

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

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


7

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

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
            )

这样做的另一种方法是(基本上是等效的,但是如果需要的话,允许您从其他查询访问更多列):

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

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


3

不,不会。它只会在c.FirstNameis 时运行NULL

但是,您应该自己尝试。实验。您说您的子查询很长。基准测试。对此得出自己的结论。

正在运行的子查询上的@Aaron答案更加完整。

但是,我仍然认为您应该重新编写查询并使用LEFT JOIN。在大多数情况下,可以通过将查询修改为使用来删除子查询LEFT JOIN

使用子查询的问题在于,由于为主查询的结果集中的每一行都运行了子查询,因此整体语句的运行速度会变慢。


@Adrian还是不对。查看执行计划,您会发现子查询通常很巧妙地转换为JOIN。假设必须为每一行一遍又一遍地运行整个子查询,这仅仅是一个思想实验错误,尽管如果选择了一个嵌套循环与一个扫描联接,则可以有效地发生这种情况。
ErikE 2012年

3

实际的标准要求必须解析所有WHEN子句(以及ELSE子句)才能确定整个表达式的数据类型。我真的必须拿出一些旧笔记来确定如何处理错误。但是就一开始,1/0使用整数,因此我认为这是一个错误。整数数据类型错误。当合并列表中仅包含空值时,确定数据类型会有些棘手,这是另一个问题。

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.