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

Mar*_*ith 26 performance sql-server

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

假设如下表结构

CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)
Run Code Online (Sandbox Code Playgroud)

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

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

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

两个单独的EXISTS声明。这将具有允许查询在NULL发现a 后尽早停止扫描的优点。但是如果两列实际上都不包含NULLs,那么将导致两次完整扫描。

单一聚合查询

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
Run Code Online (Sandbox Code Playgroud)

这可以同时处理两列,因此最坏的情况是一次完整扫描。缺点是即使它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)
Run Code Online (Sandbox Code Playgroud)

但这不适用于生产代码,因为未定义聚合串联查询的正确行为。无论如何,通过抛出错误来终止扫描是一个非常糟糕的解决方案。

是否有另一种选择结合了上述方法的优点?

编辑

只是为了用我迄今为止提交的答案的读取结果更新这个结果(使用@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)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
Run Code Online (Sandbox Code Playgroud)

对于@托马斯的答案,我改变了TOP 3TOP 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 
Run Code Online (Sandbox Code Playgroud)

编辑 2

我想到的最后一个调整是,如果遇到 a 的第一行在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
Run Code Online (Sandbox Code Playgroud)

对于谓词而言,最好是WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULL针对先前的测试数据,即没有给我一个具有 Flow Distinct 的计划,而有NullExists IS NOT NULL一个(计划如下)。

未旋转

Tho*_*ser 20

怎么样:

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
Run Code Online (Sandbox Code Playgroud)


Tho*_*mas 6

正如我所理解的问题,您想知道任何列值中是否存在空值,而不是实际返回 B 或 C 为空的行。如果是这样,那为什么不呢:

Select Top 1 'B as nulls' As Col
From T
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T
Where T.C Is Null
Run Code Online (Sandbox Code Playgroud)

在我使用 SQL 2008 R2 和一百万行的测试设备上,我从“客户端统计”选项卡中获得了以下以毫秒为单位的结果:

Kejser                          2907,2875,2829,3576,3103
ypercube                        2454,1738,1743,1765,2305
OP single aggregate solution    (stopped after 120,000 ms) Wouldn't even finish
My solution                     1619,1564,1665,1675,1674
Run Code Online (Sandbox Code Playgroud)

如果添加 nolock 提示,结果会更快:

Select Top 1 'B as nulls' As Col
From T With(Nolock)
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T With(Nolock)
Where T.C Is Null

My solution (with nolock)       42,70,94,138,120
Run Code Online (Sandbox Code Playgroud)

作为参考,我使用 Red-gate 的 SQL Generator 来生成数据。在我的一百万行中,有 9,886 行的 B 值为空,10,019 行的 C 值为空。

在这一系列测试中,B 列中的每一行都有一个值:

Kejser                          245200  Scan count 1, logical reads 367259, physical reads 858, read-ahead reads 367278
                                250540  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367280

ypercube(1)                     249137  Scan count 2, logical reads 367276, physical reads 850, read-ahead reads 367278
                                248276  Scan count 2, logical reads 367276, physical reads 869, read-ahead reads 368765

My solution                     250348  Scan count 2, logical reads 367276, physical reads 858, read-ahead reads 367278
                                250327  Scan count 2, logical reads 367276, physical reads 854, read-ahead reads 367278
Run Code Online (Sandbox Code Playgroud)

每个测试(两套)之前,我跑CHECKPOINTDBCC DROPCLEANBUFFERS

以下是表中没有空值时的结果。请注意,ypercube 提供的 2exists 解决方案在读取和执行时间方面与我的几乎相同。我(我们)相信这是由于使用高级扫描的企业/开发人员版的优势。如果您只使用标准版或更低版本,Kejser 的解决方案很可能是最快的解决方案。

Kejser                          248875  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367290

ypercube(1)                     243349  Scan count 2, logical reads 367265, physical reads 851, read-ahead reads 367278
                                242729  Scan count 2, logical reads 367265, physical reads 858, read-ahead reads 367276
                                242531  Scan count 2, logical reads 367265, physical reads 855, read-ahead reads 367278

My solution                     243094  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
                                243444  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
Run Code Online (Sandbox Code Playgroud)