SQL调优常用方法
在使用DBMS时经常对系统的性能有非常高的要求:不能占用过多的系统内存和 CPU资源、要尽可能快的完成的数据库操作、要有尽可能高的系统吞吐量。如果系统开发出来不能满足要求的所有性能指标,则必须对系统进行调整,这个工作被称为调优。绝定DBMS的性能的因素有两个因素:硬件和软件。使用频率高的CPU、使用多处理器、加大内存容量、增加Cache、提高网络速度等这些都是非常有效的硬件调优方式,不过对硬件进行调优对系统性能的提高是有限的,如果有非常好的硬件条件但是如果编写的SQL质量非常差的话系统的性能并不会有明显的改善,而如果能对SQL语句进行充分的优化的话即使硬件条件稍差的话,系统性能的变化也是非常惊人的。硬件的调优涉及到非常多的内容,不是本节所能覆盖的,因此本节将主要讲解SQL的调优。
SQL调优的基本原则
"二八原理" 是一个普遍的真理,特别是在计算机的世界中表现的更加明显,那就是20%的代码的资源消耗占用了80%的总资源消耗。SQL语句也是一种代码,因此它也符合这个原理。在进行SQL调优的时候应该把主要精力放到这20%的最消耗系统资 源的SQL语句中,不要想把所有的SQL语句都调整到最优状态。
很多DBMS都提供了非常好的工具用来分析系统中所有SQL语句资源消耗的工具,借助于这些工具发现占用系统资源排在前面的SQL语句,然后尝试对它们进行优化,优化后再次执行分析,迭代这一过程,直到系统中没有明显的系统资源消耗异常的SQL语句为止。
索引
索引是数据库调优的最根本的优化方法,很多优化手法都是围绕索引展开的,可以说索引是一切优化手法的“内功”,而所有的优化手法都是由索引衍化出来的招式而已。
根据索引的顺序与数据表的物理顺序是否相同,可以把索引分成两种类型:聚簇索引,数据表的物理顺序与索引顺序相同;非聚簇索引,数据表的物理顺序与索引顺序不相同。下面,我们举例来说明一下聚集索引和非聚集索引的区别:
字典的目录就是一种索引,因为通过目录我们可以很快的定位到要检索的内容,而不用从头到尾把字典翻一遍。汉语字典一般都至少提供两种目录,一种是拼音目录,一种是偏旁部首目录。汉语字典是按照拼音的顺序排列的,因此拼音目录就是聚集索引,而偏旁部首目录则是非聚集索引。应该在表中经常搜索的列或者按照顺序访问的列上创建聚簇索引。当创建聚簇索引时要需要每一个表只能有一个聚簇索引,因为表中数据的物理顺序只能有一个,而非聚集索引则可以创建多个。
由于索引需要占据一定的存储空间,而且索引也会降低数据插入、更新和删除的速度,所以应该只创建必要的索引,一般是在检索的时候用的字段中创建索引。
索引还会造成存储碎片的问题。当删除一条记录时将会导致对应的索引中的该记录的对应项为空,由于索引是采用B树结构存储的,所以对应的索引项并不会被删除,经过一段时间的增删改操作后,数据库中就会出现大量的存储碎片,这和磁盘碎片、内 存碎片产生原理是类似的,这些存储碎片不仅占用了存储空间,而且降低了数据库运行的速度。如果发现索引中存在过多的存储碎片的话就要进行“碎片整理”了,最方便的“碎片整理”手段就是重建索引,重建索引会将先前创建的索引删除然后重新创建索引,主流数据库管理系统都提供了重建索引的功能,比如REINDEX、REBUILD等,如果 使用的数据库管理系统没有提供重建索引的功能,可以首先用DROP INDEX语句删除索引,然后用ALTER TABLE语句重新创建索引。
表扫描和索引查找
一般地,系统访问数据库中的数据,可以使用两种方法:全表扫描和索引查找。
全表扫描,就是指系统必须在数据表中逐条检索表中的每条记录,以检查该记录是否匹配检索条件。全表扫描有可能会造成巨大的性能损失,当然也有可能不会影响性能,这取决于表中的数据量,如果表中有上千万条甚至上亿条记录的话,全表扫描的速度会 非常慢,而如果表中只有几条、几十条记录的话表扫描的性能消耗就可以忽略不计了。当表中数据量比较小的时候,使用全表扫描非常有用。但是随着表中数据量的增加,全表扫描会导致系统性能严重下降。
如果表中有索引并且待匹配条件符合索引的要求的话,DBMS就不会执行全表扫描,而是直接到索引中查找,这将大大加快检索的速度。
DBMS中都有查询优化器,它会根据分布的统计信息生成该查询语句的优化执行规划,以提高访问数据的效率为目标,确定是使用全表扫描还是使用索引查找。注意并不是表中存在索引在进行检索的时候就会使用索引查找,如果使用不当检索的过程仍然会是采用全表扫描,这样索引就起不到效果了。
优化手法
下面将会列出了一些常用的优化手法,注意这些优化手法只是一些常规条件下的优化手法,具体的优化效果是与使用的DBMS以及数据的特点密切相关的,需要根据具体情况来使用不同的优化手法,如果使用不当的话有可能会适得其反。
1、创建必要的索引
在经常需要进行检索的字段上创建索引,比如经常要按照图书名称进行检索,那么就应该在图书名称字段上创建索引,如果经常要按照员工部门和员工岗位级别进行检索,那么就应该在员工部门和员工岗位级别这两个字段上创建索引。创建索引给检索带来的性能提升往往是巨大的,因此在发现检索速度过慢的时候应该首先想到的就是创建索引。
2、使用预编译查询
程序中通常是根据用户的输入来动态执行SQL语句,这时应该尽量使用参数化SQL,这样不仅可以避免SQL注入漏洞攻击,最重要数据库会对这些参数化SQL执行预编译,这样第一次执行的时候DBMS会为这个SQL语句进行查询优化并且执行预编译,这样以后再执行这个SQL的时候就直接使用预编译的结果,这样可以大大提高执行的速度。
3、调整WHERE子句中的连接顺序
DBMS一般采用自下而上的顺序解析WHERE子句,根据这个原理,表连接最好写在其他WHERE条件之前,那些可以过滤掉最大数量记录的条件写在WHERE子句的末尾。
比如下面的SQL语句性能较差:
1 SELECT * 2 FROM T_Person 3 WHERE FSalary > 50000 4 AND FPosition= 'MANAGER' 5 AND 25 < (SELECT COUNT(*) 6 FROM T_Manager 7 WHERE FManagerId=2);
我们将子查询的条件放到最前面,下面的SQL语句性能比较好:
1 SELECT * 2 FROM T_Person 3 WHERE 4 25 < (SELECT COUNT(*) 5 FROM T_Manager 6 WHERE FManagerId = 2) 7 AND FSalary > 50000 8 AND FPosition= 'MANAGER';
4、SELECT语句中避免使用'*'
SELECT *比较简单,但是除非确实需要检索所有的列,否则将会检索出不需要的列,这回增加网络的负载和服务器的资源消耗;即使确实需要检索所有列,也不要使用 SELECT *,因为这是一个非常低效的方法,DBMS在解析的过程中,会将*依次转换成所有的列名,这意味着将耗费更多的时间。
5、尽量将多条SQL语句压缩到一句SQL中
每次执行SQL的时候都要建立网络连接、进行权限校验、进行SQL语句的查询优化、发送执行结果,这个过程是非常耗时的,因此应该尽量避免过多的执行SQL语句, 能够压缩到一句SQL执行的语句就不要用多条来执行。
6、用Where子句替换HAVING子句
避免使用HAVING子句,因为HAVING 只会在检索出所有记录之后才对结果集进行过滤。如果能通过WHERE子句限制记录的数目,那就能减少这方面的开销。 HAVING 中的条件一般用于聚合函数的过滤,除此而外,应该将条件写在WHERE子句中。
7、使用表的别名
当在SQL语句中连接多个表时,请使用表的别名并把别名前缀于每个列名上。这样就可以减少解析的时间并减少那些由列名歧义引起的语法错误。
8、用EXISTS替代IN
在查询中,为了满足一个条件,往往需要对另一个表进行联接,在这种情况下,使用EXISTS而不是IN通常将提高查询的效率,因为IN 子句将执行一个子查询内部的排序和合并。下面的语句2就比语句1效率更加高。
1 --语句1: 2 SELECT * FROM T_Employee 3 WHERE FNumber> 0 4 AND FDEPTNO IN (SELECT FNumber 5 FROM T_Department 6 WHERE FMangerName = 'Tome') 7 8 --语句2: 9 SELECT * FROM T_Employee EMP 10 WHERE EMP.FNumber > 0 11 AND EXISTS (SELECT 1 12 FROM T_Department DEP 13 WHERE DEP.FDEPTNO = EMP.FNumber 14 AND DEP.FMangerName = 'Tome')
9 、用表连接替换EXISTS
通常来说,表连接的方式比EXISTS更有效率,因此如果可能的话尽量使用表连接替换EXISTS。下面的语句2就比语句1效率更加高。
1 --语句1: 2 SELECT * FROM T_Employee EMP 3 WHERE EMP.FNumber > 0 4 AND EXISTS (SELECT 1 5 FROM T_Department DEP 6 WHERE DEP.FDeptno = EMP.FNumber 7 AND DEP.FKind = 'A') 8 --语句2: 9 SELECT FName FROM T_Department DEP,T_Employee EMP 10 WHERE DEP.FDeptno = EMP.FNumber 11 AND DEP.FKind = 'A';
10、避免在索引列上使用计算
在WHERE子句中,如果索引列是计算或者函数的一部分,DBMS的优化器将不会使用索引而使用全表扫描。
例如下面的SQL语句用于检索月薪的12倍大于两万五千元的员工:
1 SELECT * 2 FROM T_Employee 3 WHERE FSalary * 12 >25000;
由于在大于号左边的是FSalary与12的成绩表达式,这样DBMS的优化器将不会使用字段FSalary的索引,因为DBMS必须对T_Employee表进行全表扫描,从而计算 FSalary * 12 的值,然后与25000进行比较。将上面的SQL语句修改为下面的等价写法后DBMS将会使用索引查找,从而大大提高了效率:
1 SELECT * 2 FROM T_Employee 3 WHERE FSalary >25000/12;
同样的,不能在索引列上使用函数,因为函数也是一种计算,会造成全表扫描。下面的语句2就比语句1效率更加高。
1 --语句1: 2 SELECT * 3 FROM T_Example 4 WHERE ABS(FAmount)=300 5 --语句2: 6 SELECT * 7 FROM T_Example 8 WHERE FAmount=300 OR FAmount=-300
11、用UNION ALL 替换UNION
当SQL语句需要UNION两个查询结果集合时,即使检索结果中不会有重复的记录,如果使用UNION这两个结果集同样会尝试进行合并,然后在输出最终结果前进行排序。
因此,如果检索结果中不会有重复的记录的话,应该用UNION ALL替代UNION,这样效率就会因此得到提高。下面的语句2就比语句1效率更加高。
1 --语句1: 2 SELECT ACCT_NUM, BALANCE_AMT 3 FROM DEBIT_TRANSACTIONS1 4 WHERE TRAN_DATE = '20010101' 5 UNION 6 SELECT ACCT_NUM, BALANCE_AMT 7 FROM DEBIT_TRANSACTIONS2 8 WHERE TRAN_DATE ='20010102'; 9 --语句2: 10 SELECT ACCT_NUM, BALANCE_AMT 11 FROM DEBIT_TRANSACTIONS1 12 WHERE TRAN_DATE ='20010101' 13 UNION ALL 14 SELECT ACCT_NUM, BALANCE_AMT 15 FROM DEBIT_TRANSACTIONS2 16 WHERE TRAN_DATE = '20010102';
12、避免隐式类型转换造成的全表扫描
T_Person表的字符串类型字段FLevel为人员的级别,在FAge字段上建有索引。我们执行下面的SQL语句用于检索所有级别等于10的员工:
1 SELECT FId,FAge,FName 2 FROM T_Person 3 WHERE FAge=10
在这个SQL语句中,将字符串类型字段FLevel与数值10进行比较,由于在大部分数据库中隐式转换类型中数值类型的优先级高于字符串类型,因此DBMS会对FAge字段进行隐式类型转换,相当于执行了下面的SQL语句:
1 SELECT FId,FAge,FName 2 FROM T_Person 3 WHERE TO_INT(FAge)=10;
由于在索引字段上进行了计算,所以造成了索引失效而使用全表扫描。因此应将 SQL语句做如下修改:
1 SELECT FId,FAge,FName 2 FROM T_Person 3 WHERE FAge='10';
13、防止检索范围过宽
如果DBMS优化器认为检索范围过宽,那么它将放弃索引查找而使用全表扫描。下面是几种可能造成检索范围过宽的情况:使用IS NOT NULL或者不等于判断,可能造成优化器假设匹配的记录数太多。 使用LIKE运算符的时候,"a%"将会使用索引,而"a%c"和"%c"则会使用全表扫描,因此"a%c"和"%c"不能被有效的评估匹配的数量。
14、SQL语句尽量用大写
对Oracle来说,Oracle总是先解析sql语句,把小写的字母转换成大写的再执行。
15、利用with字句重用查询
如获得工资大于平均工资的员工和工资信息:
1 select * from ( 2 select employee_id,avg(salary) avg_salary 3 from salary 4 group by employee_id 5 )t 6 where t.avg_salary >( 7 select avg(avg_salary) from ( 8 select employee_id,avg(salary) avg_salary 9 from salary 10 group by employee_id 11 ));
可以看到子查询:
1 select employee_id,avg(salary) avg_salary 2 from salary 3 group by employee_id;
被重复执行,通过with子句可以将该子查询独立出来,并重用其查询结果,如下所示:
1 with employee_avg_salary as ( 2 select employee_id,avg(salary) avg_salary 3 from salary 4 group by employee_id) 5 select * from employee_avg_salary t 6 where t.avg_salary > ( 7 select avg(avg_salary) 8 from employee_avg_salary);