上周给南京某客户一个重要业务系统的数据库做优化,能实施的马上做了实施,优化前后性能对比非常明显,系统最为严重的IO负载过重问题基本得到解决:优化前一天的物理读是48亿次,优化后是15亿次,效果那是刚刚的,业务处理效率明显改善,磁盘的寿命也会有大幅提升。 这些还只是优化建议的一部分。
如果开发人员能把改SQL和业务逻辑的优化建议都完成,一天物理读少于5亿次应该也是完全没问题的。如果不知道优化能够带来如此巨大的提升,可能业务部门会考虑花个上百万换个闪存存储都有可能,实际上当前的普通存储对业务要求来说已经绰绰有余。
当前的性能提升主要是通过调整索引完成的,创建了11个索引。同时还做了调整数据库参数,把optimizer_index_cost_adj参数由20改回默认的100;再有通过hint+sql profile,让sql选择高效执行计划。这些事情DBA可以搞定。但是,如果SQL写的不好,就只能改代码,这个周期可能就比较长了。下面就列举其中的几种情况:
1、部分sql没有使用绑定变量,每秒硬解析次数将近200。这是一个超标很多的数值(硬解析小于10次/秒的系统,基本还算OK)。合理使用绑定变量是OLTP系统的一个最最基本的要求,但是目前还是有很多系统没有使用绑定变量,只能说目前我们国内的很多开发团队的数据库开发水平还很初级。有些公司会要求DBA要有OCP、OCM等资质才能上岗,但是对开发人员却非常宽容,只要功能实现了就算万事大吉,如果经过一些简单的培训,这种不使用绑定变量的低级问题应该不会出现。 这个客户的另一套数据库在某个时段的硬解析接近1000次/秒,CPU使用率接近100%,当DBA告知开发人员这是因为没有使用绑定变量的原因,开发人员居然振振有词的说以前没有出现类似问题,这就是典型的无知者无畏。
不使用绑定变量的一个无奈的解决办法是改数据库参数,将cursor_sharing有默认的exact改成force。这个改动非常不建议,很有可能引起其他性能问题,有很多bug与之相关。
2、这是一个非常低级而且普遍存在的写法问题,对一个日期字段做to_char:where to_char(starttime,'yyyy-mm-dd')='20200110'。这个starttime字段上,已经存在一个单字段索引,还有一个to_char(starttime,'yyyymmdd')函数索引。但是上面的sql写法,如果不改写,就只能再创建一个to_char(starttime,'yyyy-mm-dd')的函数索引。如果开发人员又写出 to_char(starttime,'yyyy/mm/dd')的sql呢?是不是还得再创建一个函数索引?这种sql的存在,说明开发团队还不知道什么是开发规范和SQL审核。
如果你是开发人员,上面两个写法都命中了你(可能有的开发人员仍不知绑定变量写法是个什么概念,建议马上百度一下),那么只能说你的数据库开发水平还有非常巨大的进步空间。
3、select xxseq.nextval from (select 1 from all_objects where rownum<=10) ; 这个sql是借助all_objects视图,生成10个sequence序列值。 这个写法在功能是没问题,但是sql频繁的执行,大量all_objects视图的调用,把这些本来对资源消耗可以忽略不计的sql,变成了top cpu SQL。 建议改成select xxseq.nextval from dual connect by level<=10;
4、select ...... from xxtab where name like '%这是一个字段内容的完整值%'; SQL(简化版)涉及的表比较大,消耗了大量的IO资源。 随便选择几个两个百分号中间的内容到表里按等值查询,都能查得到,说明两个百分号是可以去掉的,同时like 可以改成=。如果业务上线初期表只有几千或是几万条记录,这个sql消耗的资源也很少,但是随着时间的增长,表的数据量达到千万或上亿,你这样一个SQL就能把大部分IO资源都消耗掉。 凡是使用两个百分号模糊查询为主要过滤条件的,多从业务角度多考虑一下,尽量避免在大表上使用。
5、不明所以,照猫画出狗(不是虎)的分页写法(xxtab这个表很大,sql全表扫描,消耗了大量IO资源):
SELECT * FROM
( SELECT A.*, ROWNUM RN
FROM ( SELECT * FROM xxtab) A
) WHERE RN >= 1
and rownum<= 3000 and ((type = 'typename') and (state = 0));
这个分页写法不知道是从哪里学来的,错的离谱。type和state两个字段上的索引根本用不上,只能使用全表扫描的执行计划。正常的写法应该是:
SELECT A.*, ROWNUM RN
FROM
(SELECT * FROM xxtab
where rownum<= 3000 and ((type = 'typename') and (state = 0))
) A; 这种写法大表就能用上索引了,IO消耗也会大幅减少。
6、select ... from xxtab where (:b1 is null or col1=:b1) and (:b2 is null or col2=:b2) ; 这种写法,可能来源于某些搜索需求,两个输入框,不管有没有输入查询条件,都可以使用上面的sql。写法简单了,效率却差了很多:不管b1、b2是否有输入,col1和col2字段上即使有索引,也都只能选择全表扫描。 这个在业务上可以要求必须有一个输入条件,那么sql就可以根据不同的输入,生成不同的内容,比如b1有输入,b2无输入:select .... from xxtab where col1=b1; 两个都有输入,对应sql是select ... from xxtab where col1=:b1 and col2=:b2; 这样才能高效的使用索引。
7、用rownum分页写法取数:
SELECT *
FROM ( SELECT t.*, ROWNUM RN
FROM ( select t1.*,ROWNUM
from ( select *
from p
left join c on c.objectid = p.objectid
left join o on o.objectid = p.objectid
left join e on e.objectid = o.objectid
) t1
) t WHERE ROWNUM < 1500000
) WHERE RN >= 1200000;
这个分页sql的框架完全正确,满分。但是使用场景却是严重不合适的:p表7700万记录(o表也很大),4表left join没有谓词条件,主查询结果集应该也至少是7700万,每次取30万(1500000-1200000)条,要执行250多次,执行计划都是全表扫描,需要两个大表做250多次全表扫描。如果这些sql是串行执行,那么还有一个逻辑问题,就是这些表的数据都是动态变化的,用rownum分页这种方法,一定会造成漏取或重复取数。其实这个sql的最佳做法就是不加分页,一次完成。表扫描只需要一次,而且也不会出现漏取或重复取数的问题。
总结:
SQL优化对大部分系统来说,都能带来非常大的性能提升。很多问题靠更换高级硬件是无法解决的,比如一个大表全表扫描,现有硬件条件执行时间10分钟,更换硬件后执行时间1分钟。而通过优化SQL,建个合适的索引,在不更换硬件的情况下,可能用不到10毫秒就能出结果。
类似的sql写法问题可能还有很多,这次只是挑选了其中一部分top sql做分享。SQL写法灵活多变,性能好的SQL都是符合规范的,性能差的SQL会暴露你的开发水平。