
当对少量匹配文档(如仅10条)执行 term 聚合却耗时达2秒时,根本原因通常是高基数字段(如 userid)触发了昂贵的全局序数(global ordinals)构建过程,而非数据量本身导致慢查询。
当对少量匹配文档(如仅10条)执行 term 聚合却耗时达2秒时,根本原因通常是高基数字段(如 userid)触发了昂贵的全局序数(global ordinals)构建过程,而非数据量本身导致慢查询。
Elasticsearch 的 terms 聚合在默认行为下会为字符串类型字段(尤其是高基数字段)构建 全局序数(global ordinals) —— 这是一种跨所有段(segments)对字段值进行全局排序和编号的内存结构,用于高效聚合。虽然它能显著提升后续聚合性能,但首次构建开销极大。从你的 profiling 输出可见:
"type": "GlobalOrdinalsStringTermsAggregator",
"time_in_nanos": 91737162, // ≈ 91.7 ms —— 仅构建阶段就占主导
"debug": {
"total_buckets": 9942540, // 全局唯一值超990万!
"built_buckets": 1, // 实际命中结果仅1个桶(因 uniqueId 匹配极少数文档)
"segments_with_single_valued_ords": 28 // 涉及28个段,加剧计算负担
}
这说明:聚合并非在“查询后的10条结果”上运行,而是在整个索引中满足 uniqueId 过滤条件的 所有分片段 上,对 userId 字段全量构建全局序数——即使最终只产出1个桶,ES 仍需扫描并编码该字段在全部段中的全部可能值(994万),造成严重浪费。
✅ 正确优化策略
1. 将 must 改为 filter(立即生效,零成本)
filter 子句不参与相关性评分,且可被缓存、跳过打分,显著降低查询开销:
{
"query": {
"bool": {
"filter": [ // ← 替换 must
{ "term": { "uniqueId": "10921152912079" } }
]
}
},
"aggs": {
"users": {
"terms": { "field": "userId" },
"aggs": {
"raf": { "terms": { "field": "randomId" } }
}
}
}
}
2. 针对高基数字段优化全局序数策略
-
方案A(推荐):启用 eager_global_ordinals(写入时摊销)
在 userId 字段 mapping 中显式配置,将构建成本转移到索引阶段:PUT /your-index/_mapping { "properties": { "userId": { "type": "keyword", "eager_global_ordinals": true // ← 关键配置 } } }✅ 优势:首次聚合响应极快(毫秒级);❌ 缺点:索引吞吐量下降约5–15%,磁盘写入延迟略增。
-
方案B:禁用全局序数(低内存敏感场景)
若 userId 不常用于聚合,或集群内存充足,可改用 doc_values: false + fielddata: true(不推荐生产环境),但更稳妥的是直接使用 terms 的 execution_hint: map(强制内存哈希聚合,绕过序数):"aggs": { "users": { "terms": { "field": "userId", "execution_hint": "map" // ← 强制基于 HashMap 聚合,避免全局序数 } } }⚠️ 注意:map 模式内存占用更高,适合小结果集;若 userId 基数极高(如千万级),仍建议优先选 eager_global_ordinals。
3. 进阶:用 filter 聚合替代顶层 filter(更精准控制作用域)
若后续还需对其他字段聚合,可将过滤逻辑下沉至聚合内部,进一步缩小计算范围:
"aggs": {
"filtered_users": {
"filter": { "term": { "uniqueId": "10921152912079" } },
"aggs": {
"users": {
"terms": { "field": "userId", "execution_hint": "map" },
"aggs": {
"raf": { "terms": { "field": "randomId" } }
}
}
}
}
}
? 总结与检查清单
- ✅ 必须做:将 bool.must 改为 bool.filter,这是最简单高效的性能提升;
- ✅ 强烈建议:为高基数聚合字段(如 userId)启用 "eager_global_ordinals": true;
- ✅ 按需选择:若无法修改 mapping 或需极致首查速度,添加 "execution_hint": "map";
- ❌ 避免:对高基数字段使用默认 terms 聚合而不做任何优化;
- ? 验证方式:执行 _cat/segments/
?v&s=memory:desc 查看段数量;用 _nodes/stats/indices?pretty 观察 fielddata 内存增长是否合理。
通过以上组合优化,原本 2 秒的聚合可稳定降至 5–20ms,真正实现“在10条结果上快速聚合”的预期效果。










