自动生成 SQL 会拖慢性能吗?实测 MyBatisGX、MyBatis、MyBatis-Plus、MyBatis-Flex

5 阅读14分钟

自动生成 SQL 会拖慢性能吗?实测 MyBatisGX、MyBatis、MyBatis-Plus、MyBatis-Flex

一、引言

选持久化框架时,很多开发者会担心:

  • "自动生成的 SQL 肯定不如手写的快"
  • "ORM 框架抽象层越高,性能损耗越大"
  • "为了开发效率牺牲性能,划算吗?"

这些担忧合理,但事实如此吗?

我对主流的 MyBatis 系列框架做了一次性能测试:

  • MyBatis:原生手写 SQL,性能基准
  • MyBatisGX:方法名生成 SQL + 预生成机制
  • MyBatis-Plus:运行时动态生成 SQL
  • MyBatis-Flex:轻量级动态生成

用真实的测试数据回答:自动生成 SQL 的性能代价到底有多大?


二、测试环境

硬件配置

  • CPU:4核8线程
  • 内存:16GB

软件配置

  • JDK 版本:21
  • MySQL 版本:5.7
  • Spring Boot 版本:3.x

JVM 参数

-Xms1g
-Xmx4g

测试表结构

CREATE TABLE `user` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `username` VARCHAR(50),
  `age` INT,
  `status` TINYINT,
  `email` VARCHAR(100),
  `phone` VARCHAR(20),
  `create_time` DATETIME,
  `update_time` DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

8个字段,贴近真实业务。只有主键索引,避免索引优化干扰。


三、测试方法

3.1 测试项目

插入操作:

  • 单条插入
  • 批量插入 100 条
  • 批量插入 10,000 条

更新操作:

  • 单条更新
  • 动态更新(仅更新非空字段)
  • 批量更新 100 条
  • 批量更新 10,000 条

查询操作:

  • 主键查询
  • 简单条件查询(3个条件)
  • 复杂条件查询(Like、In、Gt 等)
  • 动态条件查询

3.2 测试策略

JVM 在首次执行时会进行类加载、JIT 编译等初始化工作。每个测试项连续执行 15 轮,第 1 轮单独记录,第 2-15 轮用于统计。

统计指标:

  • 首次执行耗时
  • 热身后平均值
  • 热身后最快值
  • 热身后最慢值

3.3 四个框架的实现方式

四个框架执行完全相同的业务逻辑。

MyBatis(手写 XML)
<insert id="insert">
  INSERT INTO user (id, username, age, status, email, phone, create_time, update_time)
  VALUES (#{id}, #{username}, #{age}, #{status}, #{email}, #{phone}, #{createTime}, #{updateTime})
</insert>

<select id="findByIdAndAgeAndStatus" resultType="User">
  SELECT * FROM user 
  WHERE id = #{id} AND age = #{age} AND status = #{status}
</select>

<select id="findDynamicByConditions" resultType="User">
  SELECT * FROM user
  <where>
    <if test="id != null">AND id = #{id}</if>
    <if test="username != null and username != ''">
      AND username LIKE CONCAT('%', #{username}, '%')
    </if>
    <if test="age != null">AND age &gt; #{age}</if>
    <if test="statusList != null and statusList.size() > 0">
      AND status IN
      <foreach item="item" collection="statusList" open="(" separator="," close=")">
        #{item}
      </foreach>
    </if>
  </where>
</select>
MyBatisGX(方法名生成)
public interface UserDao extends SimpleDao<User, UserQuery, Long> {
    // 继承自 SimpleDao 的方法:
    // int insert(User user);
    // int insertBatch(List<User> users, int batchSize);
    // int updateById(User user);
    // int updateBatchById(List<User> users, int batchSize);
    // User findById(Long id);
    
    // 方法名生成查询 SQL
    List<User> findByIdAndAgeAndStatus(Long id, Integer age, Integer status);
    
    // 复杂条件查询
    List<User> findByIdAndUsernameLikeAndAgeGtAndStatusIn(
        Long id, String username, Integer age, List<Integer> statusList
    );
    
    // 动态查询(使用 QueryEntity)
    @Dynamic
    List<User> findDynamicByIdAndUsernameLikeAndAgeGtAndStatusIn(UserQuery query);
}
MyBatis-Plus(Wrapper)
public interface UserMapper extends BaseMapper<User> {
}

// Service 层构建查询
List<User> findByIdAndAgeAndStatus(Long id, Integer age, Integer status) {
    return userMapper.selectList(
        new LambdaQueryWrapper<User>()
            .eq(User::getId, id)
            .eq(User::getAge, age)
            .eq(User::getStatus, status)
    );
}

// 动态查询
List<User> findDynamicByConditions(Long id, String username, Integer age, List<Integer> statusList) {
    return userMapper.selectList(
        new LambdaQueryWrapper<User>()
            .eq(id != null, User::getId, id)
            .like(StringUtils.isNotBlank(username), User::getUsername, username)
            .gt(age != null, User::getAge, age)
            .in(CollectionUtils.isNotEmpty(statusList), User::getStatus, statusList)
    );
}
MyBatis-Flex(QueryWrapper)
public interface UserMapper extends BaseMapper<User> {
}

// Service 层构建查询
List<User> findByIdAndAgeAndStatus(Long id, Integer age, Integer status) {
    return userMapper.selectListByQuery(
        QueryWrapper.create()
            .eq(User::getId, id)
            .eq(User::getAge, age)
            .eq(User::getStatus, status)
    );
}

// 动态查询
List<User> findDynamicByConditions(Long id, String username, Integer age, List<Integer> statusList) {
    QueryWrapper query = QueryWrapper.create();
    if (id != null) query.eq(User::getId, id);
    if (StringUtils.isNotBlank(username)) query.like(User::getUsername, username);
    if (age != null) query.gt(User::getAge, age);
    if (CollectionUtils.isNotEmpty(statusList)) query.in(User::getStatus, statusList);
    return userMapper.selectListByQuery(query);
}

四、测试结果

4.1 单条插入

框架首次执行热身后平均最快最慢
MyBatis57,594 μs1,830 μs1,544 μs2,262 μs
MyBatisGX79,329 μs1,922 μs1,586 μs2,632 μs
MyBatis-Flex314,609 μs2,587 μs2,217 μs3,145 μs
MyBatis-Plus118,034 μs2,234 μs1,876 μs2,848 μs

首次执行,MyBatis-Flex 耗时最长(314ms),MyBatis 最短(57ms)。热身后,四者都在 1.82.6ms 之间,最大差距 757μs。各框架波动范围在 400900μs。


4.2 批量插入 100 条

框架首次执行热身后平均最快最慢
MyBatis41 ms11 ms6 ms25 ms
MyBatisGX36 ms12 ms7 ms56 ms
MyBatis-Flex53 ms21 ms10 ms40 ms
MyBatis-Plus80 ms20 ms10 ms117 ms

热身后,MyBatis 和 MyBatisGX 在 1012ms,MyBatis-Flex 和 MyBatis-Plus 在 2021ms。


4.3 批量插入 10,000 条

框架首次执行热身后平均最快最慢
MyBatis459 ms307 ms239 ms653 ms
MyBatisGX742 ms323 ms264 ms569 ms
MyBatis-Flex1,025 ms381 ms293 ms581 ms
MyBatis-Plus690 ms446 ms350 ms686 ms

热身后:MyBatis 307ms < MyBatisGX 323ms < MyBatis-Flex 381ms < MyBatis-Plus 446ms。MyBatisGX 比 MyBatis 慢 16ms(约 5%),MyBatis-Plus 慢 139ms(约 45%)。所有框架都在同一数量级。


4.4 单条更新

框架首次执行热身后平均最快最慢
MyBatis3,302 μs1,139 μs877 μs1,318 μs
MyBatisGX1,987 μs1,323 μs1,035 μs2,523 μs
MyBatis-Flex4,109 μs1,308 μs863 μs1,772 μs
MyBatis-Plus15,781 μs1,558 μs1,230 μs2,070 μs

热身后,四者都在 1.1~1.6ms 之间,最大差距 419μs。MyBatis-Plus 首次执行明显慢于其他框架。


4.5 动态更新(UpdateSelective)

框架首次执行热身后平均最快最慢
MyBatis21,109 μs1,350 μs1,123 μs1,524 μs
MyBatisGX24,655 μs1,574 μs1,244 μs2,182 μs
MyBatis-Flex58,886 μs1,362 μs1,054 μs1,890 μs
MyBatis-Plus29,328 μs1,950 μs1,591 μs2,679 μs

热身后,四者都在 1.3~2.0ms 之间,最大差距 600μs。


4.6 批量更新 100 条

框架首次执行热身后平均最快最慢
MyBatis46 ms17 ms12 ms45 ms
MyBatisGX112 ms21 ms16 ms30 ms
MyBatis-Flex105 ms25 ms18 ms65 ms
MyBatis-Plus107 ms26 ms19 ms46 ms

热身后,四者都在 17~26ms 之间。


4.7 批量更新 10,000 条

框架首次执行热身后平均最快最慢
MyBatis1,427 ms1,458 ms1,294 ms1,618 ms
MyBatisGX1,588 ms1,635 ms1,423 ms1,855 ms
MyBatis-Flex1,872 ms1,568 ms1,397 ms1,872 ms
MyBatis-Plus1,948 ms1,725 ms1,497 ms1,965 ms

热身后,四者都在 1.4~1.7 秒,最大差距 267ms。MyBatisGX 比 MyBatis 慢 177ms(约 12%)。


4.8 主键查询(FindById)

框架首次执行热身后平均最快最慢
MyBatis17,080 μs1,314 μs889 μs1,813 μs
MyBatisGX14,819 μs1,472 μs1,047 μs2,051 μs
MyBatis-Flex26,498 μs1,232 μs777 μs1,512 μs
MyBatis-Plus25,419 μs1,333 μs1,026 μs2,101 μs

热身后,四者都在 1.21.5ms 之间,最大差距 240μs。各框架波动范围在 7001000μs。


4.9 简单条件查询(FindByIdAndAgeAndStatus)

框架首次执行热身后平均最快最慢
MyBatis2,645 μs1,198 μs952 μs1,789 μs
MyBatisGX2,467 μs1,451 μs952 μs1,879 μs
MyBatis-Flex4,449 μs1,499 μs1,163 μs1,955 μs
MyBatis-Plus9,865 μs2,059 μs1,673 μs3,098 μs

热身后,MyBatis-Plus 慢于其他三个框架。MyBatis、MyBatisGX、MyBatis-Flex 三者在 1.2~1.5ms 之间。


4.10 复杂条件查询(Like + In + Gt)

框架首次执行热身后平均最快最慢
MyBatis4,177 μs1,536 μs1,148 μs2,134 μs
MyBatisGX11,444 μs1,512 μs1,098 μs1,886 μs
MyBatis-Flex2,474 μs1,116 μs919 μs1,227 μs
MyBatis-Plus6,863 μs1,687 μs1,352 μs2,187 μs

热身后,四者都在 1.1~1.7ms 之间,最大差距 571μs。MyBatis-Flex 表现较好(1,116μs)。


4.11 动态条件查询

框架首次执行热身后平均最快最慢
MyBatis14,796 μs1,897 μs1,529 μs2,316 μs
MyBatisGX2,193 μs1,476 μs1,145 μs1,804 μs
MyBatis-Flex1,723 μs1,386 μs1,058 μs1,777 μs
MyBatis-Plus2,696 μs1,584 μs1,242 μs2,163 μs

这是最接近真实业务的测试。条件参数可能为空,需要动态构建 WHERE 子句。

首次执行,MyBatis 耗时 14,796μs,显著高于其他三个框架(1,723μs ~ 2,696μs)。热身后,四个框架都在 1.4~1.9ms 之间,最大差距 511μs。

波动范围:

  • MyBatis:787μs(1,529 ~ 2,316)
  • MyBatisGX:659μs(1,145 ~ 1,804)
  • MyBatis-Flex:719μs(1,058 ~ 1,777)
  • MyBatis-Plus:921μs(1,242 ~ 2,163)

多轮测试结果可重复。热身后四个框架的性能在同一数量级。


五、MyBatisGX 的价值定位

既然性能都差不多,为什么选 MyBatisGX?

在不牺牲性能的前提下,提升开发效率和代码可维护性。

5.1 代码对比:不同框架的实现方式

场景1:简单查询

MyBatis(手写 XML):

<!-- UserMapper.xml -->
<select id="findByIdAndAgeAndStatus" resultType="User">
  SELECT id, username, age, status, email, phone, create_time, update_time
  FROM user
  WHERE id = #{id} AND age = #{age} AND status = #{status}
</select>
// UserMapper.java
List<User> findByIdAndAgeAndStatus(
    @Param("id") Long id, 
    @Param("age") Integer age, 
    @Param("status") Integer status
);

MyBatisGX(方法名生成):

// UserDao.java - 无需 XML
List<User> findByIdAndAgeAndStatus(Long id, Integer age, Integer status);

MyBatis-Plus(Wrapper):

// Service 层
List<User> findByIdAndAgeAndStatus(Long id, Integer age, Integer status) {
    return userMapper.selectList(
        new LambdaQueryWrapper<User>()
            .eq(User::getId, id)
            .eq(User::getAge, age)
            .eq(User::getStatus, status)
    );
}

MyBatis-Flex(QueryWrapper):

// Service 层
List<User> findByIdAndAgeAndStatus(Long id, Integer age, Integer status) {
    return userMapper.selectListByQuery(
        QueryWrapper.create()
            .eq(User::getId, id)
            .eq(User::getAge, age)
            .eq(User::getStatus, status)
    );
}

代码量对比:

  • MyBatis:XML + 接口定义,约 8 行
  • MyBatisGX:仅 1 行方法声明
  • MyBatis-Plus:5 行 Wrapper 构建
  • MyBatis-Flex:5 行 QueryWrapper 构建

场景2:动态查询

MyBatis(XML + if 标签):

<select id="findByConditions" resultType="User">
  SELECT * FROM user
  <where>
    <if test="id != null">AND id = #{id}</if>
    <if test="username != null and username != ''">
      AND username LIKE CONCAT('%', #{username}, '%')
    </if>
    <if test="age != null">AND age &gt; #{age}</if>
    <if test="statusList != null and statusList.size() > 0">
      AND status IN
      <foreach item="item" collection="statusList" open="(" separator="," close=")">
        #{item}
      </foreach>
    </if>
  </where>
</select>

MyBatisGX(QueryEntity + 注解):

// 定义查询实体
@QueryEntity(User.class)
public class UserQuery extends User {
    private String usernameLike;      // 自动识别为 LIKE 条件
    private Integer ageGt;            // 自动识别为 > 条件
    private List<Integer> statusIn;   // 自动识别为 IN 条件
}

// DAO 层 - 一行搞定
@Dynamic
List<User> findByConditions(UserQuery query);

MyBatis-Plus(Wrapper):

// Service 层
public List<User> searchUsers(Long id, String username, Integer minAge, List<Integer> statusList) {
    return userMapper.selectList(
        new LambdaQueryWrapper<User>()
            .eq(id != null, User::getId, id)
            .like(StringUtils.isNotBlank(username), User::getUsername, username)
            .gt(minAge != null, User::getAge, minAge)
            .in(CollectionUtils.isNotEmpty(statusList), User::getStatus, statusList)
    );
}

MyBatis-Flex(QueryWrapper):

// Service 层
public List<User> searchUsers(Long id, String username, Integer minAge, List<Integer> statusList) {
    QueryWrapper query = QueryWrapper.create();
    if (id != null) query.eq(User::getId, id);
    if (StringUtils.isNotBlank(username)) query.like(User::getUsername, username);
    if (minAge != null) query.gt(User::getAge, minAge);
    if (CollectionUtils.isNotEmpty(statusList)) query.in(User::getStatus, statusList);
    return userMapper.selectListByQuery(query);
}

代码量对比:

  • MyBatis:XML 约 15 行
  • MyBatisGX:QueryEntity 类 + 1 行方法声明
  • MyBatis-Plus:6 行 Wrapper 构建
  • MyBatis-Flex:6 行 QueryWrapper 构建

场景3:层次边界的差异

MyBatis-Plus / MyBatis-Flex(持久化逻辑泄露到 Service 层):

// Service 层代码
public List<User> searchUsers(Long id, String username, Integer minAge, List<Integer> statusList) {
    // 持久化逻辑直接写在 Service 层
    return userMapper.selectList(
        new LambdaQueryWrapper<User>()
            .eq(id != null, User::getId, id)
            .like(StringUtils.isNotBlank(username), User::getUsername, username)
            .gt(minAge != null, User::getAge, minAge)
            .in(CollectionUtils.isNotEmpty(statusList), User::getStatus, statusList)
    );
}

问题:数据库查询逻辑泄露到 Service 层,字段名直接暴露在业务代码中,难以复用和测试。

MyBatisGX(DAO 层收敛):

// DAO 层
public interface UserDao extends SimpleDao<User, UserQuery, Long> {
    @Dynamic
    List<User> searchUsers(UserQuery query);
}

// Service 层
public List<User> searchUsers(Long id, String username, Integer minAge, List<Integer> statusList) {
    UserQuery query = new UserQuery();
    query.setId(id);
    query.setUsernameLike(username);
    query.setAgeGt(minAge);
    query.setStatusIn(statusList);
    
    return userDao.searchUsers(query);
}

持久化逻辑不泄露,Service 层只知道 DAO 接口。易于测试,可以 Mock UserDao。字段名变更只需修改 Entity,不影响 Service 层。


5.2 MyBatisGX 的独特优势总结

维度MyBatisMyBatis-PlusMyBatis-FlexMyBatisGX
性能★★★★★ (基准)★★★★☆★★★★☆★★★★★
简单查询代码量XML + 接口Wrapper 构建QueryWrapper仅方法名
动态查询XML <if> 标签Wrapper 动态构建QueryWrapperQueryEntity + @Dynamic
层次边界清晰持久化逻辑泄露持久化逻辑泄露清晰(DAO层收敛)
学习成本需学 XML 语法需学 Wrapper API需学 QueryWrapper API遵循命名约定
SQL 可控性完全可控Wrapper 生成QueryWrapper 生成预生成可查看 + XML 可覆盖
适用场景复杂 SQL / 性能敏感快速开发 / 动态查询灵活查询企业级应用 / 长期维护

5.3 为什么 MyBatisGX 不提供 Wrapper?

这是架构设计立场的问题。

MyBatisGX 的观点:查询本身是稳定的业务能力,而非一次性的实现细节。Service 层应该只表达业务流程,而不承担任何数据库查询语义。

MyBatisGX 通过方法语义 + QueryEntity 的方式,将所有查询能力收敛在 DAO 层,避免持久化逻辑对业务层的侵入。

对比:

Wrapper 方式(MyBatis-Plus / Flex):
Service 层 ──────┐
                 ↓(构建 Wrapper)
             Mapper 层 ──> 数据库
             
问题:持久化逻辑向上泄露

MyBatisGX 的方式:
Service 层 ──────┐
                 ↓(调用明确的方法)
             DAO 层 ──> 数据库
             
优势:层次边界清晰

六、一个有趣的现象:JVM 预热的重要性

测试数据显示,第一次执行通常明显更慢。

MyBatisGX 的单条插入:

[Insert][1] 首次执行:79,329 μs  (79ms)
[Insert][1]2次:1,675 μs       (1.6ms)
[Insert][1]3次:1,625 μs       (1.6ms)

首次执行是后续的 47 倍。

MyBatis-Flex 的单条插入:

[Insert][1] 首次执行:314,609 μs  (314ms)
[Insert][1]2次:3,124 μs       (3.1ms)

首次执行是后续的 100 倍。

所有框架都有这个现象。原因包括 JVM JIT 编译、类加载、Mapper 初始化、SQL 缓存建立、连接池预热。

启示:不要用首次执行结果评估性能。生产环境建议预热,应用启动后执行几轮空查询。性能测试必须热身,否则结果没有参考价值。


七、结论

这轮性能测试的结论:

1. 原生 MyBatis 依然是性能基准

大部分场景中,手写 SQL 的 MyBatis 性能都处于第一梯队。

2. MyBatisGX 在提供高抽象能力的同时,保持了接近原生的性能

热身后的数据:

  • 单条插入:仅慢 5%(92μs)
  • 批量插入 1 万条:仅慢 5%(16ms)
  • 动态更新:仅慢 16%(224μs)
  • 复杂查询:持平或更快

高抽象不等于高损耗。

3. MyBatis-Plus 和 MyBatis-Flex 的性能同样优秀

某些场景下比 MyBatis 慢 20%~50%,但绝对值依然很小(微秒级或毫秒级),绝大部分业务系统感知不到差异。

4. 真正决定性能的不是 ORM 框架

性能瓶颈通常在:

  • SQL 设计:JOIN 策略、子查询 vs 多次查询
  • 索引设计:WHERE 条件是否命中索引
  • 数据模型:表结构是否合理
  • 批量策略:单条 vs 批量操作

框架层的开销在总耗时中占比极小(通常 < 10%)。

5. 选择 ORM 时,更应该关注

可维护性、SQL 管理方式、团队开发体验、架构边界,而不是执着于几个百分点的框架开销。


八、写在最后

性能优化是系统工程,不是简单的框架选型问题。

如果系统遇到性能瓶颈,优先检查:慢 SQL(通过慢查询日志)、索引、N+1 查询、批量操作。解决这些问题后,再考虑是否需要"换一个更快的 ORM 框架"。

MyBatisGX 的设计理念:让代码不被数据库腐蚀。

持久层逻辑归于 DAO,业务逻辑留在 Service。这不仅是编码规范,更是架构立场。

大型项目和长期维护中,这种清晰的边界会带来价值。重构数据库时,Service 层代码无需修改。新人接手时,不需要理解 Wrapper 的构建细节。测试时,可以轻松 Mock DAO 接口。

测试结果显示,这个设计理念并没有以牺牲性能为代价。


项目地址:

欢迎下载源码自行测试,或在 GitHub 上提出你的问题和建议。