在日常后端开发中,只要涉及日志检索、订单大数据列表、业务统计报表,基本都会用到 Elasticsearch。相比于MySQL,ES在全文检索、海量数据筛选上优势非常大。
但很多朋友上线后会遇到一个很头疼的问题:首页浅分页秒开,一旦翻到几十页之后,接口直接超时;简单查询很快,带聚合统计的报表直接卡死。
我前段时间负责公司日志平台优化,原始数据量达到千万级别,项目初期直接用默认from+size分页、普通agg聚合,深度分页直接报错、聚合查询耗时飙升到数秒。踩了很多坑之后,终于整理出一套稳定落地的ES提速方案。
今天就结合真实线上问题,分享ES海量数据下深度分页、聚合查询的完整优化思路,附带错误代码和优化后可直接上线的Java代码,帮大家彻底解决ES慢查询问题。

一、先说说为什么ES深度分页会超时?
很多新手习惯沿用MySQL思维,直接用 from + size 实现分页,在数据量小的时候完全没问题。但在海量数据下,这种写法堪称性能杀手。
ES是分布式架构,深度分页时,需要在所有分片上查询前N条数据、做全局排序、合并结果,再截断返回。页码越深,需要排序和合并的数据量越大,内存开销极高。
这也是为什么ES官方会限制默认最大分页深度,深度过大会直接抛出 Result window is too large 异常,不仅慢,还极易引发OOM风险。
二、线上原始低效代码(典型踩坑案例)
下面是我之前项目的原始写法,也是网上很多教程的通用错误示范,浅分页没问题,深度分页直接崩:
// 【错误写法:from+size 不支持深度分页】 public List<LogDTO> getLogPage(int pageNum, int pageSize){ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 普通条件查询 queryBuilder.withQuery(QueryBuilders.matchQuery("type","operation")); // 传统分页 int from = (pageNum - 1) * pageSize; queryBuilder.withPageable(PageRequest.of(from, pageSize)); SearchHits<LogDTO> hits = elasticsearchRestTemplate.search( queryBuilder.build(), LogDTO.class); return hits.stream().map(hit -> hit.getContent()).collect(Collectors.toList()); }
测试发现:pageNum超过50页之后,接口响应从几十毫秒直接涨到2秒以上,页码越大越慢,完全无法满足后台列表翻页需求。
三、深度分页最优方案:Scroll + SearchAfter 实战优化
针对海量数据分页,生产环境主流且稳定的方案就是 SearchAfter。它不依赖偏移量,通过上一页最后一条数据的排序值向后遍历,没有深度损耗,越翻页速度越稳定。
适合后台大数据列表、批量数据导出、日志查询场景,我给大家贴出精简可复用代码:
// 优化后:SearchAfter 深度分页 public List<LogDTO> searchAfterPage(Long lastTime, int pageSize){ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); queryBuilder.withQuery(QueryBuilders.matchQuery("type","operation")); // 必须设置排序字段(唯一有序字段,建议时间+ID) SortBuilders.FieldSortBuilder sort = SortBuilders.fieldSort("createTime").order(SortOrder.DESC); queryBuilder.withSort(sort); // 传递上一页最后一条排序值 if(lastTime != null){ queryBuilder.withSearchAfter(Collections.singletonList(lastTime)); } queryBuilder.withPageable(PageRequest.of(0, pageSize)); SearchHits<LogDTO> hits = elasticsearchRestTemplate.search( queryBuilder.build(), LogDTO.class); return hits.stream().map(hit -> hit.getContent()).collect(Collectors.toList()); }
优化后无论翻到多少页,响应速度始终稳定在100ms以内,彻底解决深度分页超时问题。
这里提醒一句:SearchAfter不适合跳页查询,只适合下拉滚动翻页、顺序遍历,这也是大数据列表的主流交互方式。
四、聚合查询卡顿解决方案
除了分页问题,聚合统计也是ES的性能重灾区。很多人直接写多层聚合、超大范围时间聚合,导致报表查询卡死。
我优化线上报表的核心思路:缩小查询范围、拆分聚合层级、开启字段预聚合、禁止深度排序聚合。
简单实用的聚合优化代码:
// 分组聚合查询优化 public Map<String,Long> logAggSearch(){ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 时间范围过滤,缩小聚合扫描区间(核心优化点) QueryBuilder timeQuery = QueryBuilders.rangeQuery("createTime").gte("now-7d"); queryBuilder.withQuery(timeQuery); // 单层简单聚合,避免多层嵌套 TermsAggregationBuilder agg = AggregationBuilders.terms("group_by_user") .field("userId.keyword") .size(100); queryBuilder.addAggregation(agg); // 关闭结果集,只需要聚合不需要列表 queryBuilder.withPageable(PageRequest.of(0,0)); SearchHits<LogDTO> hits = elasticsearchRestTemplate.search( queryBuilder.build(), LogDTO.class); // 解析聚合数据... return new HashMap<>(); }
五、额外几条生产级优化小技巧
1、聚合字段务必设置为 keyword 类型,文本类型聚合会极大拖慢速度; 2、超大报表统计不要实时查ES,建议定时同步到MySQL做统计展示; 3、高频查询字段建立索引,不需要检索的字段关闭存储; 4、避免模糊匹配 *xxx 前置通配符,会导致索引失效。
总结
ES海量数据慢查询,90%都是用法问题,而非组件性能问题。传统from+size只适合前台浅分页展示,深度分页一定要用SearchAfter,报表聚合一定要缩小范围、精简层级。
这套优化方案我已经在线上千万级数据量验证过,分页、聚合速度提升非常明显,完全可以直接复用在自己的项目中,轻松解决ES接口超时、报表卡顿难题。