本文共 15120 字,大约阅读时间需要 50 分钟。
所有数据库包括Oracle的sql优化都是针对程序员的,而不是针对dba的,第一,尽量防止模糊,明确指出,即用列名代替*,第二,在where语句上下工夫。第三多表查询和子查询,第四尽量使用绑定。
根据计算机硬件的基本性能指标及其在数据库中主要操作内容,可以整理出如下图所示的性能基本优化法则:
这个优化法则归纳为5个层次:
1、减少数据访问(减少磁盘访问) 2、返回更少数据(减少网络传输或磁盘访问) 3、减少交互次数(减少网络传输) 4、减少服务器CPU开销(减少CPU及内存开销) 5、利用更多资源(增加资源) 由于每一层优化法则都是解决其对应硬件的性能问题,所以带来的性能提升比例也不一样。传统数据库系统设计是也是尽可能对低速设备提供优化方法,因此针对低速设备问题的可优化手段也更多,优化成本也更低。我们任何一个SQL的性能优化都应该按这个规则由上到下来诊断问题并提出解决方案,而不应该首先想到的是增加资源解决问题。以下是每个优化法则层级对应优化效果及成本经验参考:
优化法则 | 性能提升效果 | 优化成本 |
减少数据访问 | 1~1000 | 低 |
返回更少数据 | 1~100 | 低 |
减少交互次数 | 1~20 | 低 |
减少服务器CPU开销 | 1~5 | 低 |
利用更多资源 | @~10 | 高 |
二、SQL什么条件不会使用索引?
查询条件 | 不能使用索引原因 |
INDEX_COLUMN <> ? INDEX_COLUMN not in (?,?,...,?) | 不等于操作不能使用索引 |
function(INDEX_COLUMN) = ? INDEX_COLUMN + 1 = ? INDEX_COLUMN || 'a' = ? | 经过普通运算或函数运算后的索引字段不能使用索引 |
INDEX_COLUMN like '%'||? INDEX_COLUMN like '%'||?||'%' | 含前导模糊查询的Like语法不能使用索引 |
INDEX_COLUMN is null | B-TREE索引里不保存字段为NULL值记录,因此IS NULL不能使用索引 |
NUMBER_INDEX_COLUMN='12345' CHAR_INDEX_COLUMN=12345 | Oracle在做数值比较时需要将两边的数据转换成同一种数据类型,如果两边数据类型不同时会对字段 值隐式转换,相当于加了一层函数处理,所以不能使用索引。 |
a.INDEX_COLUMN=a.COLUMN_1 | 给索引查询的值应是已知数据,不能是未知字段值。 |
注:经过函数运算字段的字段要使用可以使用函数索引,这种需求建议与DBA沟通。 有时候我们会使用多个字段的组合索引,如果查询条件中第一个字段不能使用索引,那整个查询也不能使用索引。 如:我们company表建了一个id+name的组合索引,以下SQL是不能使用索引的 Select * from company where name=? Oracle9i后引入了一种index skip scan的索引方式来解决类似的问题,但是通过index skip scan提高性能的条件比较特殊,使用不好反而性能会更差。 |
以下是一些字段是否需要建B-TREE索引的经验分类:
| 字段类型 | 常见字段名 |
需要建索引的 字段 | 主键 | ID,PK |
外键 | PRODUCT_ID,COMPANY_ID,MEMBER_ID,ORDER_ID,TRADE_ID,PAY_ID | |
有对像或身份标识意义字段 | HASH_CODE,USERNAME,IDCARD_NO,EMAIL,TEL_NO,IM_NO | |
索引慎用字段, 需要进行数据 分布及使用场 景详细评估 | 日期 | GMT_CREATE,GMT_MODIFIED |
年月 | YEAR,MONTH | |
状态标志 | PRODUCT_STATUS,ORDER_STATUS,IS_DELETE,VIP_FLAG | |
类型 | ORDER_TYPE,IMAGE_TYPE,GENDER,CURRENCY_TYPE | |
区域 | COUNTRY,PROVINCE,CITY | |
操作人员 | CREATOR,AUDITOR | |
数值 | LEVEL,AMOUNT,SCORE | |
长字符 | ADDRESS,COMPANY_NAME,SUMMARY,SUBJECT | |
不适合建索引 的字段 | 描述备注 | DESCRIPTION,REMARK,MEMO,DETAIL |
大字段 | FILE_CONTENT,EMAIL_CONTEN |
以下是执行计划获取方式:
select SQL_TEXT,SQL_ID,OPTIMIZER_MODE,parsing_schema_name,LAST_ACTIVE_TIME from V$SQLAREA;
select*fromtable(dbms_xplan.display_cursor('gsyvkdr40w01r'));----gsyvkdr40w01r为SQL_ID
五、索引对DML(INSERT,UPDATE,DELETE)附加的开销有多少?
这个没有固定的比例,与每个表记录的大小及索引字段大小密切相关,以下是一个普通表测试数据,仅供参考: 索引对于Insert性能降低56% 索引对于Update性能降低47% 索引对于Delete性能降低29% 因此对于写IO压力比较大的系统,表的索引需要仔细评估必要性,另外索引也会占用一定的存储空间。切记,性能优化是无止境的,当性能可以满足需求时即可,不要过度优化。在实际数据库中我们不可能把每个SQL请求的字段都建在索引里,所以这种只通过索引访问数据的方法一般只用于核心应用,也就是那种对核心表访问量最高且查询字段数据量很少的查询。
推荐使用的SQL执行计划优化工具:Dell SQL Optimizer for Oracle
(1)直接通过rownum分页:
select*from(
select a.*,rownum rn from
(select*from product a where company_id=? orderby status) a
whererownum<=20)
where rn>10;
数据访问开销=索引IO+索引全部记录结果对应的表数据IO (2)采用rowid分页语法优化原理是通过纯索引找出分页记录的ROWID,再通过ROWID回表返回数据,要求内层查询和排序字段全在索引里。
createindex myindex on product(company_id,status);
select b.*from(
select*from(
select a.*,rownum rn from
(selectrowid rid,status from product a where company_id=? orderby status) a
whererownum<=20)
where rn>10) a, product b
where a.rid = b.rowid ;数据访问开销=索引IO+索引分页结果对应的表数据IO
实例: 一个公司产品有1000条记录,要分页取其中20个产品,假设访问公司索引需要50个IO,2条记录需要1个表数据IO。那么按第一种ROWNUM分页写法,需要550(50+1000/2)个IO,按第二种ROWID分页写法,只需要60个IO(50+20/2);
通过这种分拆,可以大大提少T_FILE表的单条记录及总大小,这样在查询T_FILE时性能会更好,当需要查询FILE_CONTENT字段内容时再访问T_FILECONTENT表。
假设要向一个普通表插入1000万数据,每条记录大小为1K字节,表上没有任何索引,客户端与数据库服务器网络是100Mbps,以下是根据现在一般计算机能力估算的各种batch大小性能对比值:
单位:ms | No batch | Batch=10 | Batch=100 | Batch=1000 | Batch=10000 |
服务器事务处理时间 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 |
服务器IO处理时间 | 0.02 | 0.2 | 2 | 20 | 200 |
网络交互发起时间 | 0.1 | 0.1 | 0.1 | 0.1 | 0.1 |
网络数据传输时间 | 0.01 | 0.1 | 1 | 10 | 100 |
小计 | 0.23 | 0.5 | 3.2 | 30.2 | 300.2 |
平均每条记录处理时间 | 0.23 | 0.05 | 0.032 | 0.0302 | 0.03002 |
从上可以看出,Insert操作加大Batch可以对性能提高近8倍性能,一般根据主键的Update或Delete操作也可能提高2-3倍性能,但不如Insert明显,因为Update及Delete操作可能有比较大的开销在物理IO访问。以上仅是理论计算值,实际情况需要根据具体环境测量。另外从测试角度来说,使用Batch也降低了并发性(毕竟Batch属于事务操作)。
很多时候我们需要按一些ID查询数据库记录,我们可以采用一个ID一个请求发给数据库,如下所示:
for:varin ids[] do begin
select*from mytable whereid=:var;
end ; 我们也可以做一个小的优化, 如下所示,用ID INLIST的这种方式写SQL:select*frommytable whereidin(:id1,id2,...,idn);
通过这样处理可以大大减少SQL请求的数量,从而提高性能。那如果有10000个ID,那是不是全部放在一条SQL里处理呢?答案肯定是否定的。首先大部份数据库都会有SQL长度和IN里个数的限制,如ORACLE的IN里就不允许超过1000个值。
另外当前数据库一般都是采用基于成本的优化规则,当IN数量达到一定值时有可能改变SQL执行计划,从索引访问变成全表访问,这将使性能急剧变化。随着SQL中IN的里面的值个数增加,SQL的执行计划会更复杂,占用的内存将会变大,这将会增加服务器CPU及内存成本。 评估在IN里面一次放多少个值还需要考虑应用服务器本地内存的开销,有并发访问时要计算本地数据使用周期内的并发上限,否则可能会导致内存溢出。综合考虑,一般IN里面的值个数超过20个以后性能基本没什么太大变化,也特别说明不要超过100,超过后可能会引起执行计划的不稳定性及增加数据库CPU及内存成本,这个需要专业DBA评估。
以下是jdbc测试的代码,采用本地数据库,表缓存在数据库CACHE中,因此没有网络连接及磁盘IO开销,客户端只遍历游标,不做任何处理,这样更能体现fetch参数的影响:
String vsql ="select * from t_employee";
PreparedStatementpstmt =conn.prepareStatement(vsql,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(1000);
ResultSetrs = pstmt.executeQuery(vsql);
int cnt = rs.getMetaData().getColumnCount();
Object o;
while(rs.next()) {
for(int i =1; i <= cnt; i++) {
o = rs.getObject(i);
}
}
测试示例中的employee表有100000条记录,每条记录平均长度135字节以下是测试结果,对每种fetchsize测试5次再取平均值:
etchsize | elapse_time(s) |
1 | 20.516 |
2 | 11.34 |
4 | 6.894 |
8 | 4.65 |
16 | 3.584 |
32 | 2.865 |
64 | 2.656 |
128 | 2.44 |
256 | 2.765 |
512 | 3.075 |
1024 | 2.862 |
2048 | 2.722 |
4096 | 2.681 |
8192 | 2.715 |
注:图中fetchsize在128以后会有一些小的波动,这并不是测试误差,而是由于resultset填充到具体对像时间不同的原因,由于resultset已经到本地内存里了,所以估计是由于CPU的L1,L2 Cache命中率变化造成,由于变化不大,所以笔者也未深入分析原因。
个人观点:普通业务逻辑尽量不要使用存储过程,定时性的ETL任务或报表统计函数可以根据团队资源情况采用存储过程处理。
举个例子:12306网站的春运购票压力很大,可以通过业务优化来解决部分问题,比如按客流量分散,一方面从时间上分散(允许提前两个月订票,将购买压力分散到不同时间段),另一方面按客运段分布(按省市、客户段分布售票,避免票源集中发售),还可以按需求差异分类处理(比如按T、K、G、D车型分类,一等座、二等座、硬座、卧铺分类等)。
这里需要注意的是,采用resultset游标处理记录时,应该将游标的打开方式设置为FORWARD_READONLY模式(ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY),否则会把结果缓存在JVM里,造成JVM Out of memory问题。以下代码示例:
String vsql ="select * from t_employee";
PreparedStatementpstmt =conn.prepareStatement(vsql,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(100);
ResultSetrs = pstmt.executeQuery(vsql);
int col_cnt = rs.getMetaData().getColumnCount();
Object o;
while(rs.next()) {
for(int j =1; j <= col_cnt; j++) {
o = rs.getObject(j);
}
}
以上代码实际执行时间为3.156秒,而采用分页的方法处理实际需要6.516秒。性能提高了1倍多,如果采用分页模式数据库每次还需发生磁盘IO的话那性能可以提高更多。
绑定变量是指SQL中对变化的值采用变量参数的形式提交,而不是在SQL中直接拼写对应的值。
非绑定变量写法:Select * from employee where id=1234567 绑定变量写法: Select * from employee where id=?Preparestatement.setInt(1,1234567)
Java中Preparestatement就是为处理绑定变量提供的对像,绑定变量有以下优点:
1、防止SQL注入 2、提高SQL可读性 3、提高SQL解析性能,不使用绑定变更我们一般称为硬解析,使用绑定变量我们称为软解析。 第1和第2点很好理解,做编码的人应该都清楚,这里不详细说明。关于第3点,到底能提高多少性能呢,下面举一个例子说明: 假设有这个这样的一个数据库主机: 2个4核CPU 100块磁盘,每个磁盘支持IOPS为160 业务应用的SQL如下: select * from table where pk=? 这个SQL平均4个IO(3个索引IO+1个数据IO) IO缓存命中率75%(索引全在内存中,数据需要访问磁盘) SQL硬解析CPU消耗:1ms (常用经验值)SQL软解析CPU消耗:0.02ms(常用经验值)
假设CPU每核性能是线性增长,访问内存Cache中的IO时间忽略,要求计算系统对如上应用采用硬解析与采用软解析支持的每秒最大并发数:
是否使用绑定变量 | CPU支持最大并发数 | 磁盘IO支持最大并发数 |
不使用 | 2*4*1000=8000 | 100*160=16000 |
使用 | 2*4*1000/0.02=400000 | 100*160=16000 |
从以上计算可以看出,不使用绑定变量的系统当并发达到8000时会在CPU上产生瓶颈,当使用绑定变量的系统当并行达到16000时会在磁盘IO上产生瓶颈。所以如果你的系统CPU有瓶颈时请先检查是否存在大量的硬解析操作。
使用绑定变量为何会提高SQL解析性能,这个需要从数据库SQL执行原理说明,一条SQL在Oracle数据库中的执行过程如下图所示:当一条SQL发送给数据库服务器后,系统首先会将SQL字符串进行hash运算,得到hash值后再从服务器内存里的SQL缓存区中进行检索,如果有相同的SQL字符,并且确认是同一逻辑的SQL语句,则从共享池缓存中取出SQL对应的执行计划,根据执行计划读取数据并返回结果给客户端。
如果在共享池中未发现相同的SQL则根据SQL逻辑生成一条新的执行计划并保存在SQL缓存区中,然后根据执行计划读取数据并返回结果给客户端。
为了更快的检索SQL是否在缓存区中,首先进行的是SQL字符串hash值对比,如果未找到则认为没有缓存,如果存在再进行下一步的准确对比,所以要命中SQL缓存区应保证SQL字符是完全一致,中间有大小写或空格都会认为是不同的SQL。
如果我们不采用绑定变量,采用字符串拼接的模式生成SQL,那么每条SQL都会产生执行计划,这样会导致共享池耗尽,缓存命中率也很低。
一些无需使用绑定变量的场景:
a、数据仓库应用,这种应用一般并发不高,但是每个SQL执行时间很长,SQL解析的时间相比SQL执行时间比较小,绑定变量对性能提高不明显。数据仓库一般都是内部分析应用,所以也不太会发生SQL注入的安全问题。 b、数据分布不均匀的特殊逻辑,如产品表,记录有1亿,有一产品状态字段,上面建有索引,有审核中,审核通过,审核未通过3种状态,其中审核通过9500万,审核中1万,审核不通过499万。 要做这样一个查询: select count(*) from product where status=? 采用绑定变量的话,那么只会有一个执行计划,如果走索引访问,那么对于审核中查询很快,对审核通过和审核不通过会很慢;如果不走索引,那么对于审核中与审核通过和审核不通过时间基本一样; 对于这种情况应该不使用绑定变量,而直接采用字符拼接的方式生成SQL,这样可以为每个SQL生成不同的执行计划,如下所示。 select count(*) from product where status='approved'; //不使用索引(审核通过) select count(*) from product where status='tbd'; //不使用索引(审核未通过)select count(*) from product where status='auditing';//使用索引(审核中)
Merge Join,这是一种两个表连接的内部算法,执行时会把两个表先排序好再连接,应用于两个大表连接的操作。如果你的两个表连接的条件都是等值运算,那可以采用Hash Join来提高性能,因为Hash Join使用Hash 运算来代替排序的操作。具体原理及设置参考SQL执行计划优化专题。
如果发现我们的系统IO没问题但是CPU负载很高,就有可能是上面的原因,这种情况不太常见,如果遇到了最好能和DBA沟通并确认准确的原因。
如果有大量这类函数运算,尽量放在客户端处理,一般CPU每秒中也只能处理1万-10万次这样的函数运算,放在数据库内不利于高并发处理。
如果瓶颈在服务器网络,那需要增加服务器的网络带宽或者在服务端将数据压缩后再处理了。
数据库并行处理是指客户端一条SQL的请求,数据库内部自动分解成多个进程并行处理,如下图所示:
并不是所有的SQL都可以使用并行处理,一般只有对表或索引进行全部访问时才可以使用并行。数据库表默认是不打开并行访问,所以需要指定SQL并行的提示,如下所示:
select /*+parallel(a,4)*/ * from employee; 并行的优点: 使用多进程处理,充分利用数据库主机资源(CPU,IO),提高性能。 并行的缺点: 1、单个会话占用大量资源,影响其它会话,所以只适合在主机负载低时期使用; 2、只能采用直接IO访问,不能利用缓存数据,所以执行前会触发将脏缓存数据写入磁盘操作。 注: 1、并行处理在OLTP类系统中慎用,使用不当会导致一个会话把主机资源全部占用,而正常事务得不到及时响应,所以一般只是用于数据仓库平台或大数据处理平台。 2、一般对于百万级记录以下的小表采用并行访问性能并不能提高,反而可能会让性能更差。