一尘不染

如何有效地检查多列上的 EXISTS?

sql-server

这是我定期遇到的一个问题,但尚未找到好的解决方案。

假设如下表结构

CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)

并且要求是确定任一可空列BC实际上是否包含任何NULL值(如果是,则包含哪些值)。

还假设该表包含数百万行(并且没有可用的列统计信息可以查看,因为我对此类查询的更通用的解决方案感兴趣)。

我可以想到几种方法来解决这个问题,但都有弱点。

两个单独的EXISTS陈述。这将具有允许查询在找到 a 时尽早停止扫描的优点NULL。但是如果两列实际上都包含 no 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)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+

对于@Thomas 的回答,我改为TOP 3可能TOP 2允许它提前退出。默认情况下,我为该答案制定了并行计划,因此还尝试了MAXDOP 1提示,以使读取次数与其他计划更具可比性。我对结果感到有些惊讶,因为在我之前的测试中,我在没有阅读整个表格的情况下就看到了查询短路。

我的短路测试数据计划如下

短路

ypercube 的数据计划是

非短路

因此,它在计划中添加了一个阻塞排序运算符。我也尝试了HASH GROUP提示,但最终还是读取了所有行

非短路

因此,关键似乎是让hash match (flow distinct)运营商允许该计划短路,因为其他替代方案无论如何都会阻塞并消耗所有行。我认为没有暗示可以特别强制执行此操作,但显然“通常,优化器会选择一个 Flow Distinct,它确定所需的输出行数少于输入集中的不同值。” .

@ypercube 的数据在每列中只有 1 行带有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

我发生的最后一个调整是,如果上面的查询遇到的第一行在NULLBC. 它将继续扫描而不是立即退出。避免这种情况的一种方法是在扫描行时对其进行反透视。所以我对Thomas Kejser 的回答的最后修改如下

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个人给了我(下面的计划)。

无枢轴


阅读 89

收藏
2022-11-13

共1个答案

一尘不染

怎么样:

SELECT TOP 3 *
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 T 
  WHERE 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT
2022-11-13