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


26

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

假设下面的表结构

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

要求是确定可为空的列中的任何一个BC实际上是否包含任何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 3TOP 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一个人却给了我一个(下面的计划)。

无枢轴的

Answers:


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

我喜欢这种方法。不过,在对问题进行编辑时,我会解决一些可能的问题。作为书面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
Smith

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

在使用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

如果添加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

作为参考,我使用Red-gate的SQL Generator生成数据。在我的一百万行中,有9,886行的B值为空,而10019行的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

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

这是表中没有空值时的结果。请注意,就读取和执行时间而言,ypercube提供的2存在解决方案与我的几乎相同。我(我们)认为这是由于使用Advanced Scanning的Enterprise / Developer版本的优势。如果仅使用标准版或更低版本,则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

4

是否IF允许发表声明?

这样一来,您就可以通过表格确认B或C的存在:

DECLARE 
  @A INT, 
  @B CHAR(10), 
  @C CHAR(10)

SET @B = 'X'
SET @C = 'X'

SELECT TOP 1 
  @A = A, 
  @B = B, 
  @C = C
FROM T 
WHERE B IS NULL OR C IS NULL 

IF @@ROWCOUNT = 0 
BEGIN 
  SELECT 'No nulls'
  RETURN
END

IF @B IS NULL AND @C IS NULL
BEGIN
  SELECT 'Both null'
  RETURN
END 

IF @B IS NULL 
BEGIN
  SELECT TOP 1 
    @C = C
  FROM T
  WHERE A > @A
  AND C IS NULL

  IF @B IS NULL AND @C IS NULL 
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'B is null'
    RETURN
  END
END

IF @C IS NULL 
BEGIN
  SELECT TOP 1 
    @B = B
  FROM T 
  WHERE A > @A
  AND B IS NULL

  IF @C IS NULL AND @B IS NULL
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'C is null'
    RETURN
  END
END      

4

经过SQL-Fiddle测试,版本:2008 r22012,具有3万行。

  • EXISTS查询在尽早发现Null的情况下显示出效率上的巨大好处-这是预期的。
  • 我在EXISTS查询中获得了更好的性能-在2012年的所有情况下,我都无法解释。
  • 在2008R2中,当没有Null时,它比其他两个查询慢。它发现Nulls的时间越早,获得的速度就越快,并且当两列中的Null都早于Null时,它比其他两个查询快得多。
  • 与马丁的CASE查询相比,托马斯·凯瑟(Thomas Kejser)的查询在2012年的表现似乎略有提高,但在2008R2中却不断下降。
  • 2012版似乎有更好的性能。但是,这可能与SQL-Fiddle服务器的设置有关,而不仅与优化程序的改进有关。

查询和时间安排。完成的时间:

  • 一无所有
  • 第二栏B有一个NULL小柱子id
  • 第三列,两列NULL各有一个小id。

我们开始(计划存在问题,我将在以后再试。请立即单击链接):


用2个EXISTS子查询进行查询

SELECT 
      CASE WHEN EXISTS (SELECT * FROM test WHERE b IS NULL)
             THEN 1 ELSE 0 
      END AS B,
      CASE WHEN EXISTS (SELECT * FROM test WHERE c IS NULL)
             THEN 1 ELSE 0 
      END AS C ;

-------------------------------------
Times in ms (2008R2): 1344 - 596 -  1  
Times in ms   (2012):   26 -  14 -  2

Martin Smith的单一汇总查询

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

--------------------------------------
Times in ms (2008R2):  558 - 553 - 516  
Times in ms   (2012):   37 -  35 -  36

Thomas Kejser的查询

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

--------------------------------------
Times in ms (2008R2):  859 - 705 - 668  
Times in ms   (2012):   24 -  19 -  18

我的建议(1)

WITH tmp1 AS
  ( SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id 
  ) 

  SELECT 
      tmp1.*, 
      NULL AS id2, NULL AS b2, NULL AS c2
  FROM tmp1
UNION ALL
  SELECT *
  FROM
    ( SELECT TOP (1)
          tmp1.id, tmp1.b, tmp1.c,
          test.id AS id2, test.b AS b2, test.c AS c2 
      FROM test
        CROSS JOIN tmp1
      WHERE test.id >= tmp1.id
        AND ( test.b IS NULL AND tmp1.c IS NULL
           OR tmp1.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id
    ) AS x ;

--------------------------------------
Times in ms (2008R2): 1089 - 572 -  16   
Times in ms   (2012):   28 -  15 -   1

它需要对输出进行一些优化,但效率类似于EXISTS查询。我以为没有空值会更好,但测试表明没有。


建议(2)

试图简化逻辑:

CREATE TABLE tmp
( id INT
, b CHAR(1000)
, c CHAR(1000)
) ;

DELETE  FROM tmp ;

INSERT INTO tmp 
    SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id  ; 

INSERT INTO tmp 
    SELECT TOP (1)
        test.id, test.b, test.c 
      FROM test
        JOIN tmp 
          ON test.id >= tmp.id
      WHERE ( test.b IS NULL AND tmp.c IS NULL
           OR tmp.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id ;

SELECT *
FROM tmp ;

它在2008R2中的表现似乎比以前的建议更好,但在2012年则更糟(也许INSERT可以使用来重写第二个IF,例如@ 8kb的答案):

------------------------------------------
Times in ms (2008R2): 416+6 - 1+127 -  1+1   
Times in ms   (2012):  14+1 - 0+27  -  0+29

0

当您使用EXISTS时,SQL Server会知道您正在进行状态检查。找到第一个匹配值时,它返回TRUE并停止查找。

当您合并2列并且如果任何列为null时,结果将为null

例如

null + 'a' = null

所以检查一下这段代码

IF EXISTS (SELECT 1 FROM T WHERE B+C is null)
SELECT Top 1 ISNULL(B,'B ') + ISNULL(C,'C') as [Nullcolumn] FROM T WHERE B+C is null

-3

怎么样:

select 
    exists(T.B is null) as 'B is null',
    exists(T.C is null) as 'C is null'
from T;

如果此方法有效(我尚未测试过),它将产生一个包含2列的单行表,每列为TRUE或FALSE。我没有测试效率。


2
即使这在其他任何DBMS中都有效,我也怀疑它具有正确的语义。假设T.B is null然后将其视为布尔结果,EXISTS(SELECT true)并且EXISTS(SELECT false)都将返回true。这个MySQL示例表明实际上两个列都不包含NULL
Martin Smith
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.