针对MyBatis查询超万条数据性能问题,从fetchSize原理入手,提出六种优化方案:设置fetchSize减少网络往返、分页查询控制内存、游标流式处理大数据量、SQL层面只查必要字段及索引覆盖、数据库端索引与分区优化、异步查询与缓存策略,有效提升响应速度。
当 MyBatis 查询数据量超过万条时,响应速度的下降往往成为开发中的常见痛点——几万条数据等待几十秒甚至直接导致 OOM,这个技术难点如何解决?本文从 fetchSize 原理入手,深入拆解 MyBatis 大数据量查询优化方法,提供 6 套可落地的优化方案,覆盖 SQL 层面、JDBC 层面到架构层面的选择。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
// 查询上万条数据,响应缓慢 List
| 数据量 | 默认配置耗时 | 优化后耗时 |
|---|---|---|
| 1 万条 | ~3s | ~0.3s |
| 10 万条 | ~30s | ~2s |
| 100 万条 | 超时/OOM | ~15s(需流式处理) |
很多人不知道,MyBatis 通过 JDBC 取数据时,默认每次只取 10 条(fetchSize = 10)。这意味着:
查询 1 万条数据 → 客户端与数据库往返 1000 次 查询 10 万条数据 → 客户端与数据库往返 10000 次
每次网络往返的延迟累积,耗时自然线性增长。找到病因,才能对症下药。
┌──────────┐ ┌──────────┐ ┌──────────┐ │ 客户端 │ ──query──│ 数据库 │ │ 数据库 │ │ (MyBatis)│ ──10条──│ │ │ │ │ │ ──next──│ │ │ │ │ │ ──10条──│ │ │ │ │ │ ...... │ │ │ │ └──────────┘ └──────────┘ └──────────┘
| 参数 | 默认值 | 含义 |
|---|---|---|
| fetchSize | 10(Oracle)/ 不限制(MySQL) | JDBC 每次从数据库读取的记录数 |
| 增大效果 | — | 减少往返次数,提高吞吐量 |
| 减小效果 | — | 降低单次内存占用,适合流式处理 |
| fetchSize | 1 万条往返次数 | 10 万条往返次数 |
|---|---|---|
| 10(默认) | 1000 次 | 10000 次 |
| 100 | 100 次 | 1000 次 |
| 1000 | 10 次 | 100 次 |
| 10000 | 1 次 | 10 次 |
数据一目了然:fetchSize 设为 10000,1 万条数据只需 1 次往返,效率直接提升 1000 倍。这就是 MyBatis 查询优化中第一个方案的底气。
在 标签上加一个 fetchSize 属性即可:
@Options(fetchSize = 10000)
@Select("SELECT * FROM your_table WHERE status = 'ACTIVE'")
List
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();
}
| 关注点 | 说明 |
|---|---|
| 内存占用 | fetchSize 越大,单次内存消耗越高,避免超过 JVM 堆内存 |
| 推荐值 | 一般不超过 10000,建议 1000~5000 之间 |
| 数据库差异 | Oracle 默认 10,MySQL 驱动默认不限制(全量读取) |
| 大字段影响 | 包含 BLOB/TEXT 字段时,适当降低 fetchSize |
看到这里先别急着把值设到 10 万——内存是会爆的,后面会讲如何估算。
// 分页查询,每页 1000 条
public void queryByPage() {
int pageSize = 1000;
int current = 1;
long total;
do {
Page
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);
}
| 方案 | 优点 | 缺点 |
|---|---|---|
| fetchSize | 一次 SQL,减少数据库负载 | 内存消耗集中 |
| 分页查询 | 内存可控,适合大数据量 | 多次 SQL,总耗时可能更长 |
分页查询最大的好处是内存可控,对于没有特殊要求的业务场景,这是 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); }); }
@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);
}
);
}
MySQL 使用游标需要设置:
@Options(fetchSize = Integer.MIN_VALUE) // 告诉 MySQL 驱动使用流式
或者 JDBC URL 配置:
jdbc:mysql://localhost:3306/dbuseCursorFetch=true&defaultFetchSize=1000
游标模式在处理百万级数据导出时是首选,注意连接会长时间占用,处理完务必关闭(建议 try-with-resources)。
-- 确保查询的字段都在索引中,避免回表 -- 创建联合索引 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;
SQL 优化是 MyBatis 查询优化中的基础功,很多时候把 SELECT * 改成指定字段就能省下 30% 的传输时间。
-- 查看查询计划 EXPLAIN SELECT * FROM your_table WHERE status = 'ACTIVE' ORDER BY create_time; -- 关键指标 -- type: ALL(全表扫描)→ ref/range(索引扫描) -- rows: 扫描行数(越小越好) -- Extra: Using filesort(需要优化排序)
对于超大规模数据(百万级以上),考虑表分区:
-- 按时间范围分区
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
);
-- 逐条 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);
数据库层的优化往往能带来意想不到的收益,特别是分区表和索引覆盖,长期业务值得投入。
@Service
public class ReportService {
@Async
public CompletableFuture>> generateReportAsync() {
List> data = userMapper.queryLargeData();
return CompletableFuture.completedFuture(data);
}
}
@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;
}
}
@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 │ └────────────┘
├────────────┤
│ 十万级数据: │
│ 游标流式 │
├────────────┤
│ 百万级数据: │
│ 分页+索引 │
└────────────┘
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,十万级用分页,百万级用游标 + 索引优化,高频查询加缓存。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述