承接国内外服务器租用托管、定制开发、网站代运营、网站seo优化托管接单、网站代更新,新老站点皆可!!咨询QQ:3787320601
当前位置:首页  >  互联网圈  >  数据库访问性能优化

数据库访问性能优化

管理员 2023-07-21 08:05:07 互联网圈 0 ℃ 0 评论 44565字 收藏

在网上有很多文章介绍数据库优化知识,但是大部分文章只是对某个一个方面进行说明,而对我们程序员来讲这类介绍其实不能很好的掌握优化知识,由于很多介绍只是对一些特定的场景优化的,所以反而有时会产生误导或让程序员感觉不明白其中的奥妙而对数据库优化感觉很神秘。
很多程序员总是问如何学习数据库优化,有无好的教材之类的问题。在书店也看到了许多数据库优化的专业书籍,但是感觉更多是面向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个层次
1、 减少数据访问(减少磁盘访问)
2、 返回更少数据(减少网络传输或磁盘访问)
3、 减少交互次数(减少网络传输)
4、 减少服务器CPU开消(减少CPU及内存开消)
5、 利用更多资源(增加资源)
由于每层优化法则都是解决其对应硬件的性能问题,所以带来的性能提升比例也不一样。传统数据库系统设计是也是尽量对低速装备提供优化方法,因此针对低速装备问题的可优化手段也更多,优化本钱也更低。我们任何一个SQL的性能优化都应当按这个规则由上到下来诊断问题并提出解决方案,而不应当首先想到的是增加资源解决问题。
以下是每一个优化法则层级对应优化效果及本钱经验参考:

接下来,我们针对5种优化法则罗列经常使用的优化手段并结合实例分析。

二、Oracle数据库两个基本概念
数据块(Block)
数据块是数据库中数据在磁盘中存储的最小单位,也是一次IO访问的最小单位,一个数据块通常可以存储多条记录,数据块大小是DBA在创建数据库或表空间时指定,可指定为2K、4K、8K、16K或32K字节。下图是一个Oracle数据库典型的物理结构,一个数据库可以包括多个数据文件,一个数据文件内又包括多个数据块;

ROWID
ROWID是每条记录在数据库中的唯一标识,通过ROWID可以直接定位记录到对应的文件号及数据块位置。ROWID内容包括文件号、对像号、数据块号、记录槽号,以下图所示:

三、数据库访问优化法则详解
1、减少数据访问
1.1、创建并使用正确的索引
数据库索引的原理非常简单,但在复杂的表中真正能正确使用索引的人很少,即便是专业的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甚么条件不会使用索引?

我们一般在甚么字段上建索引?
这是一个非常复杂的话题,需要对业务及数据充分分析后再能得出结果。主键及外键通常都要有索引,其它需要建索引的字段应满足以下条件:
1、字段出现在查询条件中,并且查询条件可使用索引;
2、语句履行频率高,一天会有几千次以上;
3、通过字段条件可挑选的记录集很小,那数据挑选比例是多少才合适?
这个没有固定值,需要根据表数据量来评估,以下是经验公式,可用于快速评估:
小表(记录数小于10000行的表):挑选比例<10%;
大表:(挑选返回记录数)<(表总记录数*单条记录长度)/10000/16
单条记录长度≈字段平均内容长度之和+字段数*2
以下是一些字段会不会需要建B-TREE索引的经验分类:

如何知道SQL会不会使用了正确的索引?
简单SQL可以根据索引使用语法规则判断,复杂的SQL不好办,判断SQL的响应时间是一种策略,但是这会遭到数据量、主机负载及缓存等因素的影响,有时数据全在缓存里,可能全表访问的时间比索引访问时间还少。要准确知道索引会不会正确使用,需要到数据库中查看SQL真实的履行计划,这个话题比较复杂,详见SQL履行计划专题介绍。

索引对DML(INSERT,UPDATE,DELETE)附加的开消有多少?
这个没有固定的比例,与每一个表记录的大小及索引字段大小密切相关,以下是一个普通表测试数据,仅供参考:
索引对Insert性能下降56%
索引对Update性能下降47%
索引对Delete性能下降29%
因此对写IO压力比较大的系统,表的索引需要仔细评估必要性,另外索引也会占用一定的存储空间。

1.2、只通过索引访问数据
有些时候,我们只是访问表中的几个字段,并且字段内容较少,我们可以为这几个字段单独建立一个组合索引,这样就能够直接只通过访问索引就可以得到数据,一般索引占用的磁盘空间比表小很多,所以这类方式可以大大减少磁盘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要求的字段都建在索引里,所以这类只通过索引访问数据的方法一般只用于核心利用,也就是那种对核心表访问量最高且查询字段数据量很少的查询。
1.3、优化SQL履行计划
SQL履行计划是关系型数据库最核心的技术之一,它表示SQL履行时的数据访问算法。由于业务需求愈来愈复杂,表数据量也愈来愈大,程序员愈来愈怠惰,SQL也需要支持非常复杂的业务逻辑,但SQL的性能还需要提高,因此,优秀的关系型数据库除需要支持复杂的SQL语法及更多函数外,还需要有一套优秀的算法库来提高SQL性能。
目前ORACLE有SQL履行计划的算法约300种,而且一直在增加,所以SQL履行计划是一个非常复杂的课题,一个普通DBA能掌握50种就很不错了,就算是资深DBA也不可能把每一个履行计划的算法描写清楚。虽然有这么多种算法,但其实不表示我们没法优化履行计划,由于我们经常使用的SQL履行计划算法也就十几个,如果一个程序员能把这十几个算法弄清楚,那就掌握了80%的SQL履行计划调优知识。
由于篇幅的缘由,SQL履行计划需要专题介绍,在这里就不多说了。

2、返回更少的数据
2.1、数据分页处理
一般数据分页方式有:
2.1.1、客户端(利用程序或浏览器)分页
将数据从利用服务器全部下载到本地利用程序或浏览器,在利用程序或浏览器内部通过本地代码进行分页处理
优点:编码简单,减少客户端与利用服务器网络交互次数
缺点:首次交互时间长,占用客户端内存
适应场景:客户端与利用服务器网络延时较大,但要求后续操作流畅,如手机GPRS,超远程访问(跨国)等等。
2.1.2、利用服务器分页
将数据从数据库服务器全部下载到利用服务器,在利用服务器内部再进行数据挑选。以下是一个利用服务器端Java程序分页的示例:
List list=executeQuery(“select * from employee order by id”);
Int count= list.size();
List subList= list.subList(10, 20);

优点:编码简单,只需要一次SQL交互,总数据与分页数据差不多时性能较好。
缺点:总数据量较多时性能较差。
适应场景:数据库系统不支持分页处理,数据量较小并且可控。

2.1.3、数据库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);

2.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表。

3、减少交互次数
3.1、batch DML
数据库访问框架一般都提供了批量提交的接口,jdbc支持batch的提交处理方法,当你一次性要往一个表中插入1000万条数据时,如果采取普通的executeUpdate处理,那末和服务器交互次数为1000万次,按每秒钟可以向数据库服务器提交10000次估算,要完成所有工作需要1000秒。如果采取批量提交模式,1000条提交一次,那末和服务器交互次数为1万次,交互次数大大减少。采取batch操作一般不会减少很多数据库服务器的物理IO,但是会大大减少客户端与服务真个交互次数,从而减少了屡次发起的网络延时开消,同时也会下降数据库的CPU开消。

假定要向一个普通表插入1000万数据,每条记录大小为1K字节,表上没有任何索引,客户端与数据库服务器网络是100Mbps,以下是根据现在一般计算性能力估算的各种batch大小性能对照值:

从上可以看出,Insert操作加大Batch可以对性能提高近8倍性能,一般根据主键的Update或Delete操作也可能提高2⑶倍性能,但不如Insert明显,由于Update及Delete操作可能有比较大的开消在物理IO访问。以上仅是理论计算值,实际情况需要根据具体环境丈量。

3.2、In List
很多时候我们需要按一些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评估。

3.3、设置Fetch Size
当我们采取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次再取平均值:

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>

3.4、使用存储进程
大型数据库一般都支持存储进程,公道的利用存储进程也能够提高系统性能。如你有一个业务需要将A表的数据做一些加工然后更新到B表中,但是又不可能一条SQL完成,这时候你需要以下3步操作:
a:将A表数据全部取出到客户端;
b:计算出要更新的数据;
c:将计算结果更新到B表。

如果采取存储进程你可以将全部业务逻辑封装在存储进程里,然后在客户端直接调用存储进程处理,这样可以减少网络交互的本钱。
固然,存储进程也其实不是十全十美,存储进程有以下缺点:
a、不可移植性,每种数据库的内部编程语法都不太相同,当你的系统需要兼容多种数据库时最好不要用存储进程。
b、学习本钱高,DBA一般都善于写存储进程,但其实不是每一个程序员都能写好存储进程,除非你的团队有较多的开发人员熟习写存储进程,否则后期系统保护会产生问题。
c、业务逻辑多处存在,采取存储进程后也就意味着你的系统有一些业务逻辑不是在利用程序里处理,这类架构会增加一些系统保护和调试本钱。
d、存储进程和经常使用利用程序语言不一样,它支持的函数及语法有可能不能满足需求,有些逻辑就只能通过利用程序处理。
e、如果存储进程中有复杂运算的话,会增加一些数据库服务真个处理本钱,对集中式数据库可能会致使系统可扩大性问题。
f、为了提高性能,数据库会把存储进程代码编译成中间运行代码(类似于java的class文件),所以更像静态语言。当存储进程援用的对像(表、视图等等)结构改变后,存储进程需要重新编译才能生效,在24*7高并发利用场景,一般都是在线变更结构的,所以在变更的瞬间要同时编译存储进程,这可能会致使数据库瞬间压力上升引发故障(Oracle数据库就存在这样的问题)。

个人观点:普通业务逻辑尽可能不要使用存储进程,定时性的ETL任务或报表统计函数可以根据团队资源情况采取存储进程处理。

3.5、优化业务逻辑
要通过优化业务逻辑来提高性能是比较困难的,这需要程序员对所访问的数据及业务流程非常清楚。
举一个案例:
某移动公司推出优惠套参,活动对像为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的交互次数。

以上只是一个简单的示例,实际的业务总是比这复杂很多,所以一般只是高级程序员更容易做出优化的逻辑,但是我们需要有这样一种本钱优化的意识。

3.6、使用ResultSet游标处理记录
现在大部份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内存溢出。

4、减少数据库服务器CPU运算
4.1、使用绑定变量
绑定变量是指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时间疏忽,要求计算系统对如上利用采取硬解析与采取软解析支持的每秒最大并发数:

从以上计算可以看出,不使用绑定变量的系统当并发到达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’;//使用索引

4.2、公道使用排序
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履行计划优化专题。

4.3、减少比较操作
我们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沟通并确认准确的缘由。

4.4、大量复杂运算在客户端处理
甚么是复杂运算,一般我认为是一秒钟CPU只能做10万次之内的运算。如含小数的对数及指数运算、三角函数、3DES及BASE64数据加密算法等等。
如果有大量这类函数运算,尽可能放在客户端处理,一般CPU每秒中也只能处理1万⑴0万次这样的函数运算,放在数据库内不利于高并发处理。

5、利用更多的资源
5.1、客户端多进程并行访问
多进程并行访问是指在客户端创建多个进程(线程),每一个进程建立一个与数据库的连接,然后同时向数据库提交访问要求。当数据库主机资源有空闲时,我们可以采取客户端多进程并行访问的方法来提高性能。如果数据库主机已很忙时,采取多进程并行访问性能不会提高,反而可能会更慢。所以使用这类方式最好与DBA或系统管理员进行沟通后再决定会不会采取。

例如:
我们有10000个产品ID,现在需要根据ID取生产品的详细信息,如果单线程访问,按每一个IO要5ms计算,疏忽主机CPU运算及网络传输时间,我们需要50s才能完成任务。如果采取5个并行访问,每一个进程访问2000个ID,那末10s就有可能完成任务。
那是不是是并行数越多越好呢,开1000个并行会不会只要50ms就弄定,答案肯定会不会定的,当并行数超过服务器主机资源的上限时性能就不会再提高,如果再增加反而会增加主机的进程间调度本钱和进程冲突机率。

以下是一些怎样设置并行数的基本建议:
如果瓶颈在服务器主机,但是主机还有空闲资源,那末最大并行数取主机CPU核数和主机提供数据服务的磁盘数两个参数中的最小值,同时要保证主机有资源做其它任务。
如果瓶颈在客户端处理,但是客户端还有空闲资源,那建议不要增加SQL的并行,而是用一个进程取回数据后在客户端起多个进程处理便可,进程数根据客户端CPU核数计算。
如果瓶颈在客户端网络,那建议做数据紧缩或增加多个客户端,采取map reduce的架构处理。
如果瓶颈在服务器网络,那需要增加服务器的网络带宽或在服务端将数据紧缩后再处理了。

5.2、数据库并行处理
数据库并行处理是指客户端一条SQL的要求,数据库内部自动分解成多个进程并行处理,以下图所示:

其实不是所有的SQL都可使用并行处理,一般只有对表或索引进行全部访问时才可使用并行。数据库表默许是不打开并行访问,所以需要指定SQL并行的提示,以下所示:
select /*+parallel(a,4)*/ * from employee;

并行的优点:
使用多进程处理,充分利用数据库主机资源(CPU,IO),提高性能。
并行的缺点:
1、单个会话占用大量资源,影响其它会话,所以只合适在主机负载低时期使用;
2、只能采取直接IO访问,不能利用缓存数据,所以履行前会触发将脏缓存数据写入磁盘操作。

注:
1、并行处理在OLTP类系统中慎用,使用不当会致使一个会话把主机资源全部占用,而正常事务得不到及时响应,所以一般只是用于数据仓库平台。
2、一般对百万级记录以下的小表采取并行访问性能其实不能提高,反而可能会让性能更差。

文章来源:丸子建站

文章标题:数据库访问性能优化

https://www.wanzijz.com/view/66136.html

相关文章

Related articles

X

截屏,微信识别二维码

微信号:weimawl

(点击微信号复制,添加好友)

打开微信