跳到主要内容

AutoPOI 大数据导出优化方案

问题背景

当数据表中有 40万+ 条数据,使用传统的 exportBigExcel 函数进行分页导出会遇到严重的性能问题:

根本原因:

exportBigExcel中使用传统分页方式查询数据,但是传统分页方式存在性能衰减问题

使用 LIMIT offset, size 方式进行分页时,MySQL需要:

  1. 扫描前 offset + size 行数据
  2. 丢弃前 offset 行
  3. 返回后 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的字段