首页 > 编程语言 >MyBatis大数据查询优化技巧

MyBatis大数据查询优化技巧

来源:互联网 2026-07-18 08:09:08

针对MyBatis查询超万条数据性能问题,从fetchSize原理入手,提出六种优化方案:设置fetchSize减少网络往返、分页查询控制内存、游标流式处理大数据量、SQL层面只查必要字段及索引覆盖、数据库端索引与分区优化、异步查询与缓存策略,有效提升响应速度。

当 MyBatis 查询数据量超过万条时,响应速度的下降往往成为开发中的常见痛点——几万条数据等待几十秒甚至直接导致 OOM,这个技术难点如何解决?本文从 fetchSize 原理入手,深入拆解 MyBatis 大数据量查询优化方法,提供 6 套可落地的优化方案,覆盖 SQL 层面、JDBC 层面到架构层面的选择。

MyBatis大数据查询优化技巧

长期稳定更新的攒劲资源: >>>点此立即查看<<<

一、问题现象

1.1 典型场景

// 查询上万条数据,响应缓慢
List> list = userMapper.queryLargeData();
// 调用耗时:5s ~ 30s+
数据量默认配置耗时优化后耗时
1 万条~3s~0.3s
10 万条~30s~2s
100 万条超时/OOM~15s(需流式处理)

1.2 根因分析

很多人不知道,MyBatis 通过 JDBC 取数据时,默认每次只取 10 条(fetchSize = 10)。这意味着:

查询 1 万条数据 → 客户端与数据库往返 1000 次
查询 10 万条数据 → 客户端与数据库往返 10000 次

每次网络往返的延迟累积,耗时自然线性增长。找到病因,才能对症下药。

二、fetchSize 原理

2.1 JDBC 数据读取机制

┌──────────┐          ┌──────────┐          ┌──────────┐
│  客户端   │ ──query──│  数据库   │          │  数据库   │
│ (MyBatis)│ ──10条──│          │          │          │
│          │ ──next──│          │          │          │
│          │ ──10条──│          │          │          │
│          │  ......  │          │          │          │
└──────────┘          └──────────┘          └──────────┘
参数默认值含义
fetchSize10(Oracle)/ 不限制(MySQL)JDBC 每次从数据库读取的记录数
增大效果减少往返次数,提高吞吐量
减小效果降低单次内存占用,适合流式处理

2.2 对比数据

fetchSize1 万条往返次数10 万条往返次数
10(默认)1000 次10000 次
100100 次1000 次
100010 次100 次
100001 次10 次

数据一目了然:fetchSize 设为 10000,1 万条数据只需 1 次往返,效率直接提升 1000 倍。这就是 MyBatis 查询优化中第一个方案的底气。

三、方案一:设置 fetchSize(最直接)

3.1 XML Mapper 配置

SELECT * FROM your_table WHERE status = 'ACTIVE'

3.2 注解方式

@Options(fetchSize = 10000)
@Select("SELECT * FROM your_table WHERE status = 'ACTIVE'")
List> queryLargeData();

3.3 MyBatis-Plus 全局配置

mybatis-plus:
  configuration:
    default-fetch-size: 10000

或者在 Java 配置中:

@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
    factoryBean.setDataSource(dataSource);
    
    org.apache.ibatis.session.Configuration configuration = 
        new org.apache.ibatis.session.Configuration();
    configuration.setDefaultFetchSize(10000);
    factoryBean.setConfiguration(configuration);
    
    return factoryBean.getObject();
}

3.4 注意事项

关注点说明
内存占用fetchSize 越大,单次内存消耗越高,避免超过 JVM 堆内存
推荐值一般不超过 10000,建议 1000~5000 之间
数据库差异Oracle 默认 10,MySQL 驱动默认不限制(全量读取)
大字段影响包含 BLOB/TEXT 字段时,适当降低 fetchSize

看到这里先别急着把值设到 10 万——内存是会爆的,后面会讲如何估算。

四、方案二:分页查询(推荐,通用)

4.1 MyBatis-Plus 分页

// 分页查询,每页 1000 条
public void queryByPage() {
    int pageSize = 1000;
    int current = 1;
    long total;
    
    do {
        Page> page = new Page<>(current, pageSize);
        Page> result = userMapper.selectPage(page, null);
        
        List> records = result.getRecords();
        // 处理当前页数据
        processBatch(records);
        
        total = result.getTotal();
        current++;
    } while ((current - 1) * pageSize < total);
}

4.2 手写分页 SQL

public void queryByPageManual() {
    int pageSize = 1000;
    int offset = 0;
    List> batch;
    
    do {
        batch = userMapper.queryByPage(offset, pageSize);
        processBatch(batch);
        offset += pageSize;
    } while (batch.size() == pageSize);
}

4.3 分页 vs fetchSize 对比

方案优点缺点
fetchSize一次 SQL,减少数据库负载内存消耗集中
分页查询内存可控,适合大数据量多次 SQL,总耗时可能更长

分页查询最大的好处是内存可控,对于没有特殊要求的业务场景,这是 MyBatis 大数据量查询中最稳妥的选择。

五、方案三:游标/流式查询(适合大数据量导出)

5.1 MyBatis 游标查询

@Select("SELECT * FROM your_table WHERE status = 'ACTIVE'")
@Options(fetchSize = Integer.MIN_VALUE)  // MySQL 流式读取
Cursor> queryLargeDataCursor();
// 使用游标逐条处理,内存友好
try (Cursor> cursor = userMapper.queryLargeDataCursor()) {
    cursor.forEach(row -> {
        // 逐行处理,内存仅保留一条数据
        processRow(row);
    });
}

5.2 MyBatis-Plus 流式查询

@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) {
    // ... 配置
    configuration.setDefaultFetchSize(1000);  // 不影响游标模式
    return factoryBean.getObject();
}
// 使用 MyBatis-Plus 的流式查询
public void streamQuery() {
    userMapper.selectList(
        new LambdaQueryWrapper()
            .eq(User::getStatus, "ACTIVE"),
        resultContext -> {
            // 每行回调处理
            User user = resultContext.getResultObject();
            processUser(user);
        }
    );
}

5.3 MySQL 游标的特殊要求

MySQL 使用游标需要设置:

@Options(fetchSize = Integer.MIN_VALUE)  // 告诉 MySQL 驱动使用流式

或者 JDBC URL 配置:

jdbc:mysql://localhost:3306/dbuseCursorFetch=true&defaultFetchSize=1000

游标模式在处理百万级数据导出时是首选,注意连接会长时间占用,处理完务必关闭(建议 try-with-resources)。

六、方案四:SQL 层面优化

6.1 只查询必要字段




6.2 使用索引覆盖

-- 确保查询的字段都在索引中,避免回表
-- 创建联合索引
CREATE INDEX idx_status_create ON your_table(status, create_time) INCLUDE (id, name);

-- 查询时只走索引,不访问表数据
SELECT id, name, status, create_time 
FROM your_table 
WHERE status = 'ACTIVE'
ORDER BY create_time;

6.3 避免 JOIN 过多表





SQL 优化是 MyBatis 查询优化中的基础功,很多时候把 SELECT * 改成指定字段就能省下 30% 的传输时间。

七、方案五:数据库端优化

7.1 合理使用索引

-- 查看查询计划
EXPLAIN SELECT * FROM your_table WHERE status = 'ACTIVE' ORDER BY create_time;

-- 关键指标
-- type: ALL(全表扫描)→ ref/range(索引扫描)
-- rows: 扫描行数(越小越好)
-- Extra: Using filesort(需要优化排序)

7.2 分区表

对于超大规模数据(百万级以上),考虑表分区:

-- 按时间范围分区
CREATE TABLE your_table (
    id BIGINT,
    name VARCHAR(100),
    create_time DATETIME
) PARTITION BY RANGE (YEAR(create_time)) (
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

7.3 批量处理代替逐条操作

--  逐条 UPDATE(N 次 SQL)
UPDATE table SET status = 'DONE' WHERE id = 1;
UPDATE table SET status = 'DONE' WHERE id = 2;
-- ... N 次

--  批量 UPDATE(1 次 SQL)
UPDATE table SET status = 'DONE' WHERE id IN (1, 2, 3, ..., 1000);

数据库层的优化往往能带来意想不到的收益,特别是分区表和索引覆盖,长期业务值得投入。

八、方案六:异步 + 缓存策略

8.1 异步查询

@Service
public class ReportService {
    
    @Async
    public CompletableFuture>> generateReportAsync() {
        List> data = userMapper.queryLargeData();
        return CompletableFuture.completedFuture(data);
    }
}

8.2 缓存中间结果

@Service
public class CachedQueryService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    // 缓存查询结果,设置过期时间
    public List> queryWithCache() {
        String cacheKey = "large:data:active";
        
        // 1. 尝试从缓存获取
        List> cached = (List>) 
            redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return cached;
        }
        
        // 2. 缓存未命中,查询数据库
        List> data = userMapper.queryLargeData();
        
        // 3. 写入缓存,设置 5 分钟过期
        redisTemplate.opsForValue().set(cacheKey, data, 5, TimeUnit.MINUTES);
        
        return data;
    }
}

8.3 数据预热

@Component
public class DataWarmer implements CommandLineRunner {
    
    @Autowired
    private CachedQueryService cachedQueryService;
    
    @Override
    public void run(String... args) {
        // 应用启动时预热数据
        cachedQueryService.queryWithCache();
        log.info("Large data cache warmed up");
    }
}

对于读多写少的场景,缓存是最立竿见影的方法。配合预热,用户几乎感受不到数据库的延迟。

九、方案对比与推荐

方案适用场景性能提升实现难度风险
① fetchSize万级数据,单次查询★★★★★★☆☆☆☆内存占用增加
② 分页查询任意数据量★★★★☆★★☆☆☆多次 SQL 开销
③ 游标查询十万级以上导出★★★★☆★★★☆☆连接长时间占用
④ SQL 优化所有查询★★★☆☆★★☆☆☆索引维护成本
⑤ 数据库优化长期稳定业务★★★☆☆★★★★☆架构改造成本高
⑥ 异步+缓存读多写少场景★★★★★★★★☆☆缓存一致性

推荐最佳实践流程

┌──────────────┐
│  查询数据量大  │
└──────┬───────┘
       ▼
┌──────────────────┐
│  是否需要实时数据? │
└──────┬──────┬─────┘
       │      │
    是(实时)  否(缓存)
       │      │
       ▼      ▼
┌────────────┐ ┌────────────┐
│ 万级数据:  │ │ 缓存预热   │
│ fetchSize  │ │ + 异步刷新 │
│ = 5000     │ └────────────┘
├────────────┤
│ 十万级数据: │
│ 游标流式    │
├────────────┤
│ 百万级数据: │
│ 分页+索引   │
└────────────┘

十、常见问题 FAQ

Q:fetchSize 设为 100000 会不会更快?

A:不一定。过大可能导致内存溢出(OOM),建议根据单行数据大小估算:

内存占用 ≈ fetchSize × 单行数据大小
例如:fetchSize=10000, 单行1KB → 约 10MB 内存占用

Q:MySQL 默认 fetchSize 是多少?

A:MySQL JDBC 驱动默认不限制,一次读取全部结果到客户端内存。如果不设 fetchSize,MySQL 反而可能更容易 OOM。

Q:游标查询有什么缺点?

A:游标会长时间占用数据库连接,如果处理慢可能导致连接超时。使用后务必关闭 Cursor(建议 try-with-resources)。

Q:分页查询深度分页(OFFSET 大)慢怎么办?

A:改用游标分页(基于上次查询的最后一个 ID):

--  传统分页(深度分页慢)
SELECT * FROM table ORDER BY id LIMIT 100000, 10;
--  游标分页(走索引)
SELECT * FROM table WHERE id > 100000 ORDER BY id LIMIT 10;

Q:MyBatis-Plus 的分页和 fetchSize 能一起用吗?

A:可以,但分页本身已经控制了数据量,fetchSize 作用不大。一般分页不设 fetchSize,大批量导出才设。

十一、总结

核心要点说明
默认 10 条往返JDBC 默认 fetchSize=10,万条数据往返 1000 次
设 fetchSize万级数据最直接,推荐 5000~10000
分页查询通用方案,内存友好
游标流式十万级以上数据导出首选
SQL 优化覆盖索引、避免 SELECT *、减少 JOIN
缓存策略读多写少场景,结合 Redis 缓存中间结果

一句话总结:

万级数据用 fetchSize,十万级用分页,百万级用游标 + 索引优化,高频查询加缓存。

侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述

热游推荐

更多
湘ICP备14008430号-1 湘公网安备 43070302000280号
All Rights Reserved
本站为非盈利网站,不接受任何广告。本站所有软件,都由网友
上传,如有侵犯你的版权,请发邮件给xiayx666@163.com
抵制不良色情、反动、暴力游戏。注意自我保护,谨防受骗上当。
适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。