SQL Server的分页优化及Row_Number()分页存在的问题
最近有项目反应,在服务器CPU使用较高的时候,我们的事件查询页面非常的慢,查询几条记录竟然要4分钟甚至更长,而且在翻第二页的时候也是要这么多的时间,这肯定是不能接受的,也是让现场用SQLServerProfiler
把语句抓取了上来。
用ROW_NUMBER()进行分页
我们看看现场抓上来的分页语句:
- select top 20 a.*,ag.Name as AgentServerName,,d.Name as MgrObjTypeName,l.UserName as userName
- from eventlog as a
- left join mgrobj as b on a.MgrObjId=b.Id and a.AgentBm=b.AgentBm
- left join addrnode as c on b.AddrId=c.Id
- left join mgrobjtype as d on b.MgrObjTypeId=d.Id
- left join eventdir as e on a.EventBm=e.Bm
- left join agentserver as ag on a.AgentBm=ag.AgentBm
- left join loginUser as l on a.cfmoper=l.loginGuid
- where a.OrderNo not in (
- select top 0 OrderNo
- from eventlog as a
- left join mgrobj as b on a.MgrObjId=b.Id
- left join addrnode as c on b.AddrId=c.Id
- where 1=1 and a.AlarmTime>='2014-12-01 00:00:00' and a.AlarmTime<='2014-12-26 23:59:59'
- and b.AddrId in ('02109000',……,'02109002')
- order by AlarmTime desc
- )
- and 1=1 and a.AlarmTime>='2014-12-01 00:00:00' and a.AlarmTime<='2014-12-26 23:59:59'
- and b.AddrId in ('02109000',……,'02109002')
- order by AlarmTime DESC
这是典型的使用两次top来进行分页的写法,原理是:先查出pageSize*(pageIndex-1)
(T1)的记录数,然后再Top
出PageSize
条不在T1中的记录,就是当前页的记录。这种查询效率不高主要是使用了not in
。参考我之前文章《程序猿是如何解决SQLServer占CPU100%的》提到的:“对于不使用SARG运算符的表达式,索引是没有用的”。
那么改为使用ROW_NUMBER
分页:
- WITH cte AS(
- select a.*,ag.Name as AgentServerName,d.Name as MgrObjTypeName,l.UserName as userName,b.AddrId
- ,ROW_NUMBER() OVER(ORDER BY AlarmTime DESC) AS RowNo
- from eventlog as a WITH(FORCESEEK)
- left join mgrobj as b on a.MgrObjId=b.Id and a.AgentBm=b.AgentBm
- left join addrnode as c on b.AddrId=c.Id
- left join mgrobjtype as d on b.MgrObjTypeId=d.Id
- left join eventdir as e on a.EventBm=e.Bm
- left join agentserver As ag on a.AgentBm=ag.AgentBm
- left join loginUser as l on a.cfmoper=l.loginGuid
- where a.AlarmTime>='2014-12-01 00:00:00' and a.AlarmTime<='2014-12-26 23:59:59'
- AND b.AddrId in ('02109000',……,'02109002')
- )
- SELECT * FROM cte WHERE RowNo BETWEEN 1 AND 20;
执行时间从14秒提升到5秒,这说明Row_Number分页还是比较高效的,而且这种写法比top top
分页优雅很多。
“欺骗”查询引擎让查询按你的期望去查询
但是为什么查询20条记录竟然要5秒呢,尤其在这个表是加上了时间索引的情况下——参考《程序猿是如何解决SQLServer占CPU100%的》中提到的索引。
我尝试去掉这句AND b.AddrId in ('02109000',……,'02109002')
,结果不到1秒就把538条记录查询出来了,而加上地点限制这句,结果是204行。为什么结果集不大,花费的时间却相差这么多呢?查看执行计划,发现走的是另外的索引,而非时间索引。
把这个疑问放到了SQLServer群上,很快,高桑给了回复:要想达到跟去掉地点限制这句的效果,就使用AdddrId+'' in
。
什么意思?一时没看明白,是高桑没看懂我的语句?很快,有人补充,要欺骗查询引擎。“欺骗”?还是不懂,不过我照做了,把上述cte的语句原封不动的Copy出来,然后把这句AND b.AddrId in ('02109000',……,'02109002')
更改为了AND b.AddrId+'' in ('02109000',……,'02109002')
,一点执行,神了!!!不到1秒就执行完了。在把执行计划一对,果然走的是时间索引:
后来回味了一下,记起之前看到的查询引擎优化原理,如果你的条件中带有运算符或者使用函数等,则查询引擎会放弃优化,而执行表扫描。脑袋突然转过来了,在使用b.AddrId+''
前查询引擎尝试把mgrObj表加入一起做优化,那么两个表联查,会导致预估的记录数大大增加,而使用了b.AddrId+''
,查询引擎则会先按时间索引把记录刷选出来,这样就达到了效果,即强制先做cte在执行in
条件,而不是在cte中进行in
条件刷选。原来如此!有时候,查询引擎过度的优化,会导致相反的效果,而你如果能够知道优化的原理,那么就可以通过一些小的技巧让查询引擎按你的期望去进行优化。
ROW_NUMBER()分页在页数较大时的问题
事情到这里,还没完。后面同事又跟我反应,查询到后面的页数,又卡了!what?我重新执行上述语句,把时间范围放到2011-12-01到2014-12-26,记录数限制为为19981到20000,果然,查询要30秒左右,查看执行计划,都是一样的,为什么?
高桑怀疑是key lookup过多导致的,建议先分页取出rid 再做key lookup。不懂这么一句是什么意思。把执行计划和IO打印出来:
看看IO,很明显,主要是越到后面的页数,其他的几个关联表读取的页数就越多。我推测,在Row_Number分页的时候,如果有表连接,则按排序一致到返回的记录数位置,前面的记录都是要参与表连接的,这就导致了越到后面的分页,就越慢,因为要扫描的关联表就越多。
难道就没有了办法了吗?这个时候宋桑英勇的站了出来:“你给表后加一个forceseek
提示可破”。这真是犹如天籁之音,马上进行尝试。
使用forceseek提示可以强制表走索引
查了下资料:
SQL Server2008中引入的提示
ForceSeek
,可以用它将索引查找来替换索引扫描
那么,就在eventlog表中加上这句看看会怎样?
果然,查询计划变了,开始提示,缺少了包含索引。赶紧加上,果然,按这个方式进行查询之后查询时间变为18秒,有进步!但是查看IO,跟上面一样,并没有变少。不过,总算学会了一个新的技能,而宋桑也很热心说晚上再帮忙看看。
把其他没参与where的表放到cte外面
根据上面的IO,很快,又有人提到,把其他left join
的表放到cte外面。这是个办法,于是把除eventlog
、mgrobj
、addrnode
的表放到外面,语句如下:
- WITH cte AS(
- select a*,b.AddrId,b.Name as MgrObjName,b.MgrObjTypeId
- ,ROW_NUMBER() OVER(ORDER BY AlarmTime DESC) AS RowNo
- from eventlog as a
- left join mgrobj as b on a.MgrObjId=b.Id and a.AgentBm=b.AgentBm
- left join addrnode as c on b.AddrId=c.Id
- where a.AlarmTime>='2011-12-01 00:00:00' and a.AlarmTime<='2014-12-26 23:59:59'
- AND b.AddrId+'' in ('02109000',……,'02109002')
- )
- SELECT a.*
- ,ag.Name as AgentServerName
- ,d.Name as MgrObjTypeName,l.UserName as userName
- FROM cte a left join eventdir as e on a.EventBm=e.Bm
- left join mgrobjtype as d on a.MgrObjTypeId=d.Id
- left join agentserver As ag on a.AgentBm=ag.AgentBm
- left join loginUser as l on a.cfmoper=l.loginGuid
- WHERE RowNo BETWEEN 19980 AND 20000;
果然有效,IO大大减少了,然后速度也提升到了16秒。
- 表 'loginuser'。扫描计数 1,逻辑读取 63 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'agentserver'。扫描计数 1,逻辑读取 1617 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'mgrobjtype'。扫描计数 1,逻辑读取 126 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'eventdir'。扫描计数 1,逻辑读取 42 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'addrnode'。扫描计数 1,逻辑读取 119997 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'Worktable'。扫描计数 0,逻辑读取 0 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'eventlog'。扫描计数 1,逻辑读取 5027 次,物理读取 3 次,预读 5024 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'mgrobj'。扫描计数 1,逻辑读取 24 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
我们看到,addrNode表还是扫描计数很大。那还能不能提升,这个时候,我想到了,先把addrNode
、mgrobj
、mgrobjtype
三个表联合查询,放到一个临时表,然后再和eventlog
做inner join
,然后查询结果再和其他表做left join
,这样还能减少IO。
使用临时表存储分页记录在进行表连接减少IO
- IF OBJECT_ID('tmpMgrObj') IS NOT NULL DROP TABLE tmpMgrObj
- SELECT m.Id,AddrId,MgrObjTypeId,AgentBM,m.Name,a.Name AS AddrName
- INTO tmpMgrObj
- FROM dbo.mgrobj m
- INNER JOIN dbo.addrnode a ON a.Id=m.AddrId
- WHERE AddrId IN('02109000',……,'02109002');
- WITH cte AS(
- select a.*,b.AddrId,b.MgrObjTypeId
- ,ROW_NUMBER() OVER(ORDER BY AlarmTime DESC) AS RowNo
- ,ag.Name as AgentServerName
- ,d.Name as MgrObjTypeName,l.UserName as userName
- from eventlog as a
- INNER join tmpMgrObj as b on a.MgrObjId=b.Id and a.AgentBm=b.AgentBm
- left join mgrobjtype as d on b.MgrObjTypeId=d.Id
- left join agentserver As ag on a.AgentBm=ag.AgentBm
- left join loginUser as l on a.cfmoper=l.loginGuid
- WHERE AlarmTime>'2011-12-01 00:00:00' AND AlarmTime<='2014-12-26 23:59:59'
- )
- SELECT * FROM cte WHERE RowNo BETWEEN 19980 AND 20000
- IF OBJECT_ID('tmpMgrObj') IS NOT NULL DROP TABLE tmpMgrObj
这次查询仅用了10秒。我们来看看IO:
- 表 'Worktable'。扫描计数 0,逻辑读取 0 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'mgrobj'。扫描计数 1,逻辑读取 24 次,物理读取 2 次,预读 23 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'addrnode'。扫描计数 1,逻辑读取 6 次,物理读取 3 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- ----------
- 表 'loginuser'。扫描计数 0,逻辑读取 24 次,物理读取 1 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'Worktable'。扫描计数 0,逻辑读取 0 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'eventlog'。扫描计数 93,逻辑读取 32773 次,物理读取 515 次,预读 1536 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'tmpMgrObj'。扫描计数 1,逻辑读取 3 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'mgrobjtype'。扫描计数 1,逻辑读取 6 次,物理读取 1 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
- 表 'agentserver'。扫描计数 1,逻辑读取 77 次,物理读取 2 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
除了eventlog之外,其他的表的IO大大减少,有木有?
inner join和left join的区别
但是,多执行几次测试,发现上述语句还是有一点问题:查询第一页的时候,也竟然要用5秒,而查询时间在当前一个月份的,也接近5秒。这是为什么呢? 这个时候,宋桑再伸援手,提供了另外一个SQL语句,在查询前面几页的时候1秒就出来了,而后面的页数,则变化不大。我仔细比较了两个语句,原来我用的是inner join
,而宋桑给的是left join
。这两个有什么区别呢。仔细对比查询计划之后发现,使用inner join
的时候,查询引擎会先执行inner join
而非子查询,而使用left join
则查询引擎先执行子查询。因此如果使用了inner join
会导致在查询1个月的数据时,没有有效利用了时间索引。最终,我研究出来的语句如下,在查询最新数据或者前面几页的数据,能够在1秒左右出来,而查询后面的页数,在10秒左右,基本解决了问题。
- IF OBJECT_ID('tmpMgrObj') IS NOT NULL DROP TABLE tmpMgrObj
- SELECT m.Id,AddrId,MgrObjTypeId,AgentBM,m.Name,a.Name AS AddrName,t.Name AS MgrObjTypeName
- INTO tmpMgrObj
- FROM dbo.mgrobj m
- INNER JOIN dbo.addrnode a ON a.Id=m.AddrId
- INNER JOIN dbo.mgrobjtype t ON m.MgrObjTypeId=t.Id
- WHERE AddrId+'' IN('02109000',……,'02109002');
- SELECT tmp.*
- ,ag.Name AS AgentServerName
- , l.UserName AS userName
- FROM (
- SELECT a.* ,b.MgrObjTypeName , b.AddrId
- ,ROW_NUMBER() OVER ( ORDER BY AlarmTime DESC ) AS RowNo
- FROM
- (SELECT *
- FROM eventlog
- WHERE AlarmTime >= '2011-12-01 00:00:00' AND AlarmTime <= '2014-12-26 23:59:59') AS a
- LEFT JOIN tmpMgrObj AS b ON a.MgrObjId=b.Id AND a.AgentBM=b.AgentBm
- ) tmp
- LEFT JOIN eventdir AS e ON tmp.EventBm = e.Bm
- LEFT JOIN agentserver AS ag ON tmp.AgentBm = ag.AgentBm
- LEFT JOIN loginUser AS l ON tmp.cfmoper = l.loginGuid
- WHERE tmp.RowNo BETWEEN 1 AND 20;
- IF OBJECT_ID('tmpMgrObj') IS NOT NULL DROP TABLE tmpMgrObj
其他优化参考
在另外的群上讨论时,发现使用ROW_NUMBER
分页查询到后面的页数会越来越慢的这个问题的确困扰了不少的人。
有的人提出,谁会这么无聊,把页数翻到几千页以后?一开始我也是这么想的,但是跟其他人交流之后,发现确实有这么一种场景,我们的软件提供了最后一页这个功能,结果……当然,一种方法就是在设计软件的时候,就去掉这个最后一页的功能;另外一种思路,就是查询页数过半之后,就反向查询,那么查询最后一页其实也就是查询第一页。
还有一些人提出,把查询出来的内容,放到一个临时表,这个临时表中的加入自增Id的索引,这样,可以通过辨别Id来进行快速刷选记录。这也是一种方 法,我打算稍后尝试。但是这种方法也是存在问题的,就是无法做到通用,必须根据每个表进行临时表的构建,另外,在超大数据查询时,插入的记录过多,因为索 引的存在也是会慢的,而且每次都这么做,估计CPU也挺吃紧。但是不管怎么样,这是一种思路。
你有什么好的建议?不妨把你的想法在评论中提出来,一起讨论讨论。
总结
现在,我们来总结下在这次优化过程中学习到什么内容:
- 在SQLServer中,
ROW_NUMBER
的分页应该是最高效的了,而且兼容SQLServer2005以后的数据库 - 通过“欺骗”查询引擎的小技巧,可以控制查询引擎部分的优化过程
ROW_NUMBER
分页在大页数时存在性能问题,可以通过一些小技巧进行规避- 尽量通过cte利用索引
- 把不参与
where
条件的表放到分页的cte外面 - 如果参与
where
条件的表过多,可以考虑把不参与分页的表先做一个临时表,减少IO
inner join
会优先于子查询,而left join
不会- 使用
with(forceseek)
可以强制查询因此进行索引查询
SQL Server的分页优化及Row_Number()分页存在的问题的更多相关文章
- sql server 2000 单主键高效分页存储过程 (支持多字段排序)
sql server 2000 单主键高效分页存储过程 (支持多字段排序) Create PROC P_viewPage /* nzperfect [ ...
- SQL Server查询性能优化——堆表、碎片与索引(二)
本文是对 SQL Server查询性能优化——堆表.碎片与索引(一)的一些总结. 第一:先对 SQL Server查询性能优化——堆表.碎片与索引(一)中的例一的SET STATISTICS IO之 ...
- SQL Server查询性能优化——覆盖索引(二)
在SQL Server 查询性能优化——覆盖索引(一)中讲了覆盖索引的一些理论. 本文将具体讲一下使用不同索引对查询性能的影响. 下面通过实例,来查看不同的索引结构,如聚集索引.非聚集索引.组合索引等 ...
- SQL Server查询性能优化——创建索引原则(一)
索引是什么?索引是提高查询性能的一个重要工具,索引就是把查询语句所需要的少量数据添加到索引分页中,这样访问数据时只要访问少数索引的分页 就可以.但是索引对于提高查询性能也不是万能的,也不是建立越多的索 ...
- SQL Server 查询性能优化——创建索引原则(一)(转载)
索引是什么?索引是提高查询性能的一个重要工具,索引就是把查询语句所需要的少量数据添加到索引分页中,这样访问数据时只要访问少数索引的分页就可以.但是索引对于提高查询性能也不是万能的,也不是建立越多的索引 ...
- SQL Server 查询性能优化——创建索引原则(一)
索引是什么?索引是提高查询性能的一个重要工具,索引就是把查询语句所需要的少量数据添加到索引分页中,这样访问数据时只要访问少数索引的分页就可以.但是索引对于提高查询性能也不是万能的,也不是建立越多的索引 ...
- SQL Server 查询性能优化——创建索引原则
索引是什么?索引是提高查询性能的一个重要工具,索引就是把查询语句所需要的少量数据添加到索引分页中,这样访问数据时只要访问少数索引的分页就可以.但是索引对于提高查询性能也不是万能的,也不是建立越多的索引 ...
- SQL Server数据库性能优化之SQL语句篇【转】
SQL Server数据库性能优化之SQL语句篇http://www.blogjava.net/allen-zhe/archive/2010/07/23/326927.html 近期项目需要, 做了一 ...
- SQL SERVER 查询性能优化——分析事务与锁(五)
SQL SERVER 查询性能优化——分析事务与锁(一) SQL SERVER 查询性能优化——分析事务与锁(二) SQL SERVER 查询性能优化——分析事务与锁(三) 上接SQL SERVER ...
随机推荐
- 图片上传并回显Ajax异步篇
图片上传并回显Ajax异步篇 图片如何无刷新的上传到服务器呢?继前两篇文章后,我们来实战一下如何无刷新的异步上传图片,我们还是先看一下效果 在实战前呢,我们需要做些准备工作.比如说,了解一下FormD ...
- C语言常用命令
## 基本流程:创建——编辑——编译.链接——执行$ touch 1-1.c$ vim 1-1.c # include <stdio.h>int main{printf("... ...
- LA3485 Bridge
题意 PDF 分析 以<训练指南>上的分析为准. 二分法时间复杂度\(O(\log v)\) 代码 #include<bits/stdc++.h> #define rg reg ...
- Kettle入门--作业和转换的使用
本来想在centos7下部署的,发现因为java版本的问题,无法成功部署,无奈,转到windows平台(后来找到解决方法了,在centos7系统下yum install webkitgtk* -y 就 ...
- C语言函数指针的使用
使用函数指针时一定要注意,因为c不会检查参数是否正确 区分返回指针的函数和函数指针 int *f4();返回一个整数指针 int (*f5)();返回整数的函数指针 int * (*f6)();返回整 ...
- MySQL--InnoDB并发线程控制
InnoDB并发线程控制 MySQL InnoDB存储引擎提供innodb_thread_concurrency来控制进入InnoDB 存储引擎的线程数,以限制InnoDB存储引擎层的并发量. 当in ...
- mino federation 功能
mino federation 可以让我们进行 bucket的查找,实现更灵活的分布式控制 其中依赖了几个组件 coredns etcd 参考架构图 说明 官方也提供了一个简单的部署的demo,总的来 ...
- .NET本质论 方法
方法和JIT编译 CLR只执行本机的机器代码.如果一个方法体由CIL组成,那么它就必须在调用之前被转换为本机的机器码(将MSIL编译为本机代码,运行库提供了两种方式.一种就是在安装与部署时的预编译(由 ...
- taro 学习资料
taro 学习资料 学习资料 网址 github https://github.com/NervJS/taro taro 官方文档 https://nervjs.github.io/taro/docs ...
- php 过滤器filter_var验证邮箱/url/ip等
验证邮箱.url或者ip,除了使用正则外,也可以使用内置的函数库过滤器filter来完成这些功能. filter_var (PHP 5 >= 5.2.0, PHP 7)filter_var — ...