这是我定期遇到的一个问题,但尚未找到好的解决方案。
假设如下表结构
CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)
并且要求是确定任一可空列B
或C
实际上是否包含任何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
我发生的最后一个调整是,如果上面的查询遇到的第一行在NULL
列B
和C
. 它将继续扫描而不是立即退出。避免这种情况的一种方法是在扫描行时对其进行反透视。所以我对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
个人给了我(下面的计划)。
怎么样:
As I understand the question, you want to know whether a null exists in any of the columns values as opposed to actually returning the rows in which either B or C is null. If that is the case, then why not:
On my test rig with SQL 2008 R2 and one million rows, I got the following results in ms from the Client Statistics tab:
If you add the nolock hint, the results are even faster:
For reference I used Red-gate's SQL Generator to generate the data. Out of my one million rows, 9,886 rows had a null B value and 10,019 had a null C value.
In this series of tests, every row in column B has a value:
Before each test (both sets) I ran
CHECKPOINT
andDBCC DROPCLEANBUFFERS
.这是表中没有空值时的结果。请注意,ypercube 提供的 2 exists 解决方案在读取和执行时间方面与我的几乎相同。我(我们)相信这是由于 Enterprise/Developer 版本使用Advanced Scanning的优势。如果您只使用标准版或更低版本,Kejser 的解决方案很可能是最快的解决方案。
在 SQL-Fiddle 中测试的版本:2008 r2和2012,有 30K 行。
EXISTS
查询及早发现 Null 时,该查询在效率方面显示出巨大的优势——这是意料之中的。EXISTS
- 在 2012 年的所有情况下,我无法解释。CASE
。查询和计时。完成时间:
B
有一个NULL
小的id
。NULL
各有一个小ID。开始吧(计划有问题,我稍后再试。现在点击链接):
使用 2 个 EXISTS 子查询进行查询
Martin Smith 的单一聚合查询
Thomas Kejser 的询问
我的建议(一)
它需要对输出进行一些抛光,但效率与
EXISTS
查询相似。我认为没有空值会更好,但测试表明事实并非如此。建议 (2)
试图简化逻辑:
它在 2008R2 中的表现似乎比之前的建议更好,但在 2012 年更差(也许第二个
INSERT
可以用 重写IF
,就像@8kb 的答案):是否
IF
允许陈述?这应该允许您通过表一次确认 B 或 C 的存在:
当您使用 EXISTS 时,SQL Server 知道您正在执行存在性检查。当它找到第一个匹配值时,它返回 TRUE 并停止查找。
当您连接 2 列并且如果任何为空,则结果将为空
例如
所以检查这段代码
怎么样:
如果这可行(我还没有测试过),它将产生一个包含 2 列的单行表,每一列都是 TRUE 或 FALSE。我没有测试效率。