这不是关于接受用户输入或使用变量的全部查询的问题。
这完全是关于使用
ISNULL()
的查询的问题在WHERE
子句中将NULL
值替换为金丝雀值以与谓词进行比较,并采用不同的方式将那些查询重写为SQL Server中的SARGable。为什么您不可以坐在那儿?
我们的示例查询针对SQL Server 2016上Stack Overflow数据库的本地副本,并查找年龄为
NULL
或年龄小于18的用户。SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;
查询计划显示对相当周到的非聚集索引的扫描。
扫描运算符显示(感谢实际执行的补充)在SQL Server的最新版本中计划XML),我们将读取每一个臭皮的行。
总体而言,我们进行9157次读取,并占用了大约半秒的CPU时间:
Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 485 ms, elapsed time = 483 ms.
问题:
有什么方法可以重写此查询以使其更有效,甚至可以使SARGable?
随时提出其他建议。我认为我的答案不一定是答案,并且有足够的聪明人提出一些更好的选择。
如果您想在自己的计算机上玩,那就去吧在这里下载SO数据库。
谢谢!
#1 楼
答案部分有多种方法可以使用不同的T-SQL构造重写它。我们将在下面讨论优缺点,并进行整体比较。效率更高的Seek计划,可以读取我们需要的确切行数,但是它在查询计划中添加了技术领域称为
OR
的内容。还请注意, Seek在这里执行了两次,这实际上应该从图形运算符中更加明显:
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;
第二:使用派生表使用
OR
我们的查询也可以像这样重写
Table 'Users'. Scan count 2, logical reads 8233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 469 ms, elapsed time = 473 ms.
这产生了相同类型的计划,而malarkey少得多,而且程度明显关于索引被搜索(查找)的次数的诚实性。关闭100毫秒的CPU时间。
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records);
但是, e在这里要特别小心,因为如果该计划尝试并行执行,则两个单独的
a whole mess of malarkey
操作将被序列化,因为它们每个都被视为全局标量集合。如果我们使用跟踪标志8649强制执行并行计划,问题将变得很明显。CPU time = 313 ms, elapsed time = 315 ms.
现在,执行Seek的两个节点都已完全并行化,直到我们命中了串联运算符。 />就其价值而言,完全并行的版本具有一定的优势。以大约100次读取和大约90ms的额外CPU时间为代价,经过的时间减少到93ms。
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
CROSS APPLY怎么办?
没有
UNION ALL
的魔力,答案是不完整的!不幸的是,我们在
OR
上遇到了更多问题。SELECT SUM(Records)
FROM
(
SELECT 1
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
这个计划太可怕了。这是您最后一次出现在圣帕特里克节时最终想到的一种计划。尽管很好地并行,但由于某种原因,它正在扫描PK / CX。 w该计划的成本为2198美元。
Table 'Users'. Scan count 12, logical reads 8317, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 500 ms, elapsed time = 93 ms.
这是一个奇怪的选择,因为如果我们强迫它使用非聚集索引,则成本显着下降至1798查询美元。
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT COUNT(Id)
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
嘿,求求!在那儿检查你。另请注意,有了
COUNT
的魔力,我们不需要做任何事就能拥有几乎完全平行的计划。Table 'Users'. Scan count 7, logical reads 31676233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 29532 ms, elapsed time = 5828 ms.
如果没有
CROSS APPLY
的东西,交叉应用最终会更好。SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT COUNT(Id)
FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
该计划看起来不错,但是读取和CPU却没有改善。<
Table 'Users'. Scan count 5277838, logical reads 31685303, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 27625 ms, elapsed time = 4909 ms.
将叉形重写为派生联接的结果完全相同。我不会重新发布查询计划和统计信息-它们确实没有改变。
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT 1
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
关系代数:
为求透彻,并防止Joe Celko困扰我的梦想,我们至少需要尝试一些奇怪的关系事物。
尝试
COUNT
Table 'Users'. Scan count 20, logical reads 17564, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 4844 ms, elapsed time = 863 ms.
SELECT COUNT(u.Id)
FROM dbo.Users AS u
JOIN
(
SELECT u.Id
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT u.Id
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x ON x.Id = u.Id;
/>这是
CROSS APPLY
的尝试SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
INTERSECT
SELECT u.Age WHERE u.Age IS NOT NULL );
Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 1094 ms, elapsed time = 1090 ms.
可能还有其他写方法这些,但我将留给那些可能比我更经常使用
COUNT
和INTERSECT
的人使用。如果您真的只需要计数
我在查询中使用
EXCEPT
只是一些速记(请阅读:我太懒了,有时无法提出更多涉及的场景)。如果您只需要计数,则可以使用EXCEPT
表达式执行几乎相同的操作。SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
EXCEPT
SELECT u.Age WHERE u.Age IS NULL);
这些都具有相同的计划,并且具有相同的CPU和阅读特征。表现最好。是的,可以通过添加几个过滤索引来说明这两个谓词来辅助其中的许多查询,但我想对其他谓词进行一些试验。
谢谢!
#2 楼
我不是只为一个表恢复110 GB数据库的游戏,所以我创建了自己的数据。年龄分布应该与Stack Overflow上的内容匹配,但显然表格本身不匹配。我认为这不是一个太大的问题,因为无论如何查询都将命中索引。我正在使用SQL Server 2016 SP1的4 CPU计算机上进行测试。要注意的一件事是,对于快速完成此查询的查询,重要的是不要包含实际的执行计划。这会使事情变慢很多。我首先介绍了Erik出色答案中的一些解决方案。对于这个:
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records);
我从sys.dm_exec_sessions进行了10多次试验(以下查询对我来说是并行的),得到以下结果:
╔══════════╦════════════════════╦═══════════════╗
║ cpu_time ║ total_elapsed_time ║ logical_reads ║
╠══════════╬════════════════════╬═══════════════╣
║ 3532 ║ 975 ║ 60830 ║
╚══════════╩════════════════════╩═══════════════╝
对Erik更好的查询实际上在我的机器上执行得较差:
SELECT SUM(Records)
FROM
(
SELECT 1
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
10次试验的结果:
╔══════════╦════════════════════╦═══════════════╗
║ cpu_time ║ total_elapsed_time ║ logical_reads ║
╠══════════╬════════════════════╬═══════════════╣
║ 5704 ║ 1636 ║ 60850 ║
╚══════════╩════════════════════╩═══════════════╝
我不能立即解释为什么会这么糟糕,但是不清楚为什么我们要强制查询计划中的几乎每个运算符并行。在原始计划中,我们有一个串行区域,该区域查找带有
AGE < 18
的所有行。只有几千行。在我的机器上,对于该部分查询,我有9次逻辑读取,并报告了9 ms的CPU时间和经过的时间。对于具有AGE IS NULL
的行,还有一个全局区域的串行区域,但是每个DOP仅处理一行。在我的机器上,这只有四行。我的要点是,最优化查询部分以
NULL
查找带有Age
的行是最重要的,因为其中有数百万行。我无法创建比包含该列上的简单页面压缩索引的页面更少的索引覆盖数据。我假设每行有一个最小的索引大小,或者用我尝试的技巧无法避免很多索引空间。因此,如果我们坚持使用大约相同数量的逻辑读取来获取数据,那么使其更快的唯一方法是使查询更加并行,但这需要与使用TF的Erik查询不同的方式完成8649.在上面的查询中,CPU时间与经过时间的比率为3.62,这非常好。理想的情况是我的计算机上的比率为4.0。一个可能的改进领域是在线程之间更均匀地分配工作。在下面的屏幕截图中,我们可以看到我的一个CPU决定休息一下:
索引扫描是为数不多的可以并行实现的运算符之一而且我们对行如何分配给线程一事无成。也有一定的机会,但是一直以来,我看到一个工作不足的线程。解决此问题的一种方法是采用并行方式进行艰苦的工作:在嵌套循环连接的内部。嵌套循环内部的任何内容都将以串行方式实现,但是许多串行线程可以并发运行。只要我们有了一种有利的并行分配方法(例如轮询),就可以精确控制向每个线程发送多少行。
我正在使用DOP 4运行查询,因此我需要将表格中的
NULL
行平均分成四个存储桶。一种方法是在计算列上创建一堆索引:ALTER TABLE dbo.Users
ADD Compute_bucket_0 AS (CASE WHEN Age IS NULL AND Id % 4 = 0 THEN 1 ELSE NULL END),
Compute_bucket_1 AS (CASE WHEN Age IS NULL AND Id % 4 = 1 THEN 1 ELSE NULL END),
Compute_bucket_2 AS (CASE WHEN Age IS NULL AND Id % 4 = 2 THEN 1 ELSE NULL END),
Compute_bucket_3 AS (CASE WHEN Age IS NULL AND Id % 4 = 3 THEN 1 ELSE NULL END);
CREATE INDEX IX_Compute_bucket_0 ON dbo.Users (Compute_bucket_0) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_1 ON dbo.Users (Compute_bucket_1) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_2 ON dbo.Users (Compute_bucket_2) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_3 ON dbo.Users (Compute_bucket_3) WITH (DATA_COMPRESSION = PAGE);
我不太确定为什么四个单独的索引比一个索引快一点但这就是我在测试中发现的。
为了获得并行的嵌套循环计划,我将使用未记录的跟踪标志8649。我还将编写一些奇怪的代码,以鼓励优化器不要处理超出必要的行。以下是一个看起来效果很好的实现:
SELECT SUM(t.cnt) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18)
FROM
(VALUES (0), (1), (2), (3)) v(x)
CROSS APPLY
(
SELECT COUNT(*) cnt
FROM dbo.Users
WHERE Compute_bucket_0 = CASE WHEN v.x = 0 THEN 1 ELSE NULL END
UNION ALL
SELECT COUNT(*) cnt
FROM dbo.Users
WHERE Compute_bucket_1 = CASE WHEN v.x = 1 THEN 1 ELSE NULL END
UNION ALL
SELECT COUNT(*) cnt
FROM dbo.Users
WHERE Compute_bucket_2 = CASE WHEN v.x = 2 THEN 1 ELSE NULL END
UNION ALL
SELECT COUNT(*) cnt
FROM dbo.Users
WHERE Compute_bucket_3 = CASE WHEN v.x = 3 THEN 1 ELSE NULL END
) t
OPTION (QUERYTRACEON 8649);
十次试验的结果:
╔══════════╦════════════════════╦═══════════════╗
║ cpu_time ║ total_elapsed_time ║ logical_reads ║
╠══════════╬════════════════════╬═══════════════╣
║ 3093 ║ 803 ║ 62008 ║
╚══════════╩════════════════════╩═══════════════╝
通过该查询,我们得到的CPU与经过时间之比为3.85!我们从运行时缩短了17毫秒,仅花费了4个计算列和索引即可完成!每个线程处理的行数非常接近相同的总数,因为每个索引的行数非常接近,并且每个线程仅扫描一个索引:
最后要注意的是,我们还可以单击“轻松”按钮,并向
Age
列添加非集群CCI: /> CREATE NONCLUSTERED COLUMNSTORE INDEX X_NCCI ON dbo.Users (Age);
很难被击败。
#3 楼
尽管我没有Stack Overflow数据库的本地副本,但是我可以尝试一些查询。我的想法是从系统目录视图中获取用户数量(而不是直接从基础表中获取行数量)。然后获得符合(或可能不符合)Erik的条件的行数,并做一些简单的数学运算。 。作为参考,这里有一些查询和CPU / IO统计信息:查询1
--Erik's query From initial question.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;
SQL Server执行时间:CPU时间= 0 ms,经过的时间= 0 ms。
(返回1行)
表'Users'。扫描计数17,逻辑读201567,物理读0,
预读2740,lob逻辑读0,lob物理读0,lob
预读0。
SQL Server执行时间:CPU时间= 1829毫秒,经过的时间= <296毫秒。
查询2
--Erik's "OR" query.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;
SQL Server执行时间:CPU时间= 0毫秒,经过的时间= 0毫秒。
(返回1行)
表“用户”。扫描计数17,逻辑读201567,物理读0,
预读为0,lob逻辑读0,lob物理读0,lob
预读为0。
SQL Server执行时间:CPU时间= 2500毫秒,经过的时间=
查询3
--Erik's derived tables/UNION ALL query.
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records);
SQL Server执行时间:CPU时间= 0毫秒,经过的时间= 0毫秒。
(返回1行)
表“用户”。扫描计数34,逻辑读403134,物理读0,
预读为0,lob逻辑读0,lob物理读0,lob
预读为0。
SQL Server执行时间:CPU时间= 3156毫秒,经过的时间= <215毫秒。
第一次尝试
这比我在这里列出的所有Erik查询都要慢...至少在经过时间方面。
SELECT SUM(p.Rows) -
(
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age >= 18
)
FROM sys.objects o
JOIN sys.partitions p
ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name
SQL Server执行时间:CPU时间= 0毫秒,经过的时间= 0毫秒。
(返回1行)
表'Worktable'。扫描计数0,逻辑读取0,物理读取0,
预读读取0,lob逻辑读取0,lob物理读取0,lob
预读取读取0。表'sysrowsets'。扫描计数2,逻辑读取
10,物理读取0,预读为0,lob逻辑读取0,lob
物理读取0,lob预读为0。表'sysschobjs'。扫描
计数1,逻辑读取4,物理读取0,预读读取0,lob
逻辑读取0,lob物理读取0,lob预读取读取0。表
'用户'。扫描计数1,逻辑读取201567,物理读取0,
预读读取0,lob逻辑读取0,lob物理读取0,lob
预读读取0。
SQL Server执行时间:CPU时间= 593毫秒,经过的时间= 598
毫秒。
第二次尝试
这里我选择了一个变量存储用户总数(而不是子查询)。与第一次尝试相比,扫描次数从1增加到17。逻辑读取保持不变。但是,经过的时间大大减少。
DECLARE @Total INT;
SELECT @Total = SUM(p.Rows)
FROM sys.objects o
JOIN sys.partitions p
ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name
SELECT @Total - COUNT(*)
FROM dbo.Users AS u
WHERE u.Age >= 18
SQL Server执行时间:CPU时间= 0 ms,经过的时间= 0 ms。
表'工作表'。扫描计数0,逻辑读取0,物理读取0,
预读读取0,lob逻辑读取0,lob物理读取0,lob
预读取读取0。表'sysrowsets'。扫描计数2,逻辑读取
10,物理读取0,预读为0,lob逻辑读取0,lob
物理读取0,lob预读为0。表'sysschobjs'。扫描
计数1,逻辑读取4,物理读取0,预读读取0,lob
逻辑读取0,lob物理读取0,lob预读读取0.
SQL Server执行时间:CPU时间= 0毫秒,经过的时间= 1毫秒。
(返回1行)
表“用户”。扫描计数17,逻辑读201567,物理读0,
预读为0,lob逻辑读0,lob物理读0,lob
预读为0。
SQL Server执行时间:CPU时间= 1471毫秒,经过的时间= 98
毫秒。
其他注意事项:
堆栈交换数据上不允许使用DBCC TRACEON资源管理器,如下所述:
用户'STACKEXCHANGE \ svc_sede'没有运行DBCC的权限
TRACEON。
评论
他们可能没有与我相同的索引,因此有所不同。而且,谁知道呢?也许我的家庭服务器使用的是更好的硬件;)不错的答案!
–埃里克·达林(Erik Darling)
17 Mar 27 '17 at 19:56
您应该在第一次尝试时使用以下查询(这样会更快,因为它消除了sys.objects-headhead的大部分内容):SELECT SUM(p.Rows)-(SELECT COUNT(*)FROM dbo.Users AS u在哪里u.Age> = 18)从sys.partitions p在哪里p.index_id <2 AND p.object_id = OBJECT_ID('dbo.Users')
–托马斯·弗朗兹(Thomas Franz)
17-4-24在9:45
PS:请注意,内存中索引(NONCLUSTERED HASH)不像普通堆/聚集索引那样具有索引ID = 0/1)
–托马斯·弗朗兹(Thomas Franz)
17-4-24在9:56
#4 楼
使用变量?declare @int1 int = ( select count(*) from table_1 where bb <= 1 )
declare @int2 int = ( select count(*) from table_1 where bb is null )
select @int1 + @int2;
每个注释都可以跳过变量
SELECT (select count(*) from table_1 where bb <= 1)
+ (select count(*) from table_1 where bb is null);
评论
另外:SELECT(从bb <= 1的表_1中选择count(*))+(从bb为空的table_1中选择count(*));
–超立方体ᵀᴹ
17年3月27日在16:27
在检查CPU和IO时可能想尝试一下。提示:这与Erik的答案之一相同。
–布伦特·奥扎(Brent Ozar)
17 Mar 27 '17 at 17:09
#5 楼
很好地使用SET ANSI_NULLS OFF;
SET ANSI_NULLS OFF;
SET STATISTICS TIME ON;
SET STATISTICS IO ON;
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE age=NULL or age<18
Table 'Users'. Scan count 17, logical reads 201567
SQL Server Execution Times:
CPU time = 2344 ms, elapsed time = 166 ms.
这只是我的脑海。只需在https://data.stackexchange.com
执行此操作但效率不如@blitz_erik
#6 楼
一个简单的解决方案是计算count(*)-count(年龄> = 18):SELECT
(SELECT COUNT(*) FROM Users) -
(SELECT COUNT(*) FROM Users WHERE Age >= 18);
或:
SELECT COUNT(*)
- COUNT(CASE WHEN Age >= 18)
FROM Users;
结果在这里
评论
NOT EXISTS(INTERSECT / EXCEPT)查询可以在没有INTERSECT / EXCEPT部分的情况下工作:WHERE NOT EXISTS(SELECT u.Age WHERE u.Age> = 18);另一种方式-使用EXCEPT:SELECT COUNT(*)FROM(从dbo.Users中选择SELECT UserID。从dbo.Users中选择SELECT UserID,u.Age> = 18)AS u; (其中UserID是PK或任何唯一的非null列)。
–超立方体ᵀᴹ
17-3-27在16:34
这个测试了吗? SELECT result =(从dbo.Users AS u WHERE u.Age <18的SELECT COUNT(*)+ +(从dbo.Users AS u WHERE u.Age的COUNT(*)为空);抱歉,如果我错过了您测试过的100万个版本!
–超立方体ᵀᴹ
18年5月11日在8:22
@ypercubeᵀᴹ这是那个的计划。稍有不同,但具有与UNION ALL计划类似的特征(360ms CPU,11k读取)。
–埃里克·达林(Erik Darling)
18年5月11日在10:54