最近有项目反应,在服务器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)的记录数,然后再TopPageSize条不在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外面。这是个办法,于是把除eventlogmgrobjaddrnode的表放到外面,语句如下:

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表还是扫描计数很大。那还能不能提升,这个时候,我想到了,先把addrNodemgrobjmgrobjtype三个表联合查询,放到一个临时表,然后再和eventloginner 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()分页存在的问题的更多相关文章

  1. sql server 2000 单主键高效分页存储过程 (支持多字段排序)

    sql server 2000 单主键高效分页存储过程 (支持多字段排序) Create PROC P_viewPage             /*              nzperfect [ ...

  2. SQL Server查询性能优化——堆表、碎片与索引(二)

    本文是对 SQL Server查询性能优化——堆表.碎片与索引(一)的一些总结.  第一:先对 SQL Server查询性能优化——堆表.碎片与索引(一)中的例一的SET STATISTICS IO之 ...

  3. SQL Server查询性能优化——覆盖索引(二)

    在SQL Server 查询性能优化——覆盖索引(一)中讲了覆盖索引的一些理论. 本文将具体讲一下使用不同索引对查询性能的影响. 下面通过实例,来查看不同的索引结构,如聚集索引.非聚集索引.组合索引等 ...

  4. SQL Server查询性能优化——创建索引原则(一)

    索引是什么?索引是提高查询性能的一个重要工具,索引就是把查询语句所需要的少量数据添加到索引分页中,这样访问数据时只要访问少数索引的分页 就可以.但是索引对于提高查询性能也不是万能的,也不是建立越多的索 ...

  5. SQL Server 查询性能优化——创建索引原则(一)(转载)

    索引是什么?索引是提高查询性能的一个重要工具,索引就是把查询语句所需要的少量数据添加到索引分页中,这样访问数据时只要访问少数索引的分页就可以.但是索引对于提高查询性能也不是万能的,也不是建立越多的索引 ...

  6. SQL Server 查询性能优化——创建索引原则(一)

    索引是什么?索引是提高查询性能的一个重要工具,索引就是把查询语句所需要的少量数据添加到索引分页中,这样访问数据时只要访问少数索引的分页就可以.但是索引对于提高查询性能也不是万能的,也不是建立越多的索引 ...

  7. SQL Server 查询性能优化——创建索引原则

    索引是什么?索引是提高查询性能的一个重要工具,索引就是把查询语句所需要的少量数据添加到索引分页中,这样访问数据时只要访问少数索引的分页就可以.但是索引对于提高查询性能也不是万能的,也不是建立越多的索引 ...

  8. SQL Server数据库性能优化之SQL语句篇【转】

    SQL Server数据库性能优化之SQL语句篇http://www.blogjava.net/allen-zhe/archive/2010/07/23/326927.html 近期项目需要, 做了一 ...

  9. SQL SERVER 查询性能优化——分析事务与锁(五)

    SQL SERVER 查询性能优化——分析事务与锁(一) SQL SERVER 查询性能优化——分析事务与锁(二) SQL SERVER 查询性能优化——分析事务与锁(三) 上接SQL SERVER ...

随机推荐

  1. day021python 正则表达式

    正则表达式是由普通字符和元字符组成, 普通字符包含大小写字母, 数字. 在匹配普通字符 的时候我们直接写就可以了. 比如"abc" 匹配的就是"abc". 元字 ...

  2. 查看win电脑支持的最大内存?

    wmic memphysical get maxcapacity 67108864 大约64G

  3. 【牛客练习赛22 C】

    https://www.nowcoder.com/acm/contest/132/C 题目大意:在n个区间中取出n个数,相加的和一共会出现多少种结果. 题目分析:对于这种挑选数字相加,由于每一步不同的 ...

  4. Python 3 运行 shell 命令

    #python 3.5 , win10 引入包 #os.chdir('path') import osimport subprocess #https://docs.python.org/3.5/li ...

  5. 直面Java 第003期

    . 问:什么是平台无关性,Java是如何做到平台无关的? 解: 大家说的都很对,看来大家对这个概念掌握的很可以.我简单总结一下. 跨平台指的是一种语言在计算机上的运行不受平台的约束,一次编译,到处执行 ...

  6. day18-19 Storm

    课程介绍 课程名称:Storm是什么 课程目标: 通过该课程的学习能够了解离线计算与流式计算的区别.掌握Storm框架的基础知识.了解流式计算的一般架构图. 课程大纲: 1. 离线计算是什么? 2.  ...

  7. 依赖注入的方式测试ArrayList和LinkedList的效率(对依赖注入的再次理解)

    9/20 号再进行学习 在C++中,main函数尽可能的简单,只要调用子函数的一句话就实现了功能. java开发中,controller就相同于是main函数,其他类的方法不在本类中时候, 1.可以用 ...

  8. 使用 FreeCAD 打开 KiCad 用于制作外壳

    使用 FreeCAD 打开 KiCad 用于制作外壳 先看导入后的结果. 步骤: 安装 FreeCAD 安装 KiCad StepUp Mod Addon 重启 FreeCAD(非常重要,不重启不生效 ...

  9. 如何让你的 KiCad 在缩放时不眩晕?

    如何让你的 KiCad 在缩放时不眩晕? 使用 KiCAD 第一感觉是打开速度非常快,而且 PCB 拉线也非常快,封装库又多. 但有一个问题,缩放时总给人一种眩晕,原来是因为鼠标自动跑到屏幕中间去了, ...

  10. day4 大纲笔记

    01 上周内容回顾 int bool str int <---> str: i1 = 100 str(i1) s1 = '10' int(s1) 字符串必须是数字组成. int <- ...