- 浏览: 62325 次
文章分类
最新评论
mybatis SQL性能调优
MybatisSQL性能调优
1.Mapper层参数为Map,由Service层负责重载
Mapper由于机制的问题,不能重载,参数一般设置成Map,但这样会使参数变得模糊,如果想要使代码变得清晰,可以通过service层来实现重载的目的,对外提供的Service层是重载的,但这些重载的Service方法其实是调同一个Mapper,只不过相应的参数并不一致。
也许有人会想,为什么不在Service层也设置成Map呢?我个人是不推荐这么做的,虽然为了方便,我在之前的项目中也大量采用了这种方式,但很明显会给日后的维护工作带来麻烦。因为这么做会使你整个MVC都依赖于Map模型,这个模型其实是很不错的,方便搭框架,但存在一个问题:仅仅看方法签名,你不清楚Map中所拥有的参数个数、类型、每个参数代表的含义。
试想,你只对Service层变更,或者DAO层变更,你需要清楚整个流程中Map传递过来的参数,除非注释或者文档良好,否则必须把每一层的代码都了解清楚,你才知道传递了哪些参数。针对于简单MVC,那倒也还好,但如果层次复杂之后,代码会变得异常复杂,而且如果我增加一个参数,需要把每一个层的注释都添加上。相对于注释,使用方法签名来保证这种代码可控性会来得更可行一些,因为注释有可能是过时的,但方法签名一般不太可能是陈旧的。
2.尽量少用ifchoose等语句
Mybatis的配置SQL时,尽量少用ifchoose等标签,能用SQL实现判断的尽量用SQL来判断(CASEWHEN,DECODE等),以便后期维护。否则,一旦SQL膨胀,超级恶心,如果需要调试Mybatis中的SQL,需要去除大量的判断语句,非常麻烦。另一方面,大量的if判断,会使生成的SQL中包含大量的空格,增加网络传输的时间,也不可取。
而且大量的ifchoose语句,不可避免地,每次生成的SQL会不太一致,会导致ORACLE大量的硬解析,也不可取。
我们来看看这样的SQL:
SELECT*FROMT_NEWS_TEXTWHERE1=1
<choose>
<iftest="startdate!=nullandstartdate!=''andenddate!=nullandendate!=''">
ANDPUBLISHTIME>=#{startdate}ANDPUBLISHTIME<=#{enddate}
</if>
<otherwise>
ANDPUBLISHTIME>=SYSDATE-7ANDPUBLISHTIME<=SYSDATE
</otherwise></choose>
这样的if判断,其实是完全没有必要的,我们可以很简单的采用DECODE来解决默认值问题:
SELECT*FROMT_NEWS_TEXTWHEREPUBLISHTIME>=DECODE(#{startdate},NULL,SYSDATE-7,#{startdate})ANDPUBLISHTIME<=DECODE(#{enddate},NULL,SYSDATE,#{enddate})
当然有人会想,引入CASEWHEN,DECODE会导致需要ORACLE函数解析,会拖慢SQL执行时间,有兴趣的同学可以回去做一下测试,看看是否会有大的影响。就个人经验而言,在我的开发过程,没有发现因为函数解析导致SQL变慢的情形。影响SQL执行效率的一般情况下是JOIN、ORDERBY、DISTINCT、PARTITATIONBY等这些操作,这些操作一般与表结构设计有很大的关联。相对于这些的效率影响程度,函数解析对于SQL执行速度影响应该是可以忽略不计的。
另外一点,对于一些默认值的赋值,像上面那条SQL,默认成当前日期什么的,其实可以完全提到Service层或Controller层做处理,在Mybatis中应该要少用这些判断。因为,这样的话,很难做缓存处理。如果startdate为空,在SQL上使用动态的SYSDATE,就无法确定缓存startdate日期的key应该是什么了。所以参数最好在传递至Mybatis之前都处理好,这样Mybatis层也能减少部分ifchoose语句,同时也方便做缓存处理。
当然不使用ifchoose也并不是绝对的,有时候为了优化SQL,不得不使用if来解决,比如说LIKE语句,当然一般不推荐使用LIKE,但如果存在使用的场景,尽可能在不需要使用时候去除LIKE,比如查询文章标题,以提高查询效率。最好的方式是使用lucence等搜索引擎来解决这种全文索引的问题。
总的来说,if与choose判断分支是不可能完全去除的,但是推荐使用SQL原生的方式来解决一些动态问题,而不应该完全依赖Mybatis来完成动态分支的判断,因为判断分支过于复杂,而且难以维护。
3.用XML注释取代SQL注释
Mybatis中原SQL的注释尽量不要保留,注释会引发一些问题,如果需要使用注释,可以在XML中用<!---->来注释,保证在生成的SQL中不会存在SQL注释,从而降低问题出现的可能性。这样做还有一个好处,就是在IDE中可以很清楚的区分注释与SQL。
现在来谈谈注释引发的问题,我做的一个项目中,分页组件是基于Mybatis的,它会在你写的SQL脚本外面再套一层SELECTCOUNT(*)ROWNUM_FROM(....)计算总记录数,同时有另一个嵌套SELECT*FROM(...)WHEREROWNUM>10ANDRONNUM<10*2这种方式生成分页信息,如果你的脚本中最后一行出现了注释,则添加的部分会成为注释的一部分,执行就会报错。除此之外,某些情况下也可能导致部分条件被忽略,如下面的情况:
SELECT*FROMTESTWHERECOL1>1--这里是注释<iftest="a!=nullanda!=''">ANDCOL2=#{a}</if>
即使传入的参数中存在对应的参数,实际也不会产生效果,因为后面的内容实际上是被完全注释了。这种错误,如果不经过严格的测试,是很难发现的。一般情况下,XML注释完全可以替代SQL注释,因此这种行为应该可以禁止掉。
4.尽可能使用#{},而不是${}
Mybatis中尽量不要使用${},尽量这样做很方便开发,但是有一个问题,就是大量使用会导致ORACLE的硬解析,拖慢数据库性能,运行越久,数据库性能会越差。
select*fromT_PRINT_LAYOUTwhereD_RECID=${recId}
最后生成的SQL为:
select*fromT_PRINT_LAYOUTwhereD_RECID=1
即:直接将参数值替换到了原来${recId}的位置,相当于硬拼SQL
select*fromT_PRINT_LAYOUTwhereD_RECID=#{recid,jdbcType=DECIMAL}
最后生成的SQL为:
select*fromT_PRINT_LAYOUTwhereD_RECID=?
即:#{...}被识别为一个SQL参数
关于${},另一个误用的地方就是LIKE,我这边还有个案例:比如一些树型菜单,节点会设计成'01','0101',用两位节点来区分层级,这时候,如果需要查询01节点下所有的节点,最简单的SQL便是:SELECT*FROMTREEWHEREIDLIKE'01%',这种SQL其实无可厚非,因为它也能用到索引,所以不需要特别的处理,直接使用就行了。但如果是文章标题,则需要额外注意了:SELECT*FROMT_NEWS_TEXTWHERETITLELIKE'%OSC%',这是怎么也不会用到索引的,上面说了,最好采用全文检索。但如果离不开LIKE,就需要注意使用的方式:IDLIKE#{ID}||'%'而不是IDLIKE'${ID}%',减少硬解析的可能。
有人觉得使用||会增加ORACLE处理的时间,我觉得不要把ORACLE看得太傻,虽然有时候确实非常傻,有空可以再总结ORACLE傻不垃圾的地方,但是稍加测试便知:这种串联方式,对于整个SQL的解析执行,应该是微乎其微的。
当然还有一些特殊情况是没有办法处理的,比如说动态注入列名、表名等。对于这些情况,则比较棘手,没有找到比较方便的手段。由于这种情况出现的可能性会比较少,所以使用${}倒也不至于有什么太大的影响。当然你如果有代码洁癖的话,可以使用ORACLE的动态执行SQL的机制Executeimmediate,这样就可以完全避免${}出现的可能性了。这样会引入比较复杂的模型,这个时候,你就需要取舍了。
针对于以上动态SQL所导致的问题,最激进的方式是全部采用存储过程,用数据库原生的方式来解决,方便开发调试,当然也会带来问题:对开发人员会有更高的要求、存储过程的管理等等,我这边项目没有采用过这种方式,这里不做更多的展开。
5.大量数据的批量处理(变量绑定)
大量数据(条数>10000)做insert时,如果按常规方式,每条insertintotable(...)values(...);来提交,速度巨慢。改善性能的思路是多条insert批量提交。
oracle环境中,有一种批量insert的小技巧,原理是insertinto...selectfrom...,套在mybatis上,变形为:
INSERTINTOT_TEST
(ID,COL_A,COL_B)
SELECTSEQ_TEST.NEXTVAL,A.*
FROM(
SELECT'A1','B1'FROMDUAL
UNIONALLSELECT'A2','B2'FROMDUAL
UNIONALLSELECT'A3','B3'FROMDUAL
UNIONALLSELECT'A4','B4'FROMDUAL
UNIONALLSELECT'A5','B5'FROMDUAL
UNIONALLSELECT'A6','B6'FROMDUAL
)A
中间的部分非常有规律,可以用foreach标签生成,参考下面的片段
<insertid="insertBatch2"parameterType="ctas.entity.SharkFlt">
<selectKeykeyProperty="recId"order="BEFORE"resultType="Long">
selectSEQ_CTAS_SHARK_FLT.nextvalasrecIdfromdual
</selectKey>
insertintoCTAS_SHARK_FLT(<includerefid="Base_Column_List"/>)SELECTSEQ_TEST.NEXTVAL,A.*
FROM(
<foreachcollection="list"item="item"index="index"open=""close=""separator="unionall">
select#{item.awbType,jdbcType=VARCHAR},#{item.awbPre,jdbcType=VARCHAR},...fromdual
</foreach>
)A
</insert>
即使这样,也不能直接run,oracle中一次执行的sql语句长度是有限制的,如果最后拼出来的sql字符串过长,会导致执行失败,所以java端还要做一个分段处理,参考下面的处理:
List<SharkFlt>data=newArrayList<SharkFlt>();
for(TSharkFltf:sharkFlts){
data.add(getSharkFlt(f));
}
System.out.println(data.size());
longbeginTime=System.currentTimeMillis();
System.out.println("开始插入...");
SqlSessionFactorysqlSessionFactory=ctx.getBean(SqlSessionFactory.class);
SqlSessionsession=null;
try{
session=sqlSessionFactory.openSession(ExecutorType.BATCH,false);
inta=2000;//每次提交2000条
intloop=(int)Math.ceil(data.size()/(double)a);
List<SharkFlt>tempList=newArrayList<SharkFlt>(a);
intstart,stop;
for(inti=0;i<loop;i++){
tempList.clear();
start=i*a;
stop=Math.min(i*a+a-1,data.size()-1);
System.out.println("range:"+start+"-"+stop);
for(intj=start;j<=stop;j++){
tempList.add(data.get(j));
}
session.insert("ctas.importer.writer.mybatis.mappper.SharkFltMapper.insertBatch2",tempList);
session.commit();
session.clearCache();
System.out.println("已经插入"+(stop+1)+"条");
}
}catch(Exceptione){
e.printStackTrace();
session.rollback();
}finally{
if(session!=null){
session.close();
}
}
longendTime=System.currentTimeMillis();
System.out.println("插入完成,耗时"+(endTime-beginTime)+"毫秒!");
标绿的这几行是关键,这一段逻辑会经常使用,为了重用,可以封装一下:
/**
*批量提交数据
*@paramsqlSessionFactory
*@parammybatisSQLIdSQL语句在MapperXML文件中的ID
*@paramcommitCountEveryTime每次提交的记录数
*@paramlist要提交的数据列表
*@paramlogger日志记录器
*/
private<T>voidbatchCommit(SqlSessionFactorysqlSessionFactory,StringmybatisSQLId,intcommitCountEveryTime,List<T>list,Loggerlogger){
SqlSessionsession=null;
try{
session=sqlSessionFactory.openSession(ExecutorType.BATCH,false);
intcommitCount=(int)Math.ceil(list.size()/(double)commitCountEveryTime);
List<T>tempList=newArrayList<T>(commitCountEveryTime);
intstart,stop;
LongstartTime=System.currentTimeMillis();
for(inti=0;i<commitCount;i++){
tempList.clear();
start=i*commitCountEveryTime;
stop=Math.min(i*commitCountEveryTime+commitCountEveryTime-1,list.size()-1);
for(intj=start;j<=stop;j++){
tempList.add(list.get(j));
}
session.insert(mybatisSQLId,tempList);
session.commit();
session.clearCache();
}
LongendTime=System.currentTimeMillis();
logger.debug("batchCommit耗时:"+(endTime-startTime)+"毫秒");
}catch(Exceptione){
logger.error("batchCommiterror!",e);
e.printStackTrace();
session.rollback();
}finally{
if(session!=null){
session.close();
}
}
}
对应的,如果是批量update,也是类似的思路,只不过要注意一点:oracle环境中,多条语句提交的sql语句为
begin
updatexxxsetxxx=xxx;
updatexxxsetxxx=xxx;
end;
用mytais拼的时候,参考下面的写法:
<updateid="updateBatch"parameterType="java.util.List">
<foreachcollection="list"item="item"index="index"open="begin"close="end;">updatexxxsetx=#{item.x,jdbcType=VARCHAR}wherex=#{item.x,jdbcType=VARCHAR};</foreach>
</update>
关于批量提交的性能,Oracle环境下,我大概测试了一下:
insertinto...selectxxx
unionallselectyyy
unionallselectzzz;
最快,其次是
begin
insertinto...values...;
insertinto...values...;
end;
当然最慢是逐条insert提交,最后谈下Spring与mybatis集成后,AOP事务管理对批量提交的影响,通常情况下,我们会这样配置AOP事务管理:
<tx:adviceid="txAdvice"transaction-manager="transactionManager">
<tx:attributes>
<tx:methodname="do*"read-only="false"rollback-for="java.lang.Exception"/>
<tx:methodname="*"propagation="SUPPORTS"read-only="true"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcutid="pc"expression="execution(*ctas.service.*.*(..))"/>
<aop:advisorpointcut-ref="pc"advice-ref="txAdvice"/>
</aop:config>
这样,ctas.service(及子包)下的所有方法都被拦截,而且只有do开头的方法,具有可写的事务(即:能insert/update/delete记录),而其它方法是只读事务(即:只能select数据),但是我们前面谈到的批量提交操作,都是写代码手动提交的,不需要spring管理,所以配置中需要将某些方法排除,可以约定self开头的方法,由开发者自己管理事务,不需要spring代为管理,上面的配置要改成:
<aop:config>
<aop:pointcutid="pc"expression="execution(*ctas.service.*.*(..))and!execution(*ctas.service.*.self*(..)))"/>
<aop:advisorpointcut-ref="pc"advice-ref="txAdvice"/>
</aop:config>
通过and!execution(...)将self开头的方法排除就可以了,前面的批量操作代码写到selfXXX方法中。
关于批量提交,还有一种情况:父子表的批量插入。思路还是一样的,但是SQL的写法有点区别,原理参考下面的语句(Oracle环境),注意标绿的变量
DECLARE
BASE_IDINTEGER;
DETAIL_IDINTEGER;
BEGIN
--第1组记录
SELECTSEQ_T_BASE.NEXTVALINTOBASE_IDFROMDUAL;
INSERTINTOT_BASE(ID,FEE)VALUES(BASE_ID,?);
SELECTSEQ_T_DETAIL.NEXTVALINTODETAIL_IDFROMDUAL;
INSERTINTOT_DETAIL(ID,BASE_ID,FEE)VALUES(DETAIL_ID,BASE_ID,?);
SELECTSEQ_T_DETAIL.NEXTVALINTODETAIL_IDFROMDUAL;
INSERTINTOT_DETAIL(ID,BASE_ID,FEE)VALUES(DETAIL_ID,BASE_ID,?);
--第2组记录
SELECTSEQ_T_BASE.NEXTVALINTOBASE_IDFROMDUAL;
INSERTINTOT_BASE(ID,FEE)VALUES(BASE_ID,?);
SELECTSEQ_T_DETAIL.NEXTVALINTODETAIL_IDFROMDUAL;
INSERTINTOT_DETAIL(ID,BASE_ID,FEE)VALUES(DETAIL_ID,BASE_ID,?);
SELECTSEQ_T_DETAIL.NEXTVALINTODETAIL_IDFROMDUAL;
INSERTINTOT_DETAIL(ID,BASE_ID,FEE)VALUES(DETAIL_ID,BASE_ID,?);
--...
END;
xml映射文件中的写法:
<insertid="insertBatch"parameterType="java.util.List">
DECLARE
base_idINTEGER;
detail_idINTEGER;
<foreachcollection="list"item="item"index="index"open="begin"close="end;">
selectseq_t_base.nextvalintobase_idfromdual;
insertintot_base(id,fee)values(base_id,#{item.baseEntity.fee,jdbcType=DECIMAL});
<foreachcollection="item.details"item="detail"index="index">
selectseq_t_detail.nextvalintodetail_idfromdual;
insertintot_detail(id,base_id,fee)values(detail_id,base_id,#{detail.fee,jdbcType=DECIMAL});
</foreach>
</foreach>
</insert>
publicclassBaseDetailDto{
privateTBasebaseEntity;
privateList<TDetail>details;
publicTBasegetBaseEntity(){
returnbaseEntity;
}
publicvoidsetBaseEntity(TBasebaseEntity){
this.baseEntity=baseEntity;
}
publicList<TDetail>getDetails(){
returndetails;
}
publicvoidsetDetails(List<TDetail>details){
this.details=details;
}
}
相关推荐
它通常会显示SQL语句的原始形式,以及填充参数后的实际执行版本,帮助开发者定位潜在的SQL性能问题,如不恰当的索引使用、全表扫描等。 Free-Mybatis是这个插件的增强版,它在原有的基础上增加了更多实用功能。例如...
在数据库操作中,批量插入(Batch Insert)是一种提高性能的有效方式,尤其是在处理大量数据时。...对于大型项目,合理利用批量插入策略,结合数据库的性能调优,可以显著提高服务响应速度和用户体验。
在Java开发中,数据库连接管理是非常关键的一部分,它关乎到应用程序的性能和稳定性。MyBatis是一个优秀的持久层框架,...同时,Druid提供的监控功能也能帮助我们实时了解数据库连接池的状态,便于问题排查和性能调优。
这样的设计提供了更多的灵活性,尤其是在处理复杂查询或性能调优时。开发者可能为了项目需求,或是对现有MyBatis的扩展或简化,创建了自己的SQL解析器,以便在Spring+Hibernate的环境中更便捷地使用自定义SQL。 在...
MyBatis 是一款深受开发者喜爱的持久层框架,它简化了Java应用与数据库之间的交互,其中一个强大特性就是其动态SQL功能。...在实际项目中,结合合理的数据库设计和性能调优,动态SQL将成为你手中得心应手的工具。
包括MyBatis-Plus提供的批量更新,JdbcTemplate提供的批量更新,在xml中循环拼接sql批量更新、case when语句批量更新、replace into方式批量更新、ON DUPLICATE KEY UPDATE批量更新。 适用于对Spring Boot和数据库...
6. 性能优化:如何对SpringMVC和MyBatis进行性能调优,包括SQL优化、减少网络传输、利用缓存提升效率等。 7. 源码分析:部分视频可能会涉及SpringMVC和MyBatis的源码解读,帮助学习者理解其内部工作原理。 通过这...
包括MyBatis-Plus提供的批量更新,JdbcTemplate提供的批量更新,在xml中循环拼接sql批量更新、case when语句批量更新、replace into方式批量更新、ON DUPLICATE KEY UPDATE批量更新。 适用于对Spring Boot和数据库...
在本文中,我们将深入探讨MyBatis这一优秀的持久层框架,以及如何快速入门并进行一些基本的调优。MyBatis是一个轻量级的...通过熟练掌握MyBatis的使用和调优技巧,可以大大提高开发效率,同时保证系统的稳定性和性能。
由于描述中提到项目未完全优化并提交到GitHub,这意味着可能还有一些改进的空间,比如性能调优、错误处理、日志记录、单元测试等方面。对于学习者而言,这个项目提供了一个学习Spring Boot、MyBatis和MySQL集成的...
这在某些复杂查询或性能调优场景下显得尤为有价值。在MyBatis中,你可以直接编写SQL语句,将其配置在Mapper XML文件中,或者使用注解方式定义在接口的方法上,这样既保留了SQL的灵活性,又避免了手动管理JDBC连接的...
《高清深入浅出MyBatis...通过阅读本书,读者不仅可以掌握MyBatis的基本用法,还能深入了解其实战应用和性能调优,提升在实际项目中的开发能力。对于想要在Java Web开发领域深耕的程序员来说,这是一份非常宝贵的资源。
这包括对SQL语句、参数和执行时间的详细记录,有助于性能调优。 在错误处理和异常处理方面,Mybatis3.4.2 会提供更清晰的错误信息,帮助开发者快速定位问题。可能还新增了一些预定义的异常类,使得错误处理更加规范...
MyBatis是一个优秀的Java持久层框架,它支持定制化SQL...同时,这也有助于我们进行性能调优和解决实际开发中遇到的问题。总之,深入研究MyBatis源码对于提升Java开发者的数据库操作能力和框架理解水平具有重要的意义。
6. **测试与调优**:完成上述步骤后,进行测试以确保缓存功能正常工作,并根据实际情况进行性能调优。 MyBatis-Ehcache的整合使用不仅限于基本的缓存功能,还可以配合Spring等框架实现更复杂的缓存管理,例如利用...
Druid提供Web监控界面,可以实时查看数据库连接池状态,包括连接数量、SQL执行情况等,有助于开发者优化数据库操作和性能调优。 **PostgreSQL** PostgreSQL是一种强大的、开源的关系型数据库系统,具有丰富的数据...
10. 部署和优化,包括性能调优、错误处理和日志监控。 配合源代码,读者可以亲手实践这些示例,加深对Spring MVC和MyBatis的理解,进一步提高在企业级项目中的应用能力。这个资源对于Java Web开发者来说,无论是...
通过分析这些代码,我们可以深入理解如何在实际项目中实现批量插入,以及如何进行性能调优。 总之,这个压缩包提供的学习资源涵盖了SpringBoot与MyBatis集成、MyBatis批量插入的最佳实践、以及如何通过性能测试评估...