mybatis SQL性能调优

简介: Mybatis SQL性能调优         1.  Mapper层参数为Map,由Service层负责重载       Mapper由于机制的问题,不能重载,参数一般设置成Map,但这样会使参数变得模糊,如果想要使代码变得清晰,可以通过service层来实现重载的目的,对外提供的Service层是重载的,但这些重载的Service方法其实是调同一个Mapper,只不过相应

Mybatis SQL性能调优

 

 

 

 

1.  Mapper层参数为Map,由Service层负责重载

 

    Mapper由于机制的问题,不能重载,参数一般设置成Map,但这样会使参数变得模糊,如果想要使代码变得清晰,可以通过service层来实现重载的目的,对外提供的Service层是重载的,但这些重载的Service方法其实是调同一个Mapper,只不过相应的参数并不一致。

    也许有人会想,为什么不在Service层也设置成Map呢?我个人是不推荐这么做的,虽然为了方便,我在之前的项目中也大量采用了这种方式,但很明显会给日后的维护工作带来麻烦。因为这么做会使你整个MVC都依赖于Map模型,这个模型其实是很不错的,方便搭框架,但存在一个问题:仅仅看方法签名,你不清楚Map中所拥有的参数个数、类型、每个参数代表的含义。 

试想,你只对Service层变更,或者DAO层变更,你需要清楚整个流程中Map传递过来的参数,除非注释或者文档良好,否则必须把每一层的代码都了解清楚,你才知道传递了哪些参数。针对于简单MVC,那倒也还好,但如果层次复杂之后,代码会变得异常复杂,而且如果我增加一个参数,需要把每一个层的注释都添加上。相对于注释,使用方法签名来保证这种代码可控性会来得更可行一些,因为注释有可能是过时的,但方法签名一般不太可能是陈旧的。

 

 

2.  尽量少用if choose等语句

 

    Mybatis的配置SQL时,尽量少用if choose 等标签,能用SQL实现判断的尽量用SQL来判断(CASE WHEN ,DECODE等),以便后期维护。否则,一旦SQL膨胀,超级恶心,如果需要调试Mybatis中的SQL,需要去除大量的判断语句,非常麻烦。另一方面,大量的if判断,会使生成的SQL中包含大量的空格,增加网络传输的时间,也不可取。

而且大量的if choose语句,不可避免地,每次生成的SQL会不太一致,会导致ORACLE大量的硬解析,也不可取。 
我们来看看这样的SQL:

 

 

SELECT * FROM T_NEWS_TEXT WHERE 1 = 1

< choose>

< if test ="startdate != null and startdate != '' and enddate != null and endate != ''">

AND PUBLISHTIME >= #{startdate} AND PUBLISHTIME <= #{enddate}

</ if>

<otherwise>

AND PUBLISHTIME >= SYSDATE - 7 AND PUBLISHTIME <= SYSDATE

</otherwise></ choose >

 

 

 

    这样的if判断,其实是完全没有必要的,我们可以很简单的采用DECODE来解决默认值问题: 

    

 

SELECT * FROM T_NEWS_TEXT WHERE PUBLISHTIME >= DECODE(#{startdate},NULL,SYSDATE-7, #{startdate}) AND PUBLISHTIME <= DECODE(#{enddate},NULL,SYSDATE,#{enddate})

 

 

 当然有人会想,引入CASE WHEN,DECODE会导致需要ORACLE函数解析,会拖慢SQL执行时间,有兴趣的同学可以回去做一下测试,看看是否会有大的影响。就个人经验而言,在我的开发过程,没有发现因为函数解析导致SQL变慢的情形。影响SQL执行效率的一般情况下是JOIN、ORDER BY、DISTINCT、PARTITATION BY等这些操作,这些操作一般与表结构设计有很大的关联。相对于这些的效率影响程度,函数解析对于SQL执行速度影响应该是可以忽略不计的。

    另外一点,对于一些默认值的赋值,像上面那条SQL,默认成当前日期什么的,其实可以完全提到Service层或Controller层做处理,在Mybatis中应该要少用这些判断。因为,这样的话,很难做缓存处理。如果startdate为空,在SQL上使用动态的SYSDATE,就无法确定缓存startdate日期的key应该是什么了。所以参数最好在传递至Mybatis之前都处理好,这样Mybatis层也能减少部分if choose语句,同时也方便做缓存处理。

    当然不使用if choose也并不是绝对的,有时候为了优化SQL,不得不使用if来解决,比如说LIKE语句,当然一般不推荐使用LIKE,但如果存在使用的场景,尽可能在不需要使用时候去除LIKE,比如查询文章标题,以提高查询效率。 最好的方式是使用lucence等搜索引擎来解决这种全文索引的问题。

总的来说,if与choose判断分支是不可能完全去除的,但是推荐使用SQL原生的方式来解决一些动态问题,而不应该完全依赖Mybatis来完成动态分支的判断,因为判断分支过于复杂,而且难以维护。 

 

3.  用XML注释取代SQL注释

 

    Mybatis中原SQL的注释尽量不要保留,注释会引发一些问题,如果需要使用注释,可以在XML中用<!-- -->来注释,保证在生成的SQL中不会存在SQL注释,从而降低问题出现的可能性。这样做还有一个好处,就是在IDE中可以很清楚的区分注释与SQL。

    现在来谈谈注释引发的问题,我做的一个项目中,分页组件是基于Mybatis的,它会在你写的SQL脚本外面再套一层SELECT COUNT(*) ROWNUM_ FROM (....) 计算总记录数,同时有另一个嵌套SELECT * FROM(...) WHERE ROWNUM > 10 AND RONNUM < 10 * 2这种方式生成分页信息,如果你的脚本中最后一行出现了注释,则添加的部分会成为注释的一部分,执行就会报错。除此之外,某些情况下也可能导致部分条件被忽略,如下面的情况: 

 

SELECT * FROM TEST WHERE COL1 >  1  -- 这里是注释<if test="a != null and a != ''">AND COL2 = #{a}</if>

   

 即使传入的参数中存在对应的参数,实际也不会产生效果,因为后面的内容实际上是被完全注释了。这种错误,如果不经过严格的测试,是很难发现的。一般情况下,XML注释完全可以替代SQL注释,因此这种行为应该可以禁止掉。 

 

 

 

4.  尽可能使用#{},而不是${}

 

Mybatis中尽量不要使用${},尽量这样做很方便开发,但是有一个问题,就是大量使用会导致ORACLE的硬解析,拖慢数据库性能,运行越久,数据库性能会越差。

 

select * from T_PRINT_LAYOUT where  D_RECID = ${recId}

最后生成的SQL为:

select * from T_PRINT_LAYOUT where  D_RECID = 1

即:直接将参数值替换到了原来${recId}的位置,相当于硬拼SQL

 

select * from T_PRINT_LAYOUT where  D_RECID = #{recid,jdbcType=DECIMAL}

最后生成的SQL为:

select * from T_PRINT_LAYOUT where  D_RECID = ?

即:#{...}被识别为一个SQL参数

 

    关于${},另一个误用的地方就是LIKE,我这边还有个案例:比如一些树型菜单,节点会设计成'01','0101',用两位节点来区分层级,这时候,如果需要查询01节点下所有的节点,最简单的SQL便是:SELECT * FROM TREE WHERE ID LIKE '01%',这种SQL其实无可厚非,因为它也能用到索引,所以不需要特别的处理,直接使用就行了。但如果是文章标题,则需要额外注意了:SELECT * FROM T_NEWS_TEXT WHERE TITLE LIKE '%OSC%',这是怎么也不会用到索引的,上面说了,最好采用全文检索。但如果离不开LIKE,就需要注意使用的方式: ID LIKE #{ID} || '%'而不是ID LIKE '${ID}%',减少硬解析的可能。

    有人觉得使用||会增加ORACLE处理的时间,我觉得不要把ORACLE看得太傻,虽然有时候确实非常傻,有空可以再总结ORACLE傻不垃圾的地方,但是稍加测试便知:这种串联方式,对于整个SQL的解析执行,应该是微乎其微的。

    当然还有一些特殊情况是没有办法处理的,比如说动态注入列名、表名等。对于这些情况,则比较棘手,没有找到比较方便的手段。由于这种情况出现的可能性会比较少,所以使用${}倒也不至于有什么太大的影响。当然你如果有代码洁癖的话,可以使用ORACLE的动态执行SQL的机制Execute immediate,这样就可以完全避免${}出现的可能性了。这样会引入比较复杂的模型,这个时候,你就需要取舍了。

    针对于以上动态SQL所导致的问题,最激进的方式是全部采用存储过程,用数据库原生的方式来解决,方便开发调试,当然也会带来问题:对开发人员会有更高的要求、存储过程的管理等等,我这边项目没有采用过这种方式,这里不做更多的展开。 

 

 

 

5.  大量数据的批量处理(变量绑定)

 

大量数据(条数>10000)做insert时,如果按常规方式,每条insert into table(...) values(...);来提交,速度巨慢。改善性能的思路是多条insert批量提交。

oracle环境中,有一种批量insert的小技巧,原理是 insert into ... select from ...,套在mybatis上,变形为:

 

INSERT INTO T_TEST

  (ID, COL_A, COL_B)

  SELECT SEQ_TEST.NEXTVAL, A.*

    FROM (

              SELECT 'A1', 'B1' FROM DUAL

    UNION ALL SELECT 'A2', 'B2' FROM DUAL

    UNION ALL SELECT 'A3', 'B3' FROM DUAL

    UNION ALL SELECT 'A4', 'B4' FROM DUAL

    UNION ALL SELECT 'A5', 'B5' FROM DUAL

    UNION ALL SELECT 'A6', 'B6' FROM DUAL

) A

 

中间的部分非常有规律,可以用foreach标签生成,参考下面的片段

 

<insert id="insertBatch2" parameterType="ctas.entity.SharkFlt">

    <selectKey keyProperty="recId" order="BEFORE" resultType="Long">

      select SEQ_CTAS_SHARK_FLT.nextval as recId from dual

    </selectKey>

    insert into CTAS_SHARK_FLT (<include refid="Base_Column_List"/>) SELECT SEQ_TEST.NEXTVAL, A.*

    FROM (

    <foreach collection="list" item="item" index="index" open="" close="" separator="union all">

      select #{item.awbType,jdbcType=VARCHAR}, #{item.awbPre,jdbcType=VARCHAR},... from dual

    </foreach>

    ) A

  </insert>

 

 

即使这样,也不能直接run,oracle中一次执行的sql语句长度是有限制的,如果最后拼出来的sql字符串过长,会导致执行失败,所以java端还要做一个分段处理,参考下面的处理:

 

List<SharkFlt> data = new ArrayList<SharkFlt>();

        for (TSharkFlt f : sharkFlts) {

            data.add(getSharkFlt(f));

        }

 

        System.out.println(data.size());

 

        long beginTime = System.currentTimeMillis();

        System.out.println("开始插入...");

        SqlSessionFactory sqlSessionFactory = ctx.getBean(SqlSessionFactory.class);

        SqlSession session = null;

        try {

            session = sqlSessionFactory.openSession(ExecutorType.BATCH, false);

            int a = 2000;//每次提交2000条

            int loop = (int) Math.ceil(data.size() / (double) a);

 

            List<SharkFlt> tempList = new ArrayList<SharkFlt>(a);

            int start, stop;

            for (int i = 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 (int j = 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 (Exception e) {

            e.printStackTrace();

            session.rollback();

        } finally {

            if (session != null) {

                session.close();

            }

        }

        long endTime = System.currentTimeMillis();

        System.out.println("插入完成,耗时 " + (endTime - beginTime) + " 毫秒!");

 

 

 

标绿的这几行是关键,这一段逻辑会经常使用,为了重用,可以封装一下:

 

 

/**

     * 批量提交数据

     * @param sqlSessionFactory

     * @param mybatisSQLId SQL语句在Mapper XML文件中的ID

     * @param commitCountEveryTime 每次提交的记录数

     * @param list 要提交的数据列表

     * @param logger 日志记录器

     */

    private <T> void batchCommit(SqlSessionFactory sqlSessionFactory, String mybatisSQLId, int commitCountEveryTime, List<T> list, Logger logger) {

        SqlSession session = null;

        try {

            session = sqlSessionFactory.openSession(ExecutorType.BATCH, false);

            int commitCount = (int) Math.ceil(list.size() / (double) commitCountEveryTime);

            List<T> tempList = new ArrayList<T>(commitCountEveryTime);

            int start, stop;

            Long startTime = System.currentTimeMillis();

            for (int i = 0; i < commitCount; i++) {

                tempList.clear();

                start = i * commitCountEveryTime;

                stop = Math.min(i * commitCountEveryTime + commitCountEveryTime - 1, list.size() - 1);

                for (int j = start; j <= stop; j++) {

                    tempList.add(list.get(j));

                }

                session.insert(mybatisSQLId, tempList);

                session.commit();

                session.clearCache();

            }

            Long endTime = System.currentTimeMillis();

            logger.debug("batchCommit耗时:" + (endTime - startTime) + "毫秒");

        } catch (Exception e) {

            logger.error("batchCommit error!", e);

            e.printStackTrace();

            session.rollback();

        } finally {

            if (session != null) {

                session.close();

            }

        }

}

 

 

 

对应的,如果是批量update,也是类似的思路,只不过要注意一点:oracle环境中,多条语句提交的sql语句为

begin

  update xxx set xxx =xxx ;

  update xxx set xxx =xxx;

end;

用mytais拼的时候,参考下面的写法:

 <update id="updateBatch" parameterType="java.util.List">

     <foreach collection="list" item="item" index="index" open="begin" close="end;" >                            update xxx set x=#{item.x,jdbcType=VARCHAR} where x =#{item.x,jdbcType=VARCHAR};       </foreach>

 </update>

 

 

 

关于批量提交的性能,Oracle环境下,我大概测试了一下:

insert into ... select xxx

  union all select yyy

  union all select zzz;

最快,其次是

begin

  insert into ... values  ...;

  insert into ... values ...;

end;

当然最慢是逐条insert提交,最后谈下Spring与mybatis集成后,AOP事务管理 对 批量提交的影响 ,通常情况下,我们会这样配置AOP事务管理:

 

 

<tx:advice id="txAdvice" transaction-manager="transactionManager">

        <tx:attributes>

            <tx:method name="do*" read-only="false" rollback-for="java.lang.Exception"/>

            <tx:method name="*" propagation="SUPPORTS" read-only="true"/>

        </tx:attributes>

    </tx:advice>

 

    <aop:config>

        <aop:pointcut id="pc" expression="execution(* ctas.service.*.*(..))"/>

        <aop:advisor pointcut-ref="pc" advice-ref="txAdvice"/>

    </aop:config>

 

 

这样,ctas.service(及子包)下的所有方法都被拦截,而且只有do开头的方法,具有可写的事务(即:能insert/update/delete记录),而其它方法是只读事务(即:只能select数据),但是我们前面谈到的批量提交操作,都是写代码手动提交的,不需要spring管理,所以配置中需要将某些方法排除,可以约定self开头的方法,由开发者自己管理事务,不需要spring代为管理,上面的配置要改成:

 

 

 <aop:config>

       <aop:pointcut id="pc" expression="execution(* ctas.service.*.*(..)) and !execution(* ctas.service.*.self*(..)))"/>

         <aop:advisor pointcut-ref="pc" advice-ref="txAdvice"/>

   </aop:config>

 

 

通过 and !execution(...) 将self开头的方法排除就可以了,前面的批量操作代码写到selfXXX方法中。

 

关于批量提交,还有一种情况:父子表的批量插入。思路还是一样的,但是SQL的写法有点区别,原理参考下面的语句(Oracle环境),注意标绿的变量

 

 

DECLARE

  BASE_ID   INTEGER;

  DETAIL_ID INTEGER;

BEGIN

  --第1组记录

  SELECT SEQ_T_BASE.NEXTVAL INTO BASE_ID FROM DUAL;

  INSERT INTO T_BASE (ID, FEE) VALUES (BASE_ID, ?);

  

  SELECT SEQ_T_DETAIL.NEXTVAL INTO DETAIL_ID FROM DUAL;

  INSERT INTO T_DETAIL (ID, BASE_ID, FEE) VALUES (DETAIL_ID, BASE_ID, ?);

  SELECT SEQ_T_DETAIL.NEXTVAL INTO DETAIL_ID FROM DUAL;

  INSERT INTO T_DETAIL (ID, BASE_ID, FEE) VALUES (DETAIL_ID, BASE_ID, ?);

  

  --第2组记录

  SELECT SEQ_T_BASE.NEXTVAL INTO BASE_ID FROM DUAL;

  INSERT INTO T_BASE (ID, FEE) VALUES (BASE_ID, ?);

  

  SELECT SEQ_T_DETAIL.NEXTVAL INTO DETAIL_ID FROM DUAL;

  INSERT INTO T_DETAIL (ID, BASE_ID, FEE) VALUES (DETAIL_ID, BASE_ID, ?);

  SELECT SEQ_T_DETAIL.NEXTVAL INTO DETAIL_ID FROM DUAL;

  INSERT INTO T_DETAIL (ID, BASE_ID, FEE) VALUES (DETAIL_ID, BASE_ID, ?);

  

  --...

END;

 

 

 

 

xml映射文件中的写法:

 

<insert id="insertBatch" parameterType="java.util.List">

        DECLARE

        base_id INTEGER ;

        detail_id INTEGER ;

        <foreach collection="list" item="item" index="index" open="begin" close="end;">

            select seq_t_base.nextval into base_id from dual;

            insert into t_base(id, fee) values(base_id, #{item.baseEntity.fee,jdbcType=DECIMAL});

            <foreach collection="item.details" item="detail" index="index">

                select seq_t_detail.nextval into detail_id from dual;

                insert into t_detail(id, base_id, fee) values(detail_id,base_id,#{detail.fee,jdbcType=DECIMAL});

            </foreach>

        </foreach>

</insert>

 

 

public class BaseDetailDto {

 

    private TBase baseEntity;

 

    private List<TDetail> details;

 

    public TBase getBaseEntity() {

        return baseEntity;

    }

 

    public void setBaseEntity(TBase baseEntity) {

        this.baseEntity = baseEntity;

    }

 

 

    public List<TDetail> getDetails() {

        return details;

    }

 

    public void setDetails(List<TDetail> details) {

        this.details = details;

    }

}

目录
相关文章
|
SQL Java 数据库连接
【YashanDB知识库】解决mybatis的mapper文件sql语句结尾加分号";"报错
【YashanDB知识库】解决mybatis的mapper文件sql语句结尾加分号";"报错
|
SQL Java 数据库连接
MyBatis动态SQL字符串空值判断,这个细节99%的程序员都踩过坑!
本文深入探讨了MyBatis动态SQL中字符串参数判空的常见问题。通过具体案例分析,对比了`name != null and name != &#39;&#39;`与`name != null and name != &#39; &#39;`两种写法的差异,指出后者可能引发逻辑混乱。为避免此类问题,建议在后端对参数进行预处理(如trim去空格),简化MyBatis判断逻辑,提升代码健壮性与可维护性。细节决定成败,严谨处理参数判空是写出高质量代码的关键。
2171 0
|
SQL XML Java
通过MyBatis的XML配置实现灵活的动态SQL查询
总结而言,通过MyBatis的XML配置实现灵活的动态SQL查询,可以让开发者以声明式的方式构建SQL语句,既保证了SQL操作的灵活性,又简化了代码的复杂度。这种方式可以显著提高数据库操作的效率和代码的可维护性。
748 18
|
SQL 关系型数据库 MySQL
为什么这些 SQL 语句逻辑相同,性能却差异巨大?
我是小假 期待与你的下一次相遇 ~
558 0
|
SQL 关系型数据库 PostgreSQL
CTE vs 子查询:深入拆解PostgreSQL复杂SQL的隐藏性能差异
本文深入探讨了PostgreSQL中CTE(公共表表达式)与子查询的选择对SQL性能的影响。通过分析两者底层机制,揭示CTE的物化特性及子查询的优化融合优势,并结合多场景案例对比执行效率。最终给出决策指南,帮助开发者根据数据量、引用次数和复杂度选择最优方案,同时提供高级优化技巧和版本演进建议,助力SQL性能调优。
|
SQL Java 数据库连接
SSM相关问题-1--#{}和${}有什么区别吗?--Mybatis都有哪些动态sql?能简述一下动 态sql的执行原理吗?--Spring支持的几种bean的作用域 Scope
在MyBatis中,`#{}`是预处理占位符,可防止SQL注入,适用于大多数参数传递场景;而`${}`是直接字符串替换,不安全,仅用于动态表名、列名等特殊场景。二者在安全性、性能及使用场景上有显著区别。
658 0
|
SQL XML Java
菜鸟之路Day35一一Mybatis之XML映射与动态SQL
本文介绍了MyBatis框架中XML映射与动态SQL的使用方法,作者通过实例详细解析了XML映射文件的配置规范,包括namespace、id和resultType的设置。文章还对比了注解与XML映射的优缺点,强调复杂SQL更适合XML方式。在动态SQL部分,重点讲解了`&lt;if&gt;`、`&lt;where&gt;`、`&lt;set&gt;`、`&lt;foreach&gt;`等标签的应用场景,如条件查询、动态更新和批量删除,并通过代码示例展示了其灵活性与实用性。最后,通过`&lt;sql&gt;`和`&lt;include&gt;`实现代码复用,优化维护效率。
1566 5
|
SQL 关系型数据库 MySQL
大数据新视界--大数据大厂之MySQL数据库课程设计:MySQL 数据库 SQL 语句调优方法详解(2-1)
本文深入介绍 MySQL 数据库 SQL 语句调优方法。涵盖分析查询执行计划,如使用 EXPLAIN 命令及理解关键指标;优化查询语句结构,包括避免子查询、减少函数使用、合理用索引列及避免 “OR”。还介绍了索引类型知识,如 B 树索引、哈希索引等。结合与 MySQL 数据库课程设计相关文章,强调 SQL 语句调优重要性。为提升数据库性能提供实用方法,适合数据库管理员和开发人员。
|
SQL 关系型数据库 MySQL
如何优化SQL查询以提高数据库性能?
这篇文章以生动的比喻介绍了优化SQL查询的重要性及方法。它首先将未优化的SQL查询比作在自助餐厅贪多嚼不烂的行为,强调了只获取必要数据的必要性。接着,文章详细讲解了四种优化策略:**精简选择**(避免使用`SELECT *`)、**专业筛选**(利用`WHERE`缩小范围)、**高效联接**(索引和限制数据量)以及**使用索引**(加速搜索)。此外,还探讨了如何避免N+1查询问题、使用分页限制结果、理解执行计划以及定期维护数据库健康。通过这些技巧,可以显著提升数据库性能,让查询更高效流畅。
|
关系型数据库 MySQL 大数据
大数据新视界--大数据大厂之MySQL 数据库课程设计:MySQL 数据库 SQL 语句调优的进阶策略与实际案例(2-2)
本文延续前篇,深入探讨 MySQL 数据库 SQL 语句调优进阶策略。包括优化索引使用,介绍多种索引类型及避免索引失效等;调整数据库参数,如缓冲池、连接数和日志参数;还有分区表、垂直拆分等其他优化方法。通过实际案例分析展示调优效果。回顾与数据库课程设计相关文章,强调全面认识 MySQL 数据库重要性。为读者提供综合调优指导,确保数据库高效运行。