使用ROW_NUMBER来分页几乎是家喻户晓的东东了,而且这东西简单易用,简直就是程序员居家必备之杀器,然而ROW_NUMBER也不是一招吃遍天下鲜的无敌BUG般存在,最近就遇到几个小问题,拿出来供大家娱乐下。

---======================================================

问题1:为什么加WHERE条件就慢,不加反而快?

查询SQL:

WITH Temp AS(
SELECT * ,
ROW_NUMBER()OVER(ORDER BY T2.C6 DESC) AS RID
FROM TB001 AS T1
INNER JOIN TB002 AS T2
ON T1.C1=T2.C1
WHERE T1.C2>1000
AND T2.C3<99999
AND T1.C4=5
)
SELECT *
FROM Temp
WHERE RID BETWEEN 0 AND 10

开发大哥很激动地问我,对上面类似的的查询,如果没有WHERE RID BETWEEN 0 AND 10的话,查询在1秒内完成,如果有WHERE条件,执行超过30秒未结束,不带WHERE条件返回300行左右数据,WHERE条件过滤后返回10行数据,返回的数据行长度较小,可以忽略由于返回数据大小对网络和显示的影响,那问题出在那呢?稍微有点DBA经验的人都会很快找到问题根源--执行计划不对。

让我们换个简单的SQL来分析下

WITH Temp AS(
SELECT * ,
ROW_NUMBER()OVER(ORDER BY T1.C1 DESC) AS RID
FROM TB001 AS T1
WHERE T1.C2>1000
)
SELECT *
FROM Temp
WHERE RID BETWEEN 0 AND 10

让我们揣测下上面查询如何实现,假设在T1.C1有索引IX_C1,在T1.C2上有索引IX_C2。

实现方式1:

A=>针对CTE内部的查询,先利用索引IX_C2找出满足条件T1.C2>1000的数据,得到结果集U1

B=>对结果集U1按T1.C1排序,计算出U1中每行RID列的值,得到结果集U2

C=>对结果集U2查找满足RID BETWEEN 0 AND 10过滤的行,得到结果集U3

D=>将结果集U3返回

实现方式2:

A=>利用索引IX_C1按ORDER BY T1.C1 DESC来依次访问T1数据

B=>检查步骤A得到的行是否满足T1.C2>1000条件,将满足条件的结果放入结果集U1中,然后一次递增RID

C=>检查步骤B得到的结果集UI,当得到足够数据行(RID BETWEEN 0 AND 10)后停止步骤A和B

D=>将结果集U1返回

以上两种方式都能得到正确的返回结果,但是那种更好呢?

对于实现方式1,假设表T1有100W数据,如果满足T1.C2>1000的行只有20行,那么使用索引IX_C2快速找出满足条件的20行数据,然后对这20行数据排序也只会消耗很轻微的CPU资源;但如果满足T1.C2>1000的行只有99W行,那么排序就消耗大量CPU资源,从而导致查询慢。

对于实现方式2,假设表T1有100W数据,按照索引IX_C1 倒序遍历C1的值,如果遍历前50行便能查找到满足T1.C2>1000的10行数据,那么查询可以很快结束,只消耗少量的逻辑读;但如果需要遍历前99W数据才能找到满足T1.C2>1000的10行数据,那么就会消耗大量的逻辑读,从而导致查询慢。

由此,我们不难得出一个结论:没有绝对正确的执行计划,只有相对高质量的执行计划。

--==================================================================

我们知道,在SQL SERVER生成执行计划时,会根据输入的参数和统计信息去预估一些步骤的影响行数和开销,寻找开销较小的执行计划,对于本篇开头提到的查询,SQL SERVER很容易受到RID BETWEEN 0 AND 10的诱惑,选择类似于实现方式2的的执行计划,而数据分布情况又恰好是针对该方式最坏的情况,就出现了我们遇到的结果,查询死慢死慢的。

类似的案例还有:

1. 查询返回数据20行,然后在此查询的基础上增加ORDER BY 和TOP(10), 结果执行效率慢了很多,于是就产生了为什么对20行数据排序取TOP会这么慢的疑惑?

2. 查询返回数据20行,在查询中分别增加SELECT TOP(20)和SELECT TOP(10000),结果SELECT TOP(10000)的比SELECT TOP(20)快很多倍,我遇到的案例有SELECT TOP(10000)在5ms内完成,然后SELECT TOP(1)的十分钟都没有结果

以上案例都有相同的操作ORDER BY+TOP,ROW_NUMBER本质上也是ORDER BY+TOP,我们知道CPU资源是服务器资源中最宝贵的资源,而对结果集排序又是一个很耗CPU资源的过程,SQL SERVER为节省CPU资源选择了一个“它”认为比较合适的执行计划,结果悲剧了。

--===============================================

针对哪位开发大哥的问题,我尝试了各种写法,在不动用临时表和索引提示的情况下,我还真搞不定这SQL,于是我来了个邪恶小招数:

WITH Temp AS(
SELECT * ,
ROW_NUMBER()OVER(ORDER BY T2.C6 DESC) AS RID
FROM TB001 AS T1
INNER JOIN TB002 AS T2
ON T1.C1=T2.C1
WHERE T1.C2>1000
AND T2.C3<99999
AND T1.C4=5
)
SELECT *
FROM Temp
WHERE RID+0 BETWEEN 0 AND 10

学术派们要开始叫嚣了,这种RID+0 BETWEEN 0 AND 10写法不科学啊,效率低下,初级程序员不懂SQL写的烂SQL啊。。。

使用RID+0来骗过查询优化器,让“它”无法估算出BETWEEN 0 AND 10需要返回的行数,这样“它”只能老老实实地“先”做CET内部的查询.

PS: 我骗得过查询优化器,骗不过开发大哥,他一直认为这个写法太BT,问了其他的DBA好几次,就是不采纳我的建议,悲催啊。

--==============================================

一个小建议:

不要见到类似WHEERE C1+10>20这种的就叫嚣不好,就喊着不能走索引的口号,看看场景再说么,万一C1上就压根没有索引呢?

--===========================================================================

ROW_NUMBER在实现分页行的确很好用,但是也不是所有场景都适用,这是一个真实的例子

一个查询只有两个参数@P1和@P2,代表取第@P1行到第@P2行之间的数

当@P1=0 AND @P2=1000时,消耗是这样的:

表 'XXXDetail'。扫描计数 186,逻辑读取 4922 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'XXX'。扫描计数 1,逻辑读取 809 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 SQL Server 执行时间:
CPU 时间 = 0 毫秒,占用时间 = 73 毫秒。

当@P1=7241284 AND @P2=7240285时,消耗是这样的:

表 'XXXDetail'。扫描计数 1468817,逻辑读取 35838994 次,物理读取 1 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'XXX'。扫描计数 1,逻辑读取 5983509 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 SQL Server 执行时间:
CPU 时间 = 45926 毫秒,占用时间 = 56816 毫秒。

真有份这么多页的,无语吧!!!
既然无语,我就不多做解释,说多就是眼泪,看看就好。

--=============================================================================

打完收工,妹子附上

曲演杂坛--蛋疼的ROW_NUMBER函数的更多相关文章

  1. 曲演杂坛--一条DELETE引发的思考

    原文:曲演杂坛--一条DELETE引发的思考 场景介绍: 我们有一张表,专门用来生成自增ID供业务使用,表结构如下: CREATE TABLE TB001 ( ID ,) PRIMARY KEY, D ...

  2. 曲演杂坛--当ROW_NUMBER遇到TOP

    值班期间研发同事打来电话,说应用有超时,上服务器上检查发现有SQL大批量地执行,该SQL消耗IO资源较多,导致服务器存在IO瓶颈,细看SQL,发现自己都被整蒙了,不知道这SQL是要干啥,处理完问题赶紧 ...

  3. 曲演杂坛--为什么SELECT语句会被其他SELECT阻塞?

    很多刚入门的DBA在捕获阻塞得时候,会问这么一个问题“为什么这个SELECT语句被那个SELECT语句阻塞了,难道不是共享锁么?” 让我们来做个小测试,首先准备一些测试数据: --========== ...

  4. 曲演杂坛--HASH的一点理解

    HASH,百度百科上做如下定义: Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列 ...

  5. 曲演杂坛--特殊字符/生僻字与varchar

    对于中文版的SQL SERVER,默认安装后使用的默认排序规则为Chinese_PRC_CI_AS,在此排序规则下,使用varchar类型来可以“正常存取”存放中文字符以及一些东南亚国家的字符,同时v ...

  6. 曲演杂坛--使用CTE时踩的小坑:No Join Predicate

    在一次系统优化中,意外发现一个比较“坑”的SQL,拿出来供大家分享. 生成演示数据: --====================================== --检查测试表是否存在 IF(O ...

  7. 曲演杂坛--EXISTS语句

    通常在我写EXISTS语句时,我会写成IF EXISTS(SELECT TOP(1) 1 FROM XXX),也没细细考究过为什么要这么写,只是隐约认为这样写没有啥问题,那今天就深究下吧! 首先准备测 ...

  8. 曲演杂坛--SQLCMD下执行命令失败但没有任何错误提示的坑

    今天使用SQLCMD导入到SQL SERVER数据库中,看着数据文件都成功执行,但是意外发现有一个文件数据没有成功导入,但执行不报错,很容易导致问题被忽略. 使用存在问题的文件做下测试,从界面上看几行 ...

  9. 曲演杂坛--Update的小测试

    今天偶然想起一个UPDATE相关的小问题,正常情况下,如果我们将UPDATE改写成与之对应的SELECT语句,其SELECT查询结果应与UPDATE的目标表存在一对一的关系,例如: 对于UPDATE语 ...

随机推荐

  1. 使用struct处理二进制

    有的时候需要用python处理二进制数据,比如,存取文件.socket操作时.这时候,可以使用python的struct模块来完成. struct模块中最重要的三个函数是pack(), unpack( ...

  2. Scrapy框架爬虫初探——中关村在线手机参数数据爬取

    关于Scrapy如何安装部署的文章已经相当多了,但是网上实战的例子还不是很多,近来正好在学习该爬虫框架,就简单写了个Spider Demo来实践.作为硬件数码控,我选择了经常光顾的中关村在线的手机页面 ...

  3. 猫哥网络编程系列:HTTP PEM 万能调试法

    注:本文内容较长且细节较多,建议先收藏再阅读,原文将在 Github 上维护与更新. 在 HTTP 接口开发与调试过程中,我们经常遇到以下类似的问题: 为什么本地环境接口可以调用成功,但放到手机上就跑 ...

  4. Java基础Collection集合

    1.Collection是所有集合的父类,在JDK1.5之后又加入了Iterable超级类(可以不用了解) 2.学习集合从Collection开始,所有集合都继承了他的方法 集合结构如图:

  5. Partition:分区切换(Switch)

    在SQL Server中,对超级大表做数据归档,使用select和delete命令是十分耗费CPU时间和Disk空间的,SQL Server必须记录相应数量的事务日志,而使用switch操作归档分区表 ...

  6. Python列表去重

    标题有语病,其实是这样的: 假设有两个列表 : L1 = [1,2,3,4] ; L2 = [1,2,5,6] 然后去掉L1中包含的L2的元素 直接这样当然是不行的: def removeExists ...

  7. UE4新手引导入门教程

    请大家去这个地址下载:file:///D:/UE4%20Doc/虚幻4新手引导入门教程.pdf

  8. 【NLP】Python NLTK处理原始文本

    Python NLTK 处理原始文本 作者:白宁超 2016年11月8日22:45:44 摘要:NLTK是由宾夕法尼亚大学计算机和信息科学使用python语言实现的一种自然语言工具包,其收集的大量公开 ...

  9. C#使用GET、POST请求获取结果

    C#使用GET.POST请求获取结果,这里以一个简单的用户登陆为例. 1. 使用GET请求获取结果 1.1 创建LoginHandler.aspx处理页面 protected void Page_Lo ...

  10. 无法访问org.springframework.core.NestedRuntimeException 找不到org.springframework.core.NestedRuntimeException的类文件

    在学习springAOP时,出现如下异常: 无法访问org.springframework.core.NestedRuntimeException 找不到org.springframework.cor ...