曲演杂坛--蛋疼的ROW_NUMBER函数
使用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函数的更多相关文章
- 曲演杂坛--一条DELETE引发的思考
原文:曲演杂坛--一条DELETE引发的思考 场景介绍: 我们有一张表,专门用来生成自增ID供业务使用,表结构如下: CREATE TABLE TB001 ( ID ,) PRIMARY KEY, D ...
- 曲演杂坛--当ROW_NUMBER遇到TOP
值班期间研发同事打来电话,说应用有超时,上服务器上检查发现有SQL大批量地执行,该SQL消耗IO资源较多,导致服务器存在IO瓶颈,细看SQL,发现自己都被整蒙了,不知道这SQL是要干啥,处理完问题赶紧 ...
- 曲演杂坛--为什么SELECT语句会被其他SELECT阻塞?
很多刚入门的DBA在捕获阻塞得时候,会问这么一个问题“为什么这个SELECT语句被那个SELECT语句阻塞了,难道不是共享锁么?” 让我们来做个小测试,首先准备一些测试数据: --========== ...
- 曲演杂坛--HASH的一点理解
HASH,百度百科上做如下定义: Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列 ...
- 曲演杂坛--特殊字符/生僻字与varchar
对于中文版的SQL SERVER,默认安装后使用的默认排序规则为Chinese_PRC_CI_AS,在此排序规则下,使用varchar类型来可以“正常存取”存放中文字符以及一些东南亚国家的字符,同时v ...
- 曲演杂坛--使用CTE时踩的小坑:No Join Predicate
在一次系统优化中,意外发现一个比较“坑”的SQL,拿出来供大家分享. 生成演示数据: --====================================== --检查测试表是否存在 IF(O ...
- 曲演杂坛--EXISTS语句
通常在我写EXISTS语句时,我会写成IF EXISTS(SELECT TOP(1) 1 FROM XXX),也没细细考究过为什么要这么写,只是隐约认为这样写没有啥问题,那今天就深究下吧! 首先准备测 ...
- 曲演杂坛--SQLCMD下执行命令失败但没有任何错误提示的坑
今天使用SQLCMD导入到SQL SERVER数据库中,看着数据文件都成功执行,但是意外发现有一个文件数据没有成功导入,但执行不报错,很容易导致问题被忽略. 使用存在问题的文件做下测试,从界面上看几行 ...
- 曲演杂坛--Update的小测试
今天偶然想起一个UPDATE相关的小问题,正常情况下,如果我们将UPDATE改写成与之对应的SELECT语句,其SELECT查询结果应与UPDATE的目标表存在一对一的关系,例如: 对于UPDATE语 ...
随机推荐
- SQL数据库之DQL
初来乍到,我是一个Java行业的小学生,刚学半年. 今天老师讲了数据库的操作语句,在这里与大家分享一下我学到的知识吧,要是有不足的地方麻烦大家指出来,共同进步,共同提高! 1.数据库中的各种符号 %: ...
- x:bind不支持样式文件 或 此Xaml文件必须又代码隐藏类才能使用{x:Bind} 解决办法
这两天学习UWP开发,发现一个很有趣的问题,就是我题目中的描述的. 我习惯了在ResourceDictionary中写样式文件,但是发现用x:Bind时会有问题 如果是写在Style里,则提示 “x: ...
- iOS开发之再探多线程编程:Grand Central Dispatch详解
Swift3.0相关代码已在github上更新.之前关于iOS开发多线程的内容发布过一篇博客,其中介绍了NSThread.操作队列以及GCD,介绍的不够深入.今天就以GCD为主题来全面的总结一下GCD ...
- CSS 3学习——box-sizing和背景
box-sizing 在CSS 2中设置元素的width和height仅仅是设置了元素内容区的宽和高,元素实际的尺寸是margin + border + padding + 内容区. CSS 3(截止 ...
- Java多态性——分派
一.基本概念 Java是一门面向对象的程序设计语言,因为Java具备面向对象的三个基本特征:封装.继承和多态.这三个特征并不是各自独立的,从一定角度上看,封装和继承几乎都是为多态而准备的.多态性主要体 ...
- C++常见笔试面试要点以及常见问题
1. C++常见笔试面试要点: C++语言相关: (1) 虚函数(多态)的内部实现 (2) 智能指针用过哪些?shared_ptr和unique_ptr用的时候需要注意什么?shared_ptr的实现 ...
- 页面布局class常见命名规范
头:header 内容:content/container 尾:footer 导航:nav 侧栏:sidebar 栏目:column 页面外围控制整体布局宽度:wrapper 左右中:left rig ...
- 仿陌陌的ios客户端+服务端源码项目
软件功能:模仿陌陌客户端,功能很相似,注册.登陆.上传照片.浏览照片.浏览查找附近会员.关注.取消关注.聊天.语音和文字聊天,还有拼车和搭车的功能,支持微博分享和查找好友. 后台是php+mysql, ...
- centos 6.5 升级php
1>追加CentOS 6.5的epel及remi源. # rpm -Uvh http://ftp.iij.ad.jp/pub/linux/fedora/epel/6/x86_64/epel-re ...
- Hadoop
Hadoop应用场景 Hadoop是专为离线处理和大规模数据分析而设计的,它并不适合那种对几个记录随机读写的在线事务处理模式. 大数据存储:Hadoop最适合一次写入.多次读取的数据存储需求,如数据仓 ...