SQL和逻辑运算符以及空值检查

10

多年来,我在使用SQL Server时有一个模糊的记忆,可能是从机械式复制中获得的,即当你有一个可能为null的列时,编写“WHERE”子句谓词不安全。例如:

 ... WHERE the_column IS NULL OR the_column < 10 ...

这似乎与SQL规则不规定短路有关(实际上可能出于查询优化原因,这可能是个坏主意),因此“<”比较(或其他任何操作)即使列的值为null也会被评估。现在,为什么会是个可怕的事情,我不知道,但我记得某些文档曾严厉警告要始终将其编码为“CASE”子句:

 ... WHERE 1 = CASE WHEN the_column IS NULL THEN 1 WHEN the_column < 10 THEN 1 ELSE 0 END ...

(这个古怪的"1 ="部分是因为SQL Server没有/没有第一类布尔值,或者至少我认为它没有。)

所以我的问题是:

  1. 对于SQL Server(或者可能是回退的SQL Server 2000或2005),这是否属实,还是只是我的疑惑?
  2. 如果是这样,PostgreSQL是否也适用相同的注意事项?(8.4如果有关系)
  3. 到底问题出在哪里?是否与索引的工作方式有关?

我对SQL的基础知识还比较薄弱。


1
也许他们在谈论AND?由于null AND任何东西都是null,因此当表达式可能包含null项时,需要使用coalesce或case。 - Craig Ringer
6个回答

11
我不了解SQL Server,因此无法发表意见。
对于某些逻辑运算符L和表达式a L b,没有保证a会在b之前或之后被计算,甚至可能不会同时计算a和b:

表达式求值规则

子表达式的计算顺序未定义。特别地,运算符或函数的输入未必按照从左到右或其他固定顺序进行计算。

此外,如果一个表达式的结果只需计算其中一部分即可确定,则其他子表达式可能根本不需要计算。
[...]
注意,这与某些编程语言中的布尔运算符从左到右的“短路”不同。

因此,在复杂表达式中使用带有副作用的函数是不明智的。在 WHEREHAVING 子句中依赖于副作用或计算顺序尤其危险,因为这些子句在制定执行计划时会被广泛重新处理。

就形如下面的表达式而言:

the_column IS NULL OR the_column < 10

就此而言,不用担心,因为对于所有的n,包括NULL,NULL < n都是NULL;而且,NULL < NULL的结果也是NULL;此外,NULL不是真实的。
null is null or null < 10

这只是一种复杂的说法,表示true或null,而且无论哪个子表达式先被评估,结果都是true

整个“使用CASE”的听起来大多像对我来说是货物崇拜SQL。然而,像大多数货物崇拜主义一样,在货物下面有一个真理核心; 在我从PostgreSQL手册中摘录的第一个段落下面,您会发现以下内容:

When it is essential to force evaluation order, a CASE construct (see Section 9.16) can be used. For example, this is an untrustworthy way of trying to avoid division by zero in a WHERE clause:

SELECT ... WHERE x > 0 AND y/x > 1.5;

But this is safe:

SELECT ... WHERE CASE WHEN x > 0 THEN y/x > 1.5 ELSE false END;

因此,如果您需要防范可能会引发异常或产生其他副作用的条件,那么您应该使用 CASE 来控制评估顺序,因为 CASE 是按顺序进行评估的:

每个条件都是返回 boolean 结果的表达式。如果条件的结果为 true,则 CASE 表达式的值为后续的 结果,并且不处理 CASE 表达式的其余部分。如果条件的结果不为 true,则以同样的方式检查任何后续 WHEN 子句。

因此,假设有以下内容:

case when A then Ra
     when B then Rb
     when C then Rc
     ...

A会在B之前被评估,BC之前被评估,以此类推,并且当一个条件计算为true值时,评估停止。

总之,CASE短路但ANDOR不会短路,因此仅需要在需要保护副作用时使用CASE


1
是的,谢谢;我明白 SQL 没有强制执行短路(或者说“非短路”)规则这一事实。问题实际上是关于普通关系比较是否会对一个可能为空的列进行评估,是否会发生什么可怕的事情。感谢您提供非常详细的答案。 - Pointy

2

不要使用

the_column IS NULL OR the_column < 10

我可以

isnull(the_column,0) < 10

或者以第一个例子为例。
WHERE 1 = CASE WHEN isnull(the_column,0) < 10 THEN 1 ELSE 0 END ...

1

我从未听说过这样的问题,而SQL Server 2000文档中的一个示例使用了WHERE advance < $5000 OR advance IS NULL,因此它肯定不是一个非常严格的规则。我对OR的唯一担忧是它的优先级低于AND,因此您可能会意外地编写类似于WHERE the_column IS NULL OR the_column < 10 AND the_other_column > 20的语句,但通常的解决方案是使用括号而不是大型CASE表达式。

我认为在大多数RDBMS中,索引不包括空值,因此对the_column进行索引对于此查询并不是非常有用;但即使不是这种情况,我也不明白为什么大型CASE表达式会更加友好。

(当然,很难证明否定,也许其他人会知道您所指的是什么?)


1

嗯,我一直以来都写了像第一个例子那样的查询(甚至我写了生成这种查询的查询生成器),但我从未遇到过问题。

我认为你可能记得有人曾经告诫过你不要编写使用OR的奇怪的连接条件。在你的第一个例子中,由OR连接的条件限制了同一张表的同一列,这是可以的。如果你的第二个条件是一个连接条件(即它限制了来自两个不同表的列),那么你可能会陷入糟糕的情况,查询规划器只能使用笛卡尔积(非常糟糕!!!)。

我认为你的CASE函数在那里并没有做什么,除了可能阻碍你的查询规划器寻找好的查询执行计划。

但更普遍地说,先编写简单明了的查询,然后查看它在实际数据下的性能。不需要担心可能根本不存在的问题!


0
另一个 CASE 有用的例子是在 varchar 列上使用日期函数时。在使用 convert(colA,datetime) 之前添加 ISDATE 可能不起作用,当 colA 包含非日期数据时,查询可能出错。

0

空值可能会让人感到困惑。如果您正在尝试将 Null 或值作为参数传递," ... WHERE 1 = CASE ... " 是非常有用的,例如 "WHERE the_column = @parameter。本文可能会对您有所帮助 使用 OLEDB 传递 Null


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接