这是我定期遇到的一个问题,尚未找到一个好的解决方案。
假设下面的表结构
CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)
要求是确定可为空的列中的任何一个B或C实际上是否包含任何NULL值(以及是否包含任何值)。
还要假设该表包含数百万行(并且没有可用的列统计信息,因为我对此类查询的更通用解决方案感兴趣)。
我可以想到几种解决方法,但都有缺点。
两个单独的EXISTS语句。这样的好处是,一旦NULL找到a,查询就可以立即停止扫描。但是,如果两列实际上都不包含,NULL则将进行两次完整扫描。
单一汇总查询
SELECT 
    MAX(CASE WHEN B IS NULL THEN 1 ELSE 0 END) AS B,
    MAX(CASE WHEN C IS NULL THEN 1 ELSE 0 END) AS C
FROM T
这可能会同时处理两个列,因此最糟糕的情况是一次完整扫描。缺点是,即使NULL在查询的两个很早就在两列中都遇到了a ,仍将最终扫描整个表的其余部分。
用户变量
我可以想到第三种方式
BEGIN TRY
DECLARE @B INT, @C INT, @D INT
SELECT 
    @B = CASE WHEN B IS NULL THEN 1 ELSE @B END,
    @C = CASE WHEN C IS NULL THEN 1 ELSE @C END,
    /*Divide by zero error if both @B and @C are 1.
    Might happen next row as no guarantee of order of
    assignments*/
    @D = 1 / (2 - (@B + @C))
FROM T  
OPTION (MAXDOP 1)       
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8134 /*Divide by zero*/
    BEGIN
    SELECT 'B,C both contain NULLs'
    RETURN;
    END
ELSE
    RETURN;
END CATCH
SELECT ISNULL(@B,0),
       ISNULL(@C,0)
但这不适用于生产代码,因为未定义聚合级联查询的正确行为。无论如何,通过抛出错误来终止扫描是一个非常糟糕的解决方案。
是否有另一种选择结合了上述方法的优势?
编辑
只是为了更新此结果,我获得了到目前为止提交的答案的阅读结果(使用@ypercube的测试数据)
+----------+------------+------+---------+----------+----------------------+----------+------------------+
|          | 2 * EXISTS | CASE | Kejser  |  Kejser  |        Kejser        | ypercube |       8kb        |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
|          |            |      |         | MAXDOP 1 | HASH GROUP, MAXDOP 1 |          |                  |
| No Nulls |      15208 | 7604 |    8343 | 7604     | 7604                 |    15208 | 8346 (8343+3)    |
| One Null |       7613 | 7604 |    8343 | 7604     | 7604                 |     7620 | 7630 (25+7602+3) |
| Two Null |         23 | 7604 |    8343 | 7604     | 7604                 |       30 | 30 (18+12)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
对于@托马斯的答案,我改变了TOP 3对TOP 2潜在允许它更早退出。默认情况下,我为该答案制定了一个并行计划,因此还尝试了一个MAXDOP 1提示,以使读取次数与其他计划更具可比性。我对结果感到有些惊讶,因为在较早的测试中,我看到查询短路而没有读取整个表。
我的测试数据计划如下

ypercube数据的计划是

因此,它向计划添加了一个阻塞排序运算符。我也尝试过使用HASH GROUP提示,但最终仍会读取所有行

因此,关键似乎是要让hash match (flow distinct)运营商允许该计划短路,因为其他替代方案仍然会阻塞并消耗所有行。我认为没有迹象表明要强制执行此操作,但是显然“通常,优化器选择流唯一性,它确定所需的输出行少于输入集中不同的值。” 。
@ypercube的数据每列中只有一行具有NULL值(表基数= 30300),并且估计出入运算符的行均为1。通过使谓词对优化器更加不透明,它使用Flow Distinct运算符生成了一个计划。
SELECT TOP 2 *
FROM (SELECT DISTINCT 
        CASE WHEN b IS NULL THEN NULL ELSE 'foo' END AS b
      , CASE WHEN c IS NULL THEN NULL ELSE 'bar' END AS c
  FROM test T 
  WHERE LEFT(b,1) + LEFT(c,1) IS NULL
) AS DT 
编辑2
我发生的最后一个调整是,如果遇到的第一行在和NULL列中都为NULL ,则上面的查询仍然可能会处理超出必要数量的行。它会继续扫描而不是立即退出。避免这种情况的一种方法是在扫描行时使其不旋转。所以我对Thomas Kejser的答案的最后修正是BC
SELECT DISTINCT TOP 2 NullExists
FROM test T 
CROSS APPLY (VALUES(CASE WHEN b IS NULL THEN 'b' END),
                   (CASE WHEN c IS NULL THEN 'c' END)) V(NullExists)
WHERE NullExists IS NOT NULL
对于谓词可能会更好,WHERE (b IS NULL OR c IS NULL) AND  NullExists IS NOT NULL但相对于先前的测试数据,一个人没有给我一个具有Flow Distinct的计划,而NullExists IS NOT NULL一个人却给了我一个(下面的计划)。

TOP 3可能仅仅是TOP 2因为目前它会扫描,直到找到以下每一个(NOT_NULL,NULL),(NULL,NOT_NULL),(NULL,NULL)。这3个中的任何2个就足够了-如果找到(NULL,NULL)第一个,则也不需要第二个。同样,为了缩短计划,该计划将需要通过hash match (flow distinct)运营商来实现,而不是hash match (aggregate)distinct sort