笔者在工作实践中发现
不良的SQL往往来自于不恰当的索引设计
不充份的连接条件和不可优化的where子句
以下就对数据库优化问题进行了介绍
需要的朋友可以参考下
人们在使用SQL时往往会陷入一个误区即太关注于所得的结果是否正确而忽略了不同的实现方法之间可能存在的性能差异这种性能差异在大型的或是复杂的数据库环境中(如联机事务处理OLTP或决策支持系统DSS)中表现得尤为明显
笔者在工作实践中发现不良的SQL往往来自于不恰当的索引设计不充份的连接条件和不可优化的where子句
在对它们进行适当的优化后
其运行速度有了明显地提高!
下面将从这三个方面分别进行总结为了更直观地说明问题
所有实例中的SQL运行时间均经过测试
不超过1秒的均表示为(<
秒)
测试环境:主机
HP LH II
主频
MHZ
内存
兆
操作系统
Operserver
数据库
Sybase
一不合理的索引设计例
表record有
行
试看在不同的索引下
下面几个SQL的运行情况
在date上建有一非个群集索引select count(*) from record where date >
and date <
and amount >
(
秒)select date
sum(amount) from record group by date(
秒)select count(*) from record where date >
and place in (
BJ
SH
) (
秒)
分析
date上有大量的重复值
在非群集索引下
数据在物理上随机存放在数据页上
在范围查找时
必须执行一次表扫描才能找到这一范围内的全部行
在date上的一个群集索引select count(*) from record where date >
and date <
and amount >
(
秒)select date
sum(amount) from record group by date(
秒)select count(*) from record where date >
and place in (
BJ
SH
)(
秒)
分析
在群集索引下
数据在物理上按顺序在数据页上
重复值也排列在一起
因而在范围查找时
可以先找到这个范围的起末点
且只在这个范围内扫描数据页
避免了大范围扫描
提高了查询速度
在place
date
amount上的组合索引select count(*) from record where date >
and date <
and amount >
(
秒)select date
sum(amount) from record group by date(
秒)select count(*) from record where date >
and place in (
BJ
SH
)(<
秒)
分析
这是一个不很合理的组合索引
因为它的前导列是place
第一和第二条SQL没有引用place
因此也没有利用上索引
第三个SQL使用了place
且引用的所有列都包含在组合索引中
形成了索引覆盖
所以它的速度是非常快的
在date
place
amount上的组合索引select count(*) from record where date >
and date <
and amount >
(<
秒)select date
sum(amount) from record group by date(
秒)select count(*) from record where date >
and place in (
BJ
SH
)(<
秒)
分析
这是一个合理的组合索引
它将date作为前导列
使每个SQL都可以利用索引
并且在第一和第三个SQL中形成了索引覆盖
因而性能达到了最优
总结缺省情况下建立的索引是非群集索引
但有时它并不是最佳的
合理的索引设计要建立在对各种查询的分析和预测上
一般来说
①
有大量重复值
且经常有范围查询(between
>
<
>=
< =)和order by
group by发生的列
可考虑建立群集索引
②
经常同时存取多列
且每列都含有重复值可考虑建立组合索引
③
组合索引要尽量使关键查询形成索引覆盖
其前导列一定是使用最频繁的列
二不充份的连接条件例
表card有
行
在card_no上有一个非聚集索引
表account有
行
在account_no上有一个非聚集索引
试看在不同的表连接条件下
两个SQL的执行情况
select sum(a
amount) from account a
card b where a
card_no = b
card_no(
秒)select sum(a
amount) from account a
card b where a
card_no = b
card_no and a
account_no=b
account_no(<
秒)
分析
在第一个连接条件下
最佳查询方案是将account作外层表
card作内层表
利用card上的索引
其I/O次数可由以下公式估算为
外层表account上的
页+(外层表account的
行*内层表card上对应外层表第一行所要查找的
页)=
次I/O在第二个连接条件下
最佳查询方案是将card作外层表
account作内层表
利用account上的索引
其I/O次数可由以下公式估算为
外层表card上的
页+(外层表card的
行*内层表account上对应外层表每一行所要查找的
页)=
次I/O可见
只有充份的连接条件
真正的最佳方案才会被执行
总结多表操作在被实际执行前
查询优化器会根据连接条件
列出几组可能的连接方案并从中找出系统开销最小的最佳方案
连接条件要充份考虑带有索引的表
行数多的表
内外表的选择可由公式
外层表中的匹配行数*内层表中每一次查找的次数确定
乘积最小为最佳方案
查看执行方案的方法
用set showplanon
打开showplan选项
就可以看到连接顺序
使用何种索引的信息
想看更详细的信息
需用sa角色执行dbcc(
)
三不可优化的where子句例
下列SQL条件语句中的列都建有恰当的索引
但执行速度却非常慢
select * from record wheresubstring(card_no
)=
(
秒)select * from record whereamount/
<
(
秒)select * from record whereconvert(char(
)
date
)=
(
秒)分析
where子句中对列的任何操作结果都是在SQL运行时逐列计算得到的
因此它不得不进行表搜索
而没有使用该列上面的索引
如果这些结果在查询编译时就能得到
那么就可以被SQL优化器优化
使用索引
避免表搜索
因此将SQL重写成下面这样
select * from record where card_no like
%
(<
秒)select * from record where amount<
*
(<
秒)select * from record where date=
/
/
(<
秒)你会发现SQL明显快起来!
例
表stuff有
行
id_no上有非群集索引
请看下面这个SQL
select count(*) from stuff where id_no in(
)(
秒)分析
where条件中的
in
在逻辑上相当于
or
所以语法分析器会将in (
)转化为id_no =
or id_no=
来执行
我们期望它会根据每个or子句分别查找
再将结果相加
这样可以利用id_no上的索引
但实际上(根据showplan)
它却采用了"OR策略"
即先取出满足每个or子句的行
存入临时数据库的工作表中
再建立唯一索引以去掉重复行
最后从这个临时表中计算结果
因此
实际过程没有利用id_no上索引
并且完成时间还要受tempdb数据库性能的影响
实践证明
表的行数越多
工作表的性能就越差
当stuff有
行时
执行时间竟达到
秒!还不如将or子句分开
select count(*) from stuff where id_no=
select count(*) from stuff where id_no=
得到两个结果
再作一次加法合算
因为每句都使用了索引
执行时间只有
秒
在
行下
时间也只有
秒
或者
用更好的方法
写一个简单的存储过程
create proc count_stuff asdeclare @a intdeclare @b intdeclare @c intdeclare @d char(
)beginselect @a=count(*) from stuff where id_no=
select @b=count(*) from stuff where id_no=
endselect @c=@a+@bselect @d=convert(char(
)
@c)print @d直接算出结果
执行时间同上面一样快!
总结可见
所谓优化即where子句利用了索引
不可优化即发生了表扫描或额外开销
任何对列的操作都将导致表扫描
它包括数据库函数
计算表达式等等
查询时要尽可能将操作移至等号右边
in
or子句常会使用工作表
使索引失效
如果不产生大量重复值
可以考虑把子句拆开
拆开的子句中应该包含索引
要善于使用存储过程
它使SQL变得更加灵活和高效
从以上这些例子可以看出
SQL优化的实质就是在结果正确的前提下
用优化器可以识别的语句
充份利用索引
减少表扫描的I/O次数
尽量避免表搜索的发生
其实SQL的性能优化是一个复杂的过程
上述这些只是在应用层次的一种体现
深入研究还会涉及数据库层的资源配置
网络层的流量控制以及操作系统层的总体设计
开发人员如果用到其他库的Table或View
务必在当前库中建立View来实现跨库操作
最好不要直接使用“databse
dbo
table_name”
因为sp_depends不能显示出该SP所使用的跨库table或view
不方便校验
开发人员在提交SP前
必须已经使用set showplan on分析过查询计划
做过自身的查询优化检查
高程序运行效率
优化应用程序
在SP编写过程中应该注意以下几点
a) SQL的使用规范
i
尽量避免大事务操作
慎用holdlock子句
提高系统并发能力
ii
尽量避免反复访问同一张或几张表
尤其是数据量较大的表
可以考虑先根据条件提取数据到临时表中
然后再做连接
iii
尽量避免使用游标
因为游标的效率较差
如果游标操作的数据超过
万行
那么就应该改写
如果使用了游标
就要尽量避免在游标循环中再进行表连接的操作
iv
注意where字句写法
必须考虑语句顺序
应该根据索引顺序
范围大小来确定条件子句的前后顺序
尽可能的让字段顺序与索引顺序相一致
范围从大到小
v
不要在where子句中的“=”左边进行函数
算术运算或其他表达式运算
否则系统将可能无法正确使用索引
vi
尽量使用exists代替select count(
)来判断是否存在记录
count函数只有在统计表中所有行数时使用
而且count(
)比count(*)更有效率
vii
尽量使用“>=”
不要使用“>”
viii
注意一些or子句和union子句之间的替换
ix
注意表之间连接的数据类型
避免不同类型数据之间的连接
x
注意存储过程中参数和数据类型的关系
xi
注意insert
update操作的数据量
防止与其他应用沖突
如果数据量超过
个数据页面(
k)
那么系统将会进行锁升级
页级锁会升级成表级锁
b) 索引的使用规范
i
索引的创建要与应用结合考虑
建议大的OLTP表不要超过
个索引
ii
尽可能的使用索引字段作为查询条件
尤其是聚簇索引
必要时可以通过index index_name来强制指定索引
iii
避免对大表查询时进行table scan
必要时考虑新建索引
iv
在使用索引字段作为条件时
如果该索引是联合索引
那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引
否则该索引将不会被使用
v
要注意索引的维护
周期性重建索引
重新编译存储过程
c) tempdb的使用规范
i
尽量避免使用distinct
order by
group by
having
join
***pute
因为这些语句会加重tempdb的负担
ii
避免频繁创建和删除临时表
减少系统表资源的消耗
iii
在新建临时表时
如果一次性插入数据量很大
那么可以使用select into代替create table
避免log
提高速度
如果数据量不大
为了缓和系统表的资源
建议先create table
然后insert
iv
如果临时表的数据量较大
需要建立索引
那么应该将创建临时表和建立索引的过程放在单独一个子存储过程中
这样才能保证系统能够很好的使用到该临时表的索引
v
如果使用到了临时表
在存储过程的最后务必将所有的临时表显式删除
先truncate table
然后drop table
这样可以避免系统表的较长时间锁定
vi
慎用大的临时表与其他大表的连接查询和修改
减低系统表负担
因为这种操作会在一条语句中多次使用tempdb的系统表
d) 合理的算法使用
根据上面已提到的SQL优化技术和ASE Tuning手册中的SQL优化内容
结合实际应用
采用多种算法进行比较
以获得消耗资源最少
效率最高的方法
具体可用ASE调优命令
set statistics io on
set statistics time on
set showplan on 等