在应用系统开发初期,由于开发数据库数据比较少,对于查询sql语句,复杂试图的编写等体会不出sql语句各种写法的性能优劣,但是如果将应用系统提交实际应用后,随着数据库中数据的增加,系统的响应速度就成为目前系统需要解决的最主要问题之一。系统优化中一个很重要的方面就是sql语句的优化。对于海量数据,劣质sql语句和优质sql语句之间的速度差别可以达到上百倍,可见对于一个系统不是简单地能实现其功能就行,而是要写出高质量的sql语句,提高系统的可用性。
Oracle的sql调优第一个复杂的主题,甚至需要长篇概论来介绍OracleSQL调优的细微差别。不过有一些基本的规则是每个OracleDBA都需要遵从的,这些规则可以改善他们系统的性能。
sql调优的目标是简单的:消除不必要的大表全表搜索。不必要的全表搜索导致大量不必要的磁盘I/O,从而拖慢整个数据库的性能,对于不必要的全表搜索来说,最常见的调优方法是增加索引,可以在表中加入标准的B树索引,也可以加入位图索引和基于函数的索引。要决定是否消除一个全表搜索,你可以仔细检查索引搜索的I/O开销和全表搜索的开销,它们的开销和数据块的读取和可能的并行执行有关,并将两者作对比。
另外,在全表搜索是一个最快的访问方法时,将小表的全表搜索放到缓存(内存)中,也是一个非常明智的选择。我们会发现现在诞生了很多基于内存的数据库管理系统,将整个数据库置于内存之中,性能将得到质的飞跃。
一、与索引相关的性能优化
在多数情况下,Oracle使用索引来更快地遍历表,优化器主要根据定义的索引来提高性能。但是,如果在sql语句的where子句中写的sql代码不合理,就会造成优化器删去索引而使用全表扫描,一般这种sql语句就是所谓的劣质sql语句。在编写sql语句时我们应清楚优化器根据何种原则来删除索引,这有助于写出高性能的sql语句。
1.IS NULL 与 IS NOT NULL
不能用null做索引,任何包含null值的列都将不会被包含在索引中,即使索引有多列这样的情况下,只要这些列中有一列含有null,该列就会从索引中排除。也就是说某列存在空值,即使对该列建立索引也不会提高性能。任何在where子句中使用is null或者is nou null的语句优化器是不允许使用索引的。
2.联接列
对于有联接的列,即使最后的联接列为一个静态值,优化器是不会使用索引的。来看个例子,假定有一个职工表(employee),对于一个职工的姓和名分成两列存放(FIRST_NAME和LAST_NAME),现在要查询一个叫Beill Cliton的职工。
下面是一个采用联接查询的sql语句:
select * from employee where first_name ||''|| last_name = 'Beill Cliton';
上面这条语句完全可以查询出是否有Beill Cliton这个员工,但是这里需要注意,系统优化器对基于LAST_NAME创建的索引没有使用,当采用下面这种sql语句的编写,Oracle系统就可以采用基于LAST_NAME创建的索引:
select * from employee where first_name = 'Beill' and last_name = 'Cliton';
3.带通配符(%)的like语句
同样拿上面的例子,目前的需求是这样的,要求在职工表中查询名字中包含Cliton的人,可以采用如下的查询sql语句:
select * from employee where last_name like '%Cliton%';
这里由于通配符(%)在搜寻词首出现,所以Oracle系统无法使用last_name的索引。在很多情况下可能无法避免这种情况,但是一定要心里有底,通配符这样使用会降低查询速度。然而当通配符出现在字符串其它位置时,优化器就能利用索引,在下面的查询中索引就得到了使用:
select * from employee where last_name like 'C%';
该语句查询所有姓名以C开头的,这完全满足索引的要求,因为索引本身就是一个排序的列。
4.ORDER BY 子句
ORDER BY子句决定了Oracle如何将返回的查询结果排序。该子句对要排序的列没有什么特别的限制,也可以将函数加入到列中(象联接或者附加等)。任何在该子句的非索引项或者有计算表达式都将降低查询速度。
仔细检查order by子语句找出非索引项或者表达式,它们会降低性能。解决这个问题的办法就是重写order by子句以使用索引,也可以为所使用的列建立另外一个索引,同时应绝对避免在order by子句中使用表达式。
5.NOT 关键字
我们在查询时经常在where子句使用一些逻辑表达式,如大于、小于、等于以及不等于等等,也可以使用and(与)、or(或)以及not(非)。NOT可用来对任何逻辑运算取反。下面是一个NOT子句的例子:
... where not(status = 'VALID');
如果要使用NOT,则应在取反的短语前面加上括号,并在短语前面加上NOT运算符。NOT运算符包含在另外一个逻辑运算符中,这就是不等于(<>)运算符。换句话说,即使不在查询where子句中加入NOT关键字,NOT仍在运算符中,见下例:
... where status <> 'VALID';
再看下面这个例子:
select * from employee where salary <> 3000;
对这个查询,可以改写为不使用NOT:
select * from employee where salary < 3000 or salary > 3000;
虽然这两种查询的结果是一样的,但是第二种查询方案会比第一种查询方案更快些。第二种查询允许Oralce对salary列使用索引,而第一种查询不能使用索引。
6.IN 和 EXISTS
有时候会将一列和一系列值相比较,最简单的办法就是在where子句中使用子查询。在where子句中可以使用两种格式的子查询。
第一种格式是使用IN操作符:
... where column in (select * from ... where ...);
第二种格式是使用EXISTS操作符:
... where exists (select 'X' from ... where ...);
相信绝大多数人会使用第一种格式,因为它比较容易编写,而实际上第二种格式要远比第一种格式的效率高。在Oracle中可以几乎将所有的IN操作符子查询改写为使用EXISTS的子查询。
第二种格式中,子查询以“select 'X'”开始,运用EXISTS子句,不管子查询从表中抽取什么数据,它只查看where子句,这样优化器就不必遍历整个表而仅根据索引就可完成工作(这里假定在where语句中使用的列存在索引)。相对于IN子句来说,EXISTS使用相连子查询构造起来要比IN子查询困难一些。
通过使用EXISTS,Oracle系统会首先检查主查询,然后运行子查询直到它找到第一个匹配项,这就节省了时间。Oralce系统在执行IN子查询时,首先执行子查询,并将获得的结果列表放在一个加了索引的临时表中。在执行子查询之前,系统先将主查询挂起,待子查询执行完毕,存放在临时表中以后再执行主查询。这也就是使用EXISTS比使用IN通常查询速度快的原因。
同时应尽可能使用NOT EXISTS来代替NOT IN,尽管二者都使用了NOT(不能使用索引而降低速度),NOT EXISTS要比NOT IN查询效率更高。
7.<> 不等于符号
不等于操作符是永远不会用到索引的,因为对它的处理只会产生全表扫描。
推荐方案:用其它相同功能的操作运算符代替,如
a<>0 改为 a>0 or a<0, a<>'' 改为 a >''
8.避免在索引列上使用计算
WHERE子句中,如果索引列是函数的一部分,优化器将不使用索引而使用全表扫描
低效:
select ... from dept where SAL * 12 > 25000;
高效:
select ... from dept where SAL > 25000/12;
9.总是使用索引的第一个列
如果索引是建立在多个列上,只有在它的第一个列(leading column)被where子句引用时,优化器才会选择使用该索引。这也是一条简单而重要的规则,当仅引用索引的第二个列时,优化器使用了全表扫描而忽略了索引。
10.避免改变索引列的类型
当比较不同数据类型的数据时,Oralce自动对列进行简单的类型转换。
假设empno是一个数值类型的索引列:
select ... from emp where empno = '';
实际上,经过了Oracle类型转换,语句转化为:
select ... from emp where empno = TO_NUMBER('');
幸运的是,类型转换没有发生在索引列上,索引的用途没有被改变。
现在,假设emp_type是一个字符类型的索引列:
select ... from emp where emp_type = 123;
这个语句被Oracle转换为:
select ... from emp where TO_NUMBER(emp_type) = 123;
因为内部发生的类型转换,这个索引将不会被用到。为了避免Oracle对sql进行隐式的类型转换,最好把类型转换用显示表现出来。注意当字符和数值比较时,Oracle会优先转换数值类型到字符类型。
11.需要当心的 WHERE 子句
某些select语句中的where子句不使用索引:
- '!='将不使用索引,索引只能告诉你什么存在于表中,而不能告诉你什么不存在于表中
- '||'是字符连接函数,就像其它函数那样,停用了索引
- '+'是数学函数,就像其它数学函数那样,停用了索引
- 相同的索引列不能互相比较,这将会启动全表扫描
12.其它一些规则
- 如果检索数据量超过30%的表中记录数,使用索引将没有显著的效率提高
- 在特定情况下,使用索引也许会比全盘扫描慢,但这是同一个数量级上的区别。而通常情况下,使用索引比全表扫描要快几倍乃至几千倍
- 避免在索引列上使用IS NULL 和IS NOT NULL
- 避免在索引列上使用NOT
- 用EXISTS替代IN、用NOT EXISTS替代NOT IN
- 通过内部函数提高sql效率
- 选择最有效的表名顺序:Oracle的解析器按照从右到左的顺序处理from子句中的表名,from子句中写在最后的表(基础表)将被最先处理,在from子句中包含多个表的情况下,你必须选择记录条数最少的表作为基础表。如果有三个以上的表连接查询,那就需要选择交叉表作为基础表,交叉表是指被其它表所引用的表
- WHERE子句中的连接顺序:Oracle采用自下而上的顺序解析where子句,根据这个原理,表之间的连接必须写在其它where条件之前,那些可以过滤掉最大数量记录的条件必须写在where子句的末尾
二、与内存相关的优化
1.UNION 操作符
UNION在进行表链接后会筛选掉重复的记录,所以在表链接后会对所产生的结果集进行排序运算,删除重复的记录再返回结果。实际大部分应用中是不会产生重复的记录。最常见的是过程表与历史表UNION。如:
select * from A union select * from B;
这个sql在运行时先取出两个表的结果,再用排序空间进行排序删除重复的记录,最后返回结果集,如果表数据量大的话可能会导致用磁盘进行排序。
推荐方案:采用UNION ALL操作符替代UNION,因为UNION ALL操作只是简单的将两个结果合并后就返回:
select * from A union all select * from B;
2.SQL书写的影响
同一功能同一性能不同写法sql的影响
如,一个sql在A程序员写的为:
select * from employee;
B程序员写为:
select * from scott.employee; (带表所有者的前缀)
C程序员写为:
select * from EMPLOYEE; (大写表名)
D程序员写为:
select * from employee; (中间多了空格)
以上四个sql在Oracle分析整理之后产生的结果及执行的时间是一样的,但是从Oracle共享内存SGA的原理,可以得出Oracle对每个sql都会对其进行一次分析,并且占用共享内存,如果将sql的字符串及格式写得完全相同则Oracle只会分析一次,共享内存也只会留下一次的分析结果,这不仅可以减少分析sql的时间,而且也可以减少共享内存重复的信息,oracle也可以准确统计sql的执行频率。
3.避免在磁盘中排序
当与Oracle建立起一个session时,在内存中就会为该session分配一个私有的排序区域。如果该连接是一个专用的连接(dedicated connection),那么就会根据init.ora中sort_area_size参数的大小在内存中分配一个Program Global Area(PGA)。如果连接是通过多线程服务器建立的,那么排序的空间就在large_pool中分配。不幸的是,对于所有的session,用作排序的内存量都必须是一样的,我们不能为需要更大排序的操作分配额外的排序区域。因此,设计者必须做出一个平衡,在分配足够的排序区域以避免发生大的排序任务时出现磁盘排序(disksorts)的同时,对于那些并不需要进行很大排序的任务,就会出现一些浪费。当然,当排序的空间需求超出了sort_area_size的大小时,这时将会在TEMP表空间中分页进行磁盘排序。磁盘排序要比内存排序大概慢14000倍。
上面我们已经提到,私有排序区域的大小是由init.ora中的sort_area_size参数决定的。每个排序所占用的大小由init.ora中的sort_area_size参数决定。当排序不能在分配的空间中完成时,就会使用磁盘排序的方式,即在Oracle实例中的临时表空间中进行。
磁盘排序的开销是很大的,有几个方面的原因。首先,和内存排序相比较,它们特别慢;而且,磁盘排序会消耗临时表空间中的资源。Oracle还必须分配缓冲池块来保持临时表空间中的块。无论什么时候,内存排序都比磁盘排序好,磁盘排序将会令任务变慢,并且会影响Oracle实例的当前任务的执行。还有,过多的磁盘排序将会令freebufferwaits的值特别高,从而令其它任务的数据块由缓冲中移走。
4.避免使用耗费资源的操作
带有 DISTINCT,UNION,MINUS,INTERSECT,ORDER BY的sql语句回启动sql引擎执行耗费资源的排序(SORT)功能。DISTINCT需要一次排序操作,而其它的至少需要执行两次排序。通常,带有UNION,MINUS,INTERSECT的sql语句都可以用其它方式重写。如果你的数据库的sort_area_size调配的好,使用UNION,MINUS,INTERSECT也是可以考虑的,毕竟它们的可读性很强。
三、其它性能优化相关技巧
1.删除重复记录
最高效的删除重复记录方法(因使用了ROWID)例子:
delete from emp E where E.ROWID > (select MIN(X.ROWID) from emp X where X.emp_no = E.emp_no);
2.用 TRUNCATE 替代 DELETE
当删除表中的记录时,在通常情况下,回滚段(rollback segments)用来存放可以被恢复的信息。如果你没有COMMIT事务,Oracle会将数据恢复到删除之前的状态(准确地说是恢复到执行删除之前的状态),而当运用TRUNCATE时,回滚段不再存放任何可被恢复的信息。当命令运行后,数据不能被恢复,因此很少的资源被调用,执行时间也会很短(TRUNCATE只在清空全表适用,TRUNCATE是DDL而不是DML)。
3.SELECT 子句中避免使用 *
Oracle在解析的过程中,会将 * 依次转换成所有的列名,这个工作是通过查询数据字典完成的,这意味着将耗费更多的时间。
4.用 WHERE 子句替换 HAVING 子句
避免使用having子句,having只会在检索出所有记录之后才对结果集进行过滤,这个处理需要排序、总计等操作。如果能通过where子句限制记录的数目,那就能减少这方面的开销。sql语句中on、where、having这三个都可以加条件的子句中,on是最先执行,where次之,having最后,因为on是先把不符合条件的记录过滤后才进行统计,它就可以减少中间运算要处理的数据,按理说应该是速度最快的,where也应该比having快点的,因为它过滤数据后才进行sum,在两个表连接时采用on,所以在一个表的时候,就剩下where跟having比较了。在单表查询统计的情况下,如果要过滤的条件没有涉及到要计算字段,那它们的结果是一样的,只是where可以使用rushmore技术,而having就不能,在速度上后者要慢如果要涉及到计算的字段,就表示在没计算之前,这个字段的值是不确定的,where的作用时间是在计算之前就完成的,而having就是在计算之后才起作用的,所以在这种情况下,两者的结果会不同。在多表联接查询时,on比where更早起作用,系统首先根据各个表之间的连接条件,把多个表合成一个临时表后,再由where进行过滤,然后再计算,计算完成后再由having进行过滤。由此可见,要想过滤条件起到正确的作用,首先要明白这个条件应该在什么时候起作用,然后再决定放在哪里。
5.使用表的别名(Alias)
当在sql语句中连接多个表时,请使用表的别名并把别名前缀于每个column上。这样一来,就可以减少解析的时间并减少那些由column歧义引起的语法错误。
6.用 EXISTS 替代 IN、用 NOT EXISTS 替代 NOT IN
在许多基于基础表的查询中,为了满足一个条件,往往需要对另一个表进行联接。在这种情况下,使用EXISTS(或NOT EXISTS)通常将提高查询的效率。在子查询中NOT IN子句将执行一个内部的排序和合并。无论在哪种情况下,NOT IN都是最低效的(因为它对子查询中的表执行了一个全表遍历)。为了避免使用NOT IN,我们可以把它改写成外联接(OUTER JOIN)或NOT EXISTS。
高效:
select * from emp where empno > 0 and EXISTS (select 'X' from dept where dept.deptno = emp.deptno and loc = 'MELB');
低效:
select * from emp where empno > 0 and deptno IN (select deptno from dept where loc = 'MELB');
7.用 EXISTS 替换 DISTINCT
提交一个对多表信息(比如部门表和雇员表)进行查询的语句时,避免在select子句中使用distinct。一般可以考虑用EXISTS替换,EXISTS使查询更为迅速,因为RDBMS核心模块将在子查询的条件一但满足后,立刻返回结果。
高效:
select dept_no, dept_name from dept D where EXISTS (select 'X' from emp E where E.deptno = D.deptno);
低效:
select DESTINCT dept_no, dept_name from dept D, emp E where D.deptno = E.deptno;
8.SQL语句使用大写
因为Oracle总是先解析sql语句,把小写的字母转换成大写的再执行
9.用 >= 替代 >
高效:
select * from emp where deptno >= 4;
低效:
select * from emp where deptno > 3;
两者的区别在于,前者DBMS将直接跳到第一个deptno等于4的记录,而后者将首先定位到deptno=3的记录并且向前扫描到第一个deptno大于3的记录。
10.优化 GROUP BY
提高group by语句的效率,可以通过将不需要的记录在group by之前过滤掉。下面两个查询返回相同结果,但第二个就明显快了许多。
低效:
select job, AVG(SAL) from emp group by job having job = 'PRESIDENT' or job = 'MANAGER';
高效:
select job, AVG(SAL) from emp where job = 'PRESIDENT' or job = 'MANAGER' group by job;