数据库访问性能优化(三)

3.4、使用存储过程

大型数据库通常都支持存储过程,合理的利用存储过程也能够提升系统性能。如你有一个业务须要将A表的数据作一些加工而后更新到B表中,可是又不可能一条SQL完成,这时你须要以下3步操做:java

a:将A表数据所有取出到客户端;程序员

b:计算出要更新的数据;sql

c:将计算结果更新到B表。数据库

 

若是采用存储过程你能够将整个业务逻辑封装在存储过程里,而后在客户端直接调用存储过程处理,这样能够减小网络交互的成本。编程

固然,存储过程也并非十全十美,存储过程有如下缺点:缓存

a、不可移植性,每种数据库的内部编程语法都不太相同,当你的系统须要兼容多种数据库时最好不要用存储过程。安全

b、学习成本高,DBA通常都擅长写存储过程,但并非每一个程序员都能写好存储过程,除非你的团队有较多的开发人员熟悉写存储过程,不然后期系统维护会产生问题。服务器

c、业务逻辑多处存在,采用存储过程后也就意味着你的系统有一些业务逻辑不是在应用程序里处理,这种架构会增长一些系统维护和调试成本。网络

d、存储过程和经常使用应用程序语言不同,它支持的函数及语法有可能不能知足需求,有些逻辑就只能经过应用程序处理。架构

e、若是存储过程当中有复杂运算的话,会增长一些数据库服务端的处理成本,对于集中式数据库可能会致使系统可扩展性问题。

f、为了提升性能,数据库会把存储过程代码编译成中间运行代码(相似于javaclass文件),因此更像静态语言。当存储过程引用的对像(表、视图等等)结构改变后,存储过程须要从新编译才能生效,在24*7高并发应用场景,通常都是在线变动结构的,因此在变动的瞬间要同时编译存储过程,这可能会致使数据库瞬间压力上升引发故障(Oracle数据库就存在这样的问题)

 

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

 

3.5、优化业务逻辑

要经过优化业务逻辑来提升性能是比较困难的,这须要程序员对所访问的数据及业务流程很是清楚。

举一个案例:

某移动公司推出优惠套参,活动对像为VIP会员而且2010123月平均话费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

    }

}

 

iBatisqueryWithRowHandler很好的封装了resultset遍历的事件处理,效果及性能与resultset遍历同样,也不会产生JVM内存溢出。

 

当一条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';//使用索引

相关文章
相关标签/搜索