AutoPOI 大数据导出优化方案
问题背景
当数据表中有 40万+ 条数据,使用传统的 exportBigExcel 函数进行分页导出会遇到严重的性能问题:
根本原因:
exportBigExcel中使用传统分页方式查询数据,但是传统分页方式存在性能衰减问题
使用 LIMIT offset, size 方式进行分页时,MySQL需要:
- 扫描前 offset + size 行数据
- 丢弃前 offset 行
- 返回后 size 行
-- 第1页: 扫描10,000行
SELECT * FROM sys_log ORDER BY create_time desc LIMIT 0, 10000;
-- 第10页: 扫描100,000行,丢弃前90,000行
SELECT * FROM sys_log ORDER BY create_time desc LIMIT 90000, 10000;
-- 第40页: 扫描400,000行,丢弃前390,000行
SELECT * FROM sys_log ORDER BY create_time desc LIMIT 390000, 10000;
查询时间(ms)
^
10000| ●
| ●
5000| ●
| ●
2000| ●
| ●
1000| ●
| ●
500| ●
| ●
200| ●
| ●
50|●
+-------------------------------------------------> 页码
1 5 10 15 20 25 30 35 40
图1: 传统LIMIT分页性能衰减曲线
解决方案
引入 游标分页(Cursor-based Pagination) 机制,使用上一批次的最后一条记录作为查询起点,避免深分页问题。
-- 第1页: 使用索引快速定位
SELECT * FROM sys_log WHERE create_time <= 2025-11-04 10:55:22 ORDER BY create_time desc LIMIT 10000;
-- 第10页: 仍然使用索引快速定位
SELECT * FROM sys_log WHERE create_time > 2025-11-02 10:55:22 ORDER BY create_time desc LIMIT 10000;
-- 第40页: 依然使用索引快速定位
SELECT * FROM sys_log WHERE create_time > 2025-11-01 10:55:22 ORDER BY create_time desc LIMIT 10000;
查询时间(ms)
^
10000|
|
5000|
|
2000|
|
1000|
|
500|
|
200|
|
50|● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ●
+-------------------------------------------------> 页码
1 5 10 15 20 25 30 35 40
图2: 游标分页性能恒定曲线
使用方法
导出接口手动实现游标查询方法
import org.jeecgframework.poi.handler.inter.IExcelExportServerEnhanced;
public class SysLogController extends JeecgController<SysLog, ISysLogService> {
@Autowired
private ISysLogService sysLogService;
// .........
/**
* 导出excel
* @param request
* @param syslog
*/
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, SysLog syslog) {
// 组装查询条件
QueryWrapper<SysLog> queryWrapper = QueryGenerator.initQueryWrapper(syslog, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
// 过滤选中数据
String selections = request.getParameter("selections");
if (oConvertUtils.isNotEmpty(selections)) {
List<String> selectionList = Arrays.asList(selections.split(","));
queryWrapper.in("id", selectionList);
}
// ************* 重点:实现IExcelExportServerEnhanced接口,在其中实现游标分页逻辑 ****************
// 定义IExcelExportServer
IExcelExportServerEnhanced<SysLog> excelExportServer = new IExcelExportServerEnhanced<>() {
@Override
public List<SysLog> selectListForExcelExport(Object queryParams, SysLog lastRecord, int pageSize) {
Date startDate = new Date();
if (lastRecord != null) {
startDate = lastRecord.getCreateTime();
}
QueryWrapper<SysLog> queryWrapper = (QueryWrapper<SysLog>) queryParams;
queryWrapper.le("create_time", startDate);
queryWrapper.orderByDesc("create_time");
queryWrapper.last("LIMIT " + pageSize);
log.info("本次导出起始时间:" + startDate + ", 数量:" + pageSize);
return service.list(queryWrapper);
}
@Override
public int getPageSize() {
return 10000;
}
};
//*****************************************************************************************
String title = "系统日志";
// AutoPoi 导出Excel
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
//此处设置的filename无效 ,前端会重更新设置一下
mv.addObject(NormalExcelConstants.FILE_NAME, title);
mv.addObject(NormalExcelConstants.CLASS, SysLog.class);
ExportParams exportParams = new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, jeecgBaseConfig.getPath().getUpload());
mv.addObject(NormalExcelConstants.PARAMS, exportParams);
mv.addObject(NormalExcelConstants.EXPORT_SERVER, excelExportServer);
mv.addObject(NormalExcelConstants.QUERY_PARAMS, queryWrapper);
return mv;
}
}
重要注意事项
1. 索引要求 ⚠️
必须确保游标字段有索引!
2. 游标字段选择
推荐使用的游标字段:
- 有序的主键id(需要数字类型)
- 有序的时间戳字段 (如创建时间)
- 任何递增且唯一的字段
不推荐:
- 无序字段
- 可能重复的字段
- 可能为NULL的字段