在之前的工作中,我们不得不将项目x与项目x-1进行比较,以获得大量数据(〜十亿行)。由于这是在SQL Server 2008 R2上完成的,我们必须使用自联接。这很慢。
我以为我试验了滞后功能;如果速度快,这将是非常有价值的。我发现它快2到3倍,但因为它应该是一个简单的操作引擎盖,并且因为它的查询计划/表扫描更简单/大大减少,我非常失望。代码重现如下。
创建数据库:
IF EXISTS (SELECT name
FROM sys.databases
WHERE name = N'TestDBForLag')
DROP DATABASE TestDBForLag
GO
create database TestDBForLag
ALTER DATABASE TestDBForLag SET RECOVERY SIMPLE
go
use TestDBForLag
go
set nocount on
create table big (g binary(16) not null)
go
begin transaction
declare @c int = 0
while @c < 100
begin
insert into big(g) values(cast(newid() as binary(16)))
set @c += 1
end
commit
go 10000 -- n repeats of last batch, "big" now has 1,000,000 rows
alter table big
add constraint clustered_PK primary key clustered (g)
查询:
set statistics time on
set statistics io on
-- new style
select
g, lag(g, 1) over (order by g) as xx
from big
order by g
-- old style
select obig.g,
(
select max(g)
from big as ibig
where ibig.g < obig.g
) as xx
from big as obig
order by g
您可以自己查看实际/估计的查询计划,但这里是统计数据的结果(查询运行两次以折扣编译时间):
(1000000 row(s) affected)
Table 'Worktable'. {edit: everything zero here}.
**Table 'big'. Scan count 1, logical reads 3109**, {edit: everything else is zero here}.
SQL Server Execution Times: CPU time = 1045 ms, elapsed time = 3516 ms.
---
(1000000 row(s) affected)
**Table 'big'. Scan count 1000001, logical reads 3190609**, {edit: everything else is zero here}.
SQL Server Execution Times:CPU time = 2683 ms, elapsed time = 3439 ms.
因此,lag
需要1次扫描+ 3109次读取并需要约1秒的CPU时间,一个复杂的自连接必须重复遍历btree需要100万次扫描+320万次读取需要~2.7秒。 / p>
我没有看到这种糟糕表现的任何原因。有什么想法吗?
在ThinkServer 140,8G ram上运行(完全是mem驻留),双核,没有磁盘争用。我很满意将结果集传输到在同一台机器上运行的客户端的时间是可以忽略的。
select @@version
返回:
Microsoft SQL Server 2014 - 12.0.4213.0 (X64) Developer Edition (64-bit)
on Windows NT 6.1 <X64> (Build 7601: Service Pack 1)
编辑:
根据@ vnov的评论,我在发布之前就小心翼翼地打折了客户端开销。我看的是CPU时间而不是整体时间。测试:
select *
from big
Table 'big'. Scan count 1, logical reads 3109, {rest zero}
SQL Server Execution Times: CPU time = 125 ms, elapsed time = 2840 ms.
select count(*)
from big
Table 'big'. Scan count 1, logical reads 3109, {rest zero}
SQL Server Execution Times: CPU time = 109 ms, elapsed time = 129 ms.
lag
不应该添加任何重要的AFAICS,更不用说一个数量级了。
EDIT2:
@Frisbee不明白为什么我认为滞后很差。基本上,该算法是记住先前的值并在之后的n行中传递它。如果n = 1,则更加微不足道,所以我使用游标做了一些代码,有和没有自制滞后,并测量。我还简单地总结了结果,因此根据vnov的观点,它并没有返回巨大的结果集。游标和放大器选择给出了相同的sumg = 127539666,sumglag = 127539460的结果。代码使用与上面创建的相同的DB +表。选择版本:
select
sum(cast(g as tinyint)) as sumg
from (
select g
from big
) as xx
select
sum(cast(g as tinyint)) as sumg,
sum(cast(glag as tinyint)) as sumglag
from (
select g, lag(g, 1) over (order by g) as glag
from big
) as xx
我没有进行批量测量,但通过观察,这里的普通选择与滞后相当一致~360-400ms对比~1700-1900ms,因此慢4到5倍。
对于游标,顶部模拟第一个选择,底部模拟选择滞后:
---------- nonlagging batch --------------
use TestDBForLag
set nocount on
DECLARE crsr CURSOR FAST_FORWARD READ_ONLY FOR
select g from big order by g
DECLARE @g binary(16), @sumg int = 0
OPEN crsr
FETCH NEXT FROM crsr INTO @g
WHILE (@@fetch_status <> -1)
BEGIN
IF (@@fetch_status <> -2)
BEGIN
set @sumg += cast(@g as tinyint)
END
FETCH NEXT FROM crsr INTO @g
END
CLOSE crsr
DEALLOCATE crsr
select @sumg as sumg
go 300
---------- lagging batch --------------
use TestDBForLag
set nocount on
DECLARE crsr CURSOR FAST_FORWARD READ_ONLY FOR
select g from big order by g
DECLARE @g binary(16), @sumg int = 0
DECLARE @glag binary(16) = 0, @sumglag int = 0
OPEN crsr
FETCH NEXT FROM crsr INTO @g
WHILE (@@fetch_status <> -1)
BEGIN
IF (@@fetch_status <> -2)
BEGIN
set @sumg += cast(@g as tinyint)
set @sumglag += cast(@glag as tinyint) -- the only ...
set @glag = @g -- ... differences
END
FETCH NEXT FROM crsr INTO @g
END
CLOSE crsr
DEALLOCATE crsr
select @sumg as sumg, @sumglag as sumglag
go 300
运行上面的SQL分析器(删除SQL:Batch Starting事件),对我来说需要大约2.5小时,将跟踪保存为一个名为&#39; trace&#39;的表,然后运行它以获得平均持续时间
-- trace save duration as microseconds,
-- divide by 1000 to get back to milli
select
cast(textdata as varchar(8000)) as textdata,
avg(duration/1000) as avg_duration_ms
from trace
group by cast(textdata as varchar(8000))
对我来说,非标记游标平均需要13.65秒,光标仿真延迟需要16.04秒。后者的大部分额外时间将来自解释器处理额外陈述的开销(如果在C中实现,我预计它会少得多),但无论如何都要小于计算滞后额外20%。
那么,这个解释听起来是否合理,是否有人可以建议为什么滞后在选择语句中的表现如此糟糕呢?
答案 0 :(得分:5)
检查两种变体的执行计划,您将看到发生了什么。 我使用免费版本的SQL Sentry Plan Explorer。
我正在比较这三个查询(再加上一个OUTER APPLY
):
select count(*)
from big;
-- new style
select
g, lag(g) over (order by g) as xx
from big
order by g;
-- old style
select obig.g,
(
select max(g)
from big as ibig
where ibig.g < obig.g
) as xx
from big as obig
order by g;
1)LAG
是使用Window Spool实现的,它提供了两倍于临时工作表的行数(1,999,999)(在这种情况下它在内存中,但仍然如此)。 Window Spool不会缓存工作表中的所有1,000,000行,它只缓存窗口大小。
Window Spool操作符将每一行扩展为行集 表示与之关联的窗口。
计划中还有许多其他不那么重的运营商。这里的要点是LAG
没有像你在游标测试中那样实现。
2)旧式查询的计划非常好。优化程序可以智能地扫描表格,并为每行TOP
执行索引搜索,以计算MAX
。是的,这是百万寻求,但一切都在记忆中,所以它相对较快。
3)将鼠标悬停在计划运算符之间的粗箭头上,您将看到实际的数据大小。它是Window Spool的两倍大。因此,当一切都在内存和CPU绑定时,它变得很重要。
4)您的旧样式查询可以重写为:
select obig.g, A.g
from big as obig
OUTER APPLY
(
SELECT TOP(1) ibig.g
FROM big as ibig
WHERE ibig.g < obig.g
ORDER BY ibig.g DESC
) AS A
order by obig.g;
,效率更高一些(参见截图中的CPU列)。
因此,LAG
在读取的页数方面非常有效,但使用的CPU非常多。
答案 1 :(得分:0)
你的滞后是什么?扫描仍然必须在每个扫描行上找到-1 g,如果g不是聚类键,这可能是很多工作
答案 2 :(得分:0)
lag
本身实际上可能不会花费太多时间,因为g
是群集主键。如果您尝试:
select * from big
它也需要很多时间。
并且您的查询不能比这更快,因为它处理相同数量的数据。必须有很多IO正在进行中。我不是这方面的专家,但表格大小是aprox。 24MB和sql server通过8KB块读取数据,因此这是aprox 3000物理读取。运行查询并专门查看性能监视器/进程资源管理器,磁盘IO。