特别说明:html
一、 本文只是面对数据库应用开发的程序员,不适合专业DBA,DBA在数据库性能优化方面须要了解更多的知识;java
二、 本文许多示例及概念是基于Oracle数据库描述,对于其它关系型数据库也能够参考,但许多观点不适合于KV数据库或内存数据库或者是基于SSD技术的数据库;程序员
三、 本文未深刻数据库优化中最核心的执行计划分析技术。算法
读者对像:sql
开发人员:若是你是作数据库开发,那本文的内容很是适合,由于本文是从程序员的角度来谈数据库性能优化。数据库
架构师:若是你已是数据库应用的架构师,那本文的知识你应该清楚90%,不然你多是一个喜欢折腾的架构师。编程
DBA(数据库管理员):大型数据库优化的知识很是复杂,本文只是从程序员的角度来谈性能优化,DBA除了须要了解这些知识外,还须要深刻数据库的内部体系架构来解决问题。浏览器
引言缓存
在网上有不少文章介绍数据库优化知识,可是大部份文章只是对某个一个方面进行说明,而对于咱们程序员来讲这种介绍并不能很好的掌握优化知识,由于不少介绍只是对一些特定的场景优化的,因此反而有时会产生误导或让程序员感受不明白其中的奥妙而对数据库优化感受很神秘。安全
不少程序员老是问如何学习数据库优化,有没有好的教材之类的问题。在书店也看到了许多数据库优化的专业书籍,可是感受更可能是面向DBA或者是PL/SQL开发方面的知识,我的感受不太适合普通程序员。而要想作到数据库优化的高手,不是花几周,几个月就能达到的,这并非由于数据库优化有多高深,而是由于要作好优化一方面须要有很是好的技术功底,对操做系统、存储硬件网络、数据库原理等方面有比较扎实的基础知识,另外一方面是须要花大量时间对特定的数据库进行实践测试与总结。
做为一个程序员,咱们也许不清楚线上正式的服务器硬件配置,咱们不可能像DBA那样专业的对数据库进行各类实践测试与总结,但咱们都应该很是了解咱们SQL的业务逻辑,咱们清楚SQL中访问表及字段的数据状况,咱们其实只关心咱们的SQL是否能尽快返回结果。那程序员如何利用已知的知识进行数据库优化?如何能快速定位SQL性能问题并找到正确的优化方向?
面对这些问题,笔者总结了一些面向程序员的基本优化法则,本文将结合实例来坦述数据库开发的优化知识。
要正确的优化SQL,咱们须要快速定位能性的瓶颈点,也就是说快速找到咱们SQL主要的开销在哪里?而大多数状况性能最慢的设备会是瓶颈点,以下载时网络速度可能会是瓶颈点,本地复制文件时硬盘可能会是瓶颈点,为何这些通常的工做咱们能快速确认瓶颈点呢,由于咱们对这些慢速设备的性能数据有一些基本的认识,如网络带宽是2Mbps,硬盘是每分钟7200转等等。所以,为了快速找到SQL的性能瓶颈点,咱们也须要了解咱们计算机系统的硬件基本性能指标,下图展现的当前主流计算机性能指标数据。
从图上能够看到基本上每种设备都有两个指标:
延时(响应时间):表示硬件的突发处理能力;
带宽(吞吐量):表明硬件持续处理能力。
从上图能够看出,计算机系统硬件性能从高到代依次为:
CPU——Cache(L1-L2-L3)——内存——SSD硬盘——网络——硬盘
因为SSD硬盘还处于快速发展阶段,因此本文的内容不涉及SSD相关应用系统。
根据数据库知识,咱们能够列出每种硬件主要的工做内容:
CPU及内存:缓存数据访问、比较、排序、事务检测、SQL解析、函数或逻辑运算;
网络:结果数据传输、SQL请求、远程数据库访问(dblink);
硬盘:数据访问、数据写入、日志记录、大数据量排序、大表链接。
根据当前计算机硬件的基本性能指标及其在数据库中主要操做内容,能够整理出以下图所示的性能基本优化法则:
这个优化法则概括为5个层次:
一、 减小数据访问(减小磁盘访问)
二、 返回更少数据(减小网络传输或磁盘访问)
三、 减小交互次数(减小网络传输)
四、 减小服务器CPU开销(减小CPU及内存开销)
五、 利用更多资源(增长资源)
因为每一层优化法则都是解决其对应硬件的性能问题,因此带来的性能提高比例也不同。传统数据库系统设计是也是尽量对低速设备提供优化方法,所以针对低速设备问题的可优化手段也更多,优化成本也更低。咱们任何一个SQL的性能优化都应该按这个规则由上到下来诊断问题并提出解决方案,而不该该首先想到的是增长资源解决问题。
如下是每一个优化法则层级对应优化效果及成本经验参考:
优化法则 |
性能提高效果 |
优化成本 |
减小数据访问 |
1~1000 |
低 |
返回更少数据 |
1~100 |
低 |
减小交互次数 |
1~20 |
低 |
减小服务器CPU开销 |
1~5 |
低 |
利用更多资源 |
@~10 |
高 |
接下来,咱们针对5种优化法则列举经常使用的优化手段并结合实例分析。
数据块是数据库中数据在磁盘中存储的最小单位,也是一次IO访问的最小单位,一个数据块一般能够存储多条记录,数据块大小是DBA在建立数据库或表空间时指定,可指定为2K、4K、8K、16K或32K字节。下图是一个Oracle数据库典型的物理结构,一个数据库能够包括多个数据文件,一个数据文件内又包含多个数据块;
ROWID是每条记录在数据库中的惟一标识,经过ROWID能够直接定位记录到对应的文件号及数据块位置。ROWID内容包括文件号、对像号、数据块号、记录槽号,以下图所示:
数据库索引的原理很是简单,但在复杂的表中真正能正确使用索引的人不多,即便是专业的DBA也不必定能彻底作到最优。
索引会大大增长表记录的DML(INSERT,UPDATE,DELETE)开销,正确的索引可让性能提高100,1000倍以上,不合理的索引也可能会让性能降低100倍,所以在一个表中建立什么样的索引须要平衡各类业务需求。
索引常见问题:
索引有哪些种类?
常见的索引有B-TREE索引、位图索引、全文索引,位图索引通常用于数据仓库应用,全文索引因为使用较少,这里不深刻介绍。B-TREE索引包括不少扩展类型,如组合索引、反向索引、函数索引等等,如下是B-TREE索引的简单介绍:
B-TREE索引也称为平衡树索引(Balance Tree),它是一种按字段排好序的树形目录结构,主要用于提高查询性能和惟一约束支持。B-TREE索引的内容包括根节点、分支节点、叶子节点。
叶子节点内容:索引字段内容+表记录ROWID
根节点,分支节点内容:当一个数据块中不能放下全部索引字段数据时,就会造成树形的根节点或分支节点,根节点与分支节点保存了索引树的顺序及各层级间的引用关系。
一个普通的BTREE索引结构示意图以下所示:
若是咱们把一个表的内容认为是一本字典,那索引就至关于字典的目录,以下图所示:
图中是一个字典按部首+笔划数的目录,至关于给字典建了一个按部首+笔划的组合索引。
一个表中能够建多个索引,就如一本字典能够建多个目录同样(按拼音、笔划、部首等等)。
一个索引也能够由多个字段组成,称为组合索引,如上图就是一个按部首+笔划的组合目录。
SQL什么条件会使用索引?
当字段上建有索引时,一般如下状况会使用索引:
INDEX_COLUMN = ?
INDEX_COLUMN > ?
INDEX_COLUMN >= ?
INDEX_COLUMN < ?
INDEX_COLUMN <= ?
INDEX_COLUMN between ? and ?
INDEX_COLUMN in (?,?,...,?)
INDEX_COLUMN like ?||'%'(后导模糊查询)
T1. INDEX_COLUMN=T2. COLUMN1(两个表经过索引字段关联)
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提升性能的条件比较特殊,使用很差反而性能会更差。
|
咱们通常在什么字段上建索引?
这是一个很是复杂的话题,须要对业务及数据充分分析后再能得出结果。主键及外键一般都要有索引,其它须要建索引的字段应知足如下条件:
1、字段出如今查询条件中,而且查询条件可使用索引;
2、语句执行频率高,一天会有几千次以上;
3、经过字段条件可筛选的记录集很小,那数据筛选比例是多少才适合?
这个没有固定值,须要根据表数据量来评估,如下是经验公式,可用于快速评估:
小表(记录数小于10000行的表):筛选比例<10%;
大表:(筛选返回记录数)<(表总记录数*单条记录长度)/10000/16
单条记录长度≈字段平均内容长度之和+字段数*2
如下是一些字段是否须要建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_CONTENT |
如何知道SQL是否使用了正确的索引?
简单SQL能够根据索引使用语法规则判断,复杂的SQL很差办,判断SQL的响应时间是一种策略,可是这会受到数据量、主机负载及缓存等因素的影响,有时数据全在缓存里,可能全表访问的时间比索引访问时间还少。要准确知道索引是否正确使用,须要到数据库中查看SQL真实的执行计划,这个话题比较复杂,详见SQL执行计划专题介绍。
索引对DML(INSERT,UPDATE,DELETE)附加的开销有多少?
这个没有固定的比例,与每一个表记录的大小及索引字段大小密切相关,如下是一个普通表测试数据,仅供参考:
索引对于Insert性能下降56%
索引对于Update性能下降47%
索引对于Delete性能下降29%
所以对于写IO压力比较大的系统,表的索引须要仔细评估必要性,另外索引也会占用必定的存储空间。
有些时候,咱们只是访问表中的几个字段,而且字段内容较少,咱们能够为这几个字段单独创建一个组合索引,这样就能够直接只经过访问索引就能获得数据,通常索引占用的磁盘空间比表小不少,因此这种方式能够大大减小磁盘IO开销。
如:select id,name from company where type='2';
若是这个SQL常用,咱们能够在type,id,name上建立组合索引
create index my_comb_index on company(type,id,name);
有了这个组合索引后,SQL就能够直接经过my_comb_index索引返回数据,不须要访问company表。
仍是拿字典举例:有一个需求,须要查询一本汉语字典中全部汉字的个数,若是咱们的字典没有目录索引,那咱们只能从字典内容里一个一个字计数,最后返回结果。若是咱们有一个拼音目录,那就能够只访问拼音目录的汉字进行计数。若是一本字典有1000页,拼音目录有20页,那咱们的数据访问成本至关于全表访问的50分之一。
切记,性能优化是无止境的,当性能能够知足需求时便可,不要过分优化。在实际数据库中咱们不可能把每一个SQL请求的字段都建在索引里,因此这种只经过索引访问数据的方法通常只用于核心应用,也就是那种对核心表访问量最高且查询字段数据量不多的查询。
SQL执行计划是关系型数据库最核心的技术之一,它表示SQL执行时的数据访问算法。因为业务需求愈来愈复杂,表数据量也愈来愈大,程序员愈来愈懒惰,SQL也须要支持很是复杂的业务逻辑,但SQL的性能还须要提升,所以,优秀的关系型数据库除了须要支持复杂的SQL语法及更多函数外,还须要有一套优秀的算法库来提升SQL性能。
目前ORACLE有SQL执行计划的算法约300种,并且一直在增长,因此SQL执行计划是一个很是复杂的课题,一个普通DBA能掌握50种就很不错了,就算是资深DBA也不可能把每一个执行计划的算法描述清楚。虽然有这么多种算法,但并不表示咱们没法优化执行计划,由于咱们经常使用的SQL执行计划算法也就十几个,若是一个程序员能把这十几个算法搞清楚,那就掌握了80%的SQL执行计划调优知识。
因为篇幅的缘由,SQL执行计划须要专题介绍,在这里就很少说了。
通常数据分页方式有:
将数据从应用服务器所有下载到本地应用程序或浏览器,在应用程序或浏览器内部经过本地代码进行分页处理
优势:编码简单,减小客户端与应用服务器网络交互次数
缺点:首次交互时间长,占用客户端内存
适应场景:客户端与应用服务器网络延时较大,但要求后续操做流畅,如手机GPRS,超远程访问(跨国)等等。
将数据从数据库服务器所有下载到应用服务器,在应用服务器内部再进行数据筛选。如下是一个应用服务器端Java程序分页的示例:
List list=executeQuery(“select * from employee order by id”);
Int count= list.size();
List subList= list.subList(10, 20);
优势:编码简单,只须要一次SQL交互,总数据与分页数据差很少时性能较好。
缺点:总数据量较多时性能较差。
适应场景:数据库系统不支持分页处理,数据量较小而且可控。
采用数据库SQL分页须要两次SQL完成
一个SQL计算总数量
一个SQL返回分页后的数据
优势:性能好
缺点:编码复杂,各类数据库语法不一样,须要两次SQL交互。
oracle数据库通常采用rownum来进行分页,经常使用分页语法有以下两种:
直接经过rownum分页:
select * from (
select a.*,rownum rn from
(select * from product a where company_id=? order by status) a
where rownum<=20)
where rn>10;
数据访问开销=索引IO+索引所有记录结果对应的表数据IO
采用rowid分页语法
优化原理是经过纯索引找出分页记录的ROWID,再经过ROWID回表返回数据,要求内层查询和排序字段全在索引里。
create index myindex on product(company_id,status);
select b.* from (
select * from (
select a.*,rownum rn from
(select rowid rid,status from product a where company_id=? order by status) a
where rownum<=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);
经过去除没必要要的返回字段能够提升性能,例:
调整前:select * from product where company_id=?;
调整后:select id,name from product where company_id=?;
优势:
1、减小数据在网络上传输开销
2、减小服务器数据处理开销
3、减小客户端内存占用
4、字段变动时提早发现问题,减小程序BUG
5、若是访问的全部字段恰好在一个索引里面,则可使用纯索引访问提升性能。
缺点:增长编码工做量
因为会增长一些编码工做量,因此通常需求经过开发规范来要求程序员这么作,不然等项目上线后再整改工做量更大。
若是你的查询表中有大字段或内容较多的字段,如备注信息、文件内容等等,那在查询表时必定要注意这方面的问题,不然可能会带来严重的性能问题。若是表常常要查询而且请求大内容字段的几率很低,咱们能够采用分表处理,将一个大表分拆成两个一对一的关系表,将不经常使用的大内容字段放在一张单独的表中。如一张存储上传文件的表:
T_FILE(ID,FILE_NAME,FILE_SIZE,FILE_TYPE,FILE_CONTENT)
咱们能够分拆成两张一对一的关系表:
T_FILE(ID,FILE_NAME,FILE_SIZE,FILE_TYPE)
T_FILECONTENT(ID, FILE_CONTENT)
经过这种分拆,能够大大提少T_FILE表的单条记录及总大小,这样在查询T_FILE时性能会更好,当须要查询FILE_CONTENT字段内容时再访问T_FILECONTENT表。
数据库访问框架通常都提供了批量提交的接口,jdbc支持batch的提交处理方法,当你一次性要往一个表中插入1000万条数据时,若是采用普通的executeUpdate处理,那么和服务器交互次数为1000万次,按每秒钟能够向数据库服务器提交10000次估算,要完成全部工做须要1000秒。若是采用批量提交模式,1000条提交一次,那么和服务器交互次数为1万次,交互次数大大减小。采用batch操做通常不会减小不少数据库服务器的物理IO,可是会大大减小客户端与服务端的交互次数,从而减小了屡次发起的网络延时开销,同时也会下降数据库的CPU开销。
假设要向一个普通表插入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访问。以上仅是理论计算值,实际状况须要根据具体环境测量。
不少时候咱们须要按一些ID查询数据库记录,咱们能够采用一个ID一个请求发给数据库,以下所示:
for :var in ids[] do begin
select * from mytable where id=:var;
end;
咱们也能够作一个小的优化, 以下所示,用ID INLIST的这种方式写SQL:
select * from mytable where id in(:id1,id2,...,idn);
经过这样处理能够大大减小SQL请求的数量,从而提升性能。那若是有10000个ID,那是否是所有放在一条SQL里处理呢?答案确定是否认的。首先大部份数据库都会有SQL长度和IN里个数的限制,如ORACLE的IN里就不容许超过1000个值。
另外当前数据库通常都是采用基于成本的优化规则,当IN数量达到必定值时有可能改变SQL执行计划,从索引访问变成全表访问,这将使性能急剧变化。随着SQL中IN的里面的值个数增长,SQL的执行计划会更复杂,占用的内存将会变大,这将会增长服务器CPU及内存成本。
评估在IN里面一次放多少个值还须要考虑应用服务器本地内存的开销,有并发访问时要计算本地数据使用周期内的并发上限,不然可能会致使内存溢出。
综合考虑,通常IN里面的值个数超过20个之后性能基本没什么太大变化,也特别说明不要超过100,超事后可能会引发执行计划的不稳定性及增长数据库CPU及内存成本,这个须要专业DBA评估。
当咱们采用select从数据库查询数据时,数据默认并非一条一条返回给客户端的,也不是一次所有返回客户端的,而是根据客户端fetch_size参数处理,每次只返回fetch_size条记录,当客户端游标遍历到尾部时再从服务端取数据,直到最后所有传送完成。因此若是咱们要从服务端一次取大量数据时,能够加大fetch_size,这样能够减小结果数据传输的交互次数及服务器数据准备时间,提升性能。
如下是jdbc测试的代码,采用本地数据库,表缓存在数据库CACHE中,所以没有网络链接及磁盘IO开销,客户端只遍历游标,不作任何处理,这样更能体现fetch参数的影响:
String vsql ="select * from t_employee";
PreparedStatement pstmt = conn.prepareStatement(vsql,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(1000);
ResultSet rs = 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次再取平均值:
fetchsize |
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 |
Oracle jdbc fetchsize默认值为10,由上测试能够看出fetchsize对性能影响仍是比较大的,可是当fetchsize大于100时就基本上没有影响了。fetchsize并不会存在一个最优的固定值,由于总体性能与记录集大小及硬件平台有关。根据测试结果建议当一次性要取大量数据时这个值设置为100左右,不要小于40。注意,fetchsize不能设置太大,若是一次取出的数据大于JVM的内存会致使内存溢出,因此建议不要超过1000,太大了也没什么性能提升,反而可能会增长内存溢出的危险。
注:图中fetchsize在128之后会有一些小的波动,这并非测试偏差,而是因为resultset填充到具体对像时间不一样的缘由,因为resultset已经到本地内存里了,因此估计是因为CPU的L1,L2 Cache命中率变化形成,因为变化不大,因此笔者也未深刻分析缘由。
iBatis的SqlMapping配置文件能够对每一个SQL语句指定fetchsize大小,以下所示:
<select id="getAllProduct" resultMap="HashMap" fetchSize="1000">
select * from employee
</select>
大型数据库通常都支持存储过程,合理的利用存储过程也能够提升系统性能。如你有一个业务须要将A表的数据作一些加工而后更新到B表中,可是又不可能一条SQL完成,这时你须要以下3步操做:
a:将A表数据所有取出到客户端;
b:计算出要更新的数据;
c:将计算结果更新到B表。
若是采用存储过程你能够将整个业务逻辑封装在存储过程里,而后在客户端直接调用存储过程处理,这样能够减小网络交互的成本。
固然,存储过程也并非十全十美,存储过程有如下缺点:
a、不可移植性,每种数据库的内部编程语法都不太相同,当你的系统须要兼容多种数据库时最好不要用存储过程。
b、学习成本高,DBA通常都擅长写存储过程,但并非每一个程序员都能写好存储过程,除非你的团队有较多的开发人员熟悉写存储过程,不然后期系统维护会产生问题。
c、业务逻辑多处存在,采用存储过程后也就意味着你的系统有一些业务逻辑不是在应用程序里处理,这种架构会增长一些系统维护和调试成本。
d、存储过程和经常使用应用程序语言不同,它支持的函数及语法有可能不能知足需求,有些逻辑就只能经过应用程序处理。
e、若是存储过程当中有复杂运算的话,会增长一些数据库服务端的处理成本,对于集中式数据库可能会致使系统可扩展性问题。
f、为了提升性能,数据库会把存储过程代码编译成中间运行代码(相似于java的class文件),因此更像静态语言。当存储过程引用的对像(表、视图等等)结构改变后,存储过程须要从新编译才能生效,在24*7高并发应用场景,通常都是在线变动结构的,因此在变动的瞬间要同时编译存储过程,这可能会致使数据库瞬间压力上升引发故障(Oracle数据库就存在这样的问题)。
我的观点:普通业务逻辑尽可能不要使用存储过程,定时性的ETL任务或报表统计函数能够根据团队资源状况采用存储过程处理。
要经过优化业务逻辑来提升性能是比较困难的,这须要程序员对所访问的数据及业务流程很是清楚。
举一个案例:
某移动公司推出优惠套参,活动对像为VIP会员而且2010年1,2,3月平均话费20元以上的客户。
那咱们的检测逻辑为:
select avg(money) as avg_money from bill where phone_no='13988888888' and date between '201001' and '201003';
select vip_flag from member where phone_no='13988888888';
if avg_money>20 and vip_flag=true then
begin
执行套参();
end;
若是咱们修改业务逻辑为:
select avg(money) as avg_money from bill where phone_no='13988888888' and date between '201001' and '201003';
if avg_money>20 then
begin
select vip_flag from member where phone_no='13988888888';
if vip_flag=true then
begin
执行套参();
end;
end;
经过这样能够减小一些判断vip_flag的开销,平均话费20元如下的用户就不须要再检测是否VIP了。
若是程序员分析业务,VIP会员比例为1%,平均话费20元以上的用户比例为90%,那咱们改为以下:
select vip_flag from member where phone_no='13988888888';
if vip_flag=true then
begin
select avg(money) as avg_money from bill where phone_no='13988888888' and date between '201001' and '201003';
if avg_money>20 then
begin
执行套参();
end;
end;
这样就只有1%的VIP会员才会作检测平均话费,最终大大减小了SQL的交互次数。
以上只是一个简单的示例,实际的业务老是比这复杂得多,因此通常只是高级程序员更容易作出优化的逻辑,可是咱们须要有这样一种成本优化的意识。
如今大部分Java框架都是经过jdbc从数据库取出数据,而后装载到一个list里再处理,list里多是业务Object,也多是hashmap。
因为JVM内存通常都小于4G,因此不可能一次经过sql把大量数据装载到list里。为了完成功能,不少程序员喜欢采用分页的方法处理,如一次从数据库取1000条记录,经过屡次循环搞定,保证不会引发JVM Out of memory问题。
如下是实现此功能的代码示例,t_employee表有10万条记录,设置分页大小为1000:
d1 = Calendar.getInstance().getTime();
vsql = "select count(*) cnt from t_employee";
pstmt = conn.prepareStatement(vsql);
ResultSet rs = pstmt.executeQuery();
Integer cnt = 0;
while (rs.next()) {
cnt = rs.getInt("cnt");
}
Integer lastid=0;
Integer pagesize=1000;
System.out.println("cnt:" + cnt);
String vsql = "select count(*) cnt from t_employee";
PreparedStatement pstmt = conn.prepareStatement(vsql);
ResultSet rs = pstmt.executeQuery();
Integer cnt = 0;
while (rs.next()) {
cnt = rs.getInt("cnt");
}
Integer lastid = 0;
Integer pagesize = 1000;
System.out.println("cnt:" + cnt);
for (int i = 0; i <= cnt / pagesize; i++) {
vsql = "select * from (select * from t_employee where id>? order by id) where rownum<=?";
pstmt = conn.prepareStatement(vsql);
pstmt.setFetchSize(1000);
pstmt.setInt(1, lastid);
pstmt.setInt(2, pagesize);
rs = pstmt.executeQuery();
int col_cnt = rs.getMetaData().getColumnCount();
Object o;
while (rs.next()) {
for (int j = 1; j <= col_cnt; j++) {
o = rs.getObject(j);
}
lastid = rs.getInt("id");
}
rs.close();
pstmt.close();
}
以上代码实际执行时间为6.516秒
不少持久层框架为了尽可能让程序员使用方便,封装了jdbc经过statement执行数据返回到resultset的细节,致使程序员会想采用分页的方式处理问题。实际上若是咱们采用jdbc原始的resultset游标处理记录,在resultset循环读取的过程当中处理记录,这样就能够一次从数据库取出全部记录。显著提升性能。
这里须要注意的是,采用resultset游标处理记录时,应该将游标的打开方式设置为FORWARD_READONLY模式(ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY),不然会把结果缓存在JVM里,形成JVM Out of memory问题。
代码示例:
String vsql ="select * from t_employee";
PreparedStatement pstmt = conn.prepareStatement(vsql,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(100);
ResultSet rs = 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秒
从测试结果能够看出性能提升了1倍多,若是采用分页模式数据库每次还需发生磁盘IO的话那性能能够提升更多。
iBatis等持久层框架考虑到会有这种需求,因此也有相应的解决方案,在iBatis里咱们不能采用queryForList的方法,而应用该采用queryWithRowHandler加回调事件的方式处理,以下所示:
MyRowHandler myrh=new MyRowHandler();
sqlmap.queryWithRowHandler("getAllEmployee", myrh);
class MyRowHandler implements RowHandler {
public void handleRow(Object o) {
//todo something
}
}
iBatis的queryWithRowHandler很好的封装了resultset遍历的事件处理,效果及性能与resultset遍历同样,也不会产生JVM内存溢出。
绑定变量是指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';//使用索引
Oracle的排序算法一直在优化,可是整体时间复杂度约等于nLog(n)。普通OLTP系统排序操做通常都是在内存里进行的,对于数据库来讲是一种CPU的消耗,曾在PC机作过测试,单核普通CPU在1秒钟能够完成100万条记录的全内存排序操做,因此说因为如今CPU的性能加强,对于普通的几十条或上百条记录排序对系统的影响也不会很大。可是当你的记录集增长到上万条以上时,你须要注意是否必定要这么作了,大记录集排序不只增长了CPU开销,并且可能会因为内存不足发生硬盘排序的现象,当发生硬盘排序时性能会急剧降低,这种需求须要与DBA沟通再决定,取决于你的需求和数据,因此只有你本身最清楚,而不要被别人说排序很慢就吓倒。
如下列出了可能会发生排序操做的SQL语法:
Order by
Group by
Distinct
Exists子查询
Not Exists子查询
In子查询
Not In子查询
Union(并集),Union All也是一种并集操做,可是不会发生排序,若是你确认两个数据集不须要执行去除重复数据操做,那请使用Union All 代替Union。
Minus(差集)
Intersect(交集)
Create Index
Merge Join,这是一种两个表链接的内部算法,执行时会把两个表先排序好再链接,应用于两个大表链接的操做。若是你的两个表链接的条件都是等值运算,那能够采用Hash Join来提升性能,由于Hash Join使用Hash 运算来代替排序的操做。具体原理及设置参考SQL执行计划优化专题。
咱们SQL的业务逻辑常常会包含一些比较操做,如a=b,a<b之类的操做,对于这些比较操做数据库都体现得很好,可是若是有如下操做,咱们须要保持警戒:
Like模糊查询,以下所示:
a like ‘%abc%’
Like模糊查询对于数据库来讲不是很擅长,特别是你须要模糊检查的记录有上万条以上时,性能比较糟糕,这种状况通常能够采用专用Search或者采用全文索引方案来提升性能。
不能使用索引定位的大量In List,以下所示:
a in (:1,:2,:3,…,:n) ----n>20
若是这里的a字段不能经过索引比较,那数据库会将字段与in里面的每一个值都进行比较运算,若是记录数有上万以上,会明显感受到SQL的CPU开销加大,这个状况有两种解决方式:
a、 将in列表里面的数据放入一张中间小表,采用两个表Hash Join关联的方式处理;
b、 采用str2varList方法将字段串列表转换一个临时表处理,关于str2varList方法能够在网上直接查询,这里不详细介绍。
以上两种解决方案都须要与中间表Hash Join的方式才能提升性能,若是采用了Nested Loop的链接方式性能会更差。
若是发现咱们的系统IO没问题可是CPU负载很高,就有多是上面的缘由,这种状况不太常见,若是遇到了最好能和DBA沟通并确认准确的缘由。
什么是复杂运算,通常我认为是一秒钟CPU只能作10万次之内的运算。如含小数的对数及指数运算、三角函数、3DES及BASE64数据加密算法等等。
若是有大量这类函数运算,尽可能放在客户端处理,通常CPU每秒中也只能处理1万-10万次这样的函数运算,放在数据库内不利于高并发处理。
多进程并行访问是指在客户端建立多个进程(线程),每一个进程创建一个与数据库的链接,而后同时向数据库提交访问请求。当数据库主机资源有空闲时,咱们能够采用客户端多进程并行访问的方法来提升性能。若是数据库主机已经很忙时,采用多进程并行访问性能不会提升,反而可能会更慢。因此使用这种方式最好与DBA或系统管理员进行沟通后再决定是否采用。
例如:
咱们有10000个产品ID,如今须要根据ID取出产品的详细信息,若是单线程访问,按每一个IO要5ms计算,忽略主机CPU运算及网络传输时间,咱们须要50s才能完成任务。若是采用5个并行访问,每一个进程访问2000个ID,那么10s就有可能完成任务。
那是否是并行数越多越好呢,开1000个并行是否只要50ms就搞定,答案确定是否认的,当并行数超过服务器主机资源的上限时性能就不会再提升,若是再增长反而会增长主机的进程间调度成本和进程冲突机率。
如下是一些如何设置并行数的基本建议:
若是瓶颈在服务器主机,可是主机还有空闲资源,那么最大并行数取主机CPU核数和主机提供数据服务的磁盘数两个参数中的最小值,同时要保证主机有资源作其它任务。
若是瓶颈在客户端处理,可是客户端还有空闲资源,那建议不要增长SQL的并行,而是用一个进程取回数据后在客户端起多个进程处理便可,进程数根据客户端CPU核数计算。
若是瓶颈在客户端网络,那建议作数据压缩或者增长多个客户端,采用map reduce的架构处理。
若是瓶颈在服务器网络,那须要增长服务器的网络带宽或者在服务端将数据压缩后再处理了。
数据库并行处理是指客户端一条SQL的请求,数据库内部自动分解成多个进程并行处理,以下图所示:
并非全部的SQL均可以使用并行处理,通常只有对表或索引进行所有访问时才可使用并行。数据库表默认是不打开并行访问,因此须要指定SQL并行的提示,以下所示:
select /*+parallel(a,4)*/ * from employee;
并行的优势:
使用多进程处理,充分利用数据库主机资源(CPU,IO),提升性能。
并行的缺点:
1、单个会话占用大量资源,影响其它会话,因此只适合在主机负载低时期使用;
2、只能采用直接IO访问,不能利用缓存数据,因此执行前会触发将脏缓存数据写入磁盘操做。
注:
1、并行处理在OLTP类系统中慎用,使用不当会致使一个会话把主机资源所有占用,而正常事务得不到及时响应,因此通常只是用于数据仓库平台。
2、通常对于百万级记录如下的小表采用并行访问性能并不能提升,反而可能会让性能更差。
原文地址:https://www.cnblogs.com/easypass/archive/2010/12/08/1900127.html