从字段类型选择、查询语法优化、翻页策略、结果集控制四个维度梳理 ES 查询性能最佳实践,并给出各类型查询的性能基准数据。
ES 系列:ES 简介 · ES 的查询过程分析 · ES 高风险查询识别与规避 · ES 集群与分片配置最佳实践
目录
| 章节 | 说明 |
|---|---|
| 查询耗时拆解 | 了解耗时在哪里,才能有针对性优化 |
| 字段类型对查询的影响 | keyword vs 数值,term vs range |
| 查询语法优化 | filter cache、避免危险查询 |
| 翻页策略 | from+size、scroll、search_after |
| 结果集控制 | 控制返回字段、控制 size |
| 聚合查询优化 | 嵌套深度、terms size 控制 |
| 机器与集群配置 | 内存、CPU、预热缓存 |
| 查询性能基准参考 | Term/Match/聚合 QPS 参考值 |
查询耗时拆解
一次 ES 查询的主要耗时分布:
结论:ES 查询的主要耗时在数据节点。优化方向是减少数据节点需要遍历的数据量和计算量。
字段类型对查询的影响
keyword vs 数值类型的 term 查询
| 类型 | term 查询性能 | range 查询性能 | 适用场景 |
|---|---|---|---|
keyword |
✅ 最优(倒排索引) | ❌ 不适合 | 精确匹配、枚举值、ID |
long / integer |
❌ 性能差(BKD 树不适合精确查找) | ✅ 最优(BKD 树) | 范围查询、排序、聚合 |
⚠️ 数字类型做 term 查询性能非常差。ES 5.x 起相比 2.x 下降约 80%,CPU 利用率飙升 30%+。不需要范围查询时,一律用 keyword。
// 错误:数字类型做 term 查询
{ "term": { "userId": 12345 }}
// 正确:精确匹配用 keyword 类型存储
{ "term": { "userId": "12345" }}
整值型 vs 范围型字段
整值型(double、long、integer、short、byte)
→ 底层 BKD 树,适合 range 查询
范围型(float_range、long_range、date_range、double_range)
→ 存储一个区间,用于"文档区间是否包含查询值"场景
查询语法优化
优先使用 filter(利用 Query Cache)
// 推荐:bool-filter,结果会被缓存
{
"query": {
"bool": {
"filter": [
{ "term": { "status": "active" }},
{ "range": { "price": { "lte": 100 }}}
],
"must": [
{ "match": { "title": "手机" }}
]
}
}
}
filter子句的结果会缓存在 Query Cache,相同查询第二次极快must参与评分(计算相关度),不被缓存- 能用 filter 的条件就不要放在 must 里
避免模糊查询或严格限制
| 查询类型 | 危险程度 | 替代方案 |
|---|---|---|
wildcard(前后通配 *词*) |
🔴 极危险 | 改用 match 分词查询 |
regexp(正则) |
🔴 极危险 | 提前计算好存入字段 |
fuzzy |
🟡 较危险 | 控制 fuzziness 参数 |
prefix |
🟡 注意 | 词长不超过 32 字符 |
模糊查询的危险原因:wildcard、regexp、fuzzy 内部都需要构建有穷自动机(DFA),词越长、状态数越多,CPU 直接被打满,集群不可用。
// ❌ 危险:前后通配
{ "wildcard": { "name": { "value": "*手机*" }}}
// ✅ 安全:分词查询(性能好 1 倍以上)
{ "match": { "name": "手机" }}
// ✅ 也可以用 wildcard 字段类型(ES 7.9+),性能更好
避免深度嵌套查询
// ❌ 避免:nested 查询
{
"query": {
"nested": {
"path": "comments",
"query": { "term": { "comments.author": "alice" }}
}
}
}
- nested 慢 10 倍以上,parent-child 慢 100 倍以上
- 尽量在写入时把数据打平,join 逻辑放到应用层
避免 script 查询
// ❌ 避免:查询时用脚本计算
{
"query": {
"script": {
"script": "doc['price'].value * 0.9 > 50"
}
}
}
// ✅ 提前计算好存入 ES
{ "range": { "discount_price": { "gt": 50 }}}
不要用 match_all 无条件查询
// ❌ 极度危险:全量扫描
{ "query": { "match_all": {} }, "sort": [{ "createTime": "desc" }]}
翻页策略
| 方案 | 适用场景 | 上限 | 特点 |
|---|---|---|---|
from + size |
用户翻页(前 N 页) | 10000 | 简单,但深翻页时内存消耗大 |
scroll |
批量导出/遍历全量数据 | 无上限 | 有状态,不适合实时查询 |
search_after |
实时翻页(无随机跳页需求) | 无上限 | 无状态,高效,推荐 |
from + size 的问题
from=9000, size=100 → 每个分片返回 9100 条 → 协调节点汇总后取 100 条
分片数越多,内存开销越大,是引发 Full GC 的常见原因
规则:
from + size不得超过 10000,size单次不超过 1000。
search_after 示例
// 第一页
{
"size": 20,
"query": { "match_all": {} },
"sort": [{ "createTime": "desc" }, { "_id": "asc" }]
}
// 后续页:传入上一页最后一条的 sort 值
{
"size": 20,
"query": { "match_all": {} },
"sort": [{ "createTime": "desc" }, { "_id": "asc" }],
"search_after": [1704067200000, "doc_id_xyz"]
}
结果集控制
只返回需要的字段
{
"_source": {
"includes": ["id", "name", "price"],
"excludes": ["*.description", "raw_content"]
},
"query": { "term": { "status": "active" }}
}
- 减少网络传输量和反序列化开销
- 对 IO 密集型查询效果显著
控制 terms/should 参数规模
// ❌ terms 参数过多
{ "terms": { "skuId": [1001, 1002, ..., 10000] }}
terms查询的入参 不超过 100 个(推荐),上限 1024 个(超过将被拦截)should子查询数量 不超过 1024 个
别名查询定期清理
- 别名绑定的索引数量 不超过 20 个
- 定期解绑历史索引,否则查询会遍历过多分片
聚合查询优化
嵌套深度控制
// ❌ 危险:三层以上嵌套聚合
{
"aggs": {
"by_date": {
"terms": { "field": "date" },
"aggs": {
"by_supplier": {
"terms": { "field": "supplierId" },
"aggs": {
"by_sku": {
"terms": { "field": "skuId" },
"aggs": { ... } // 第四层,危险!
}
}
}
}
}
}
}
- 聚合嵌套 不超过 3 层,深度越深 CPU 和 IO 消耗越大
terms聚合的size不超过 65536,shard_size同样限制
预计算优化
- 对 text 字段做聚合时,设置
fielddata: true(会加载到内存,谨慎使用) - 优先用
keyword字段做聚合,避免 fielddata
合理利用 index sorting
// 索引创建时配置排序,对按该字段排序的查询有显著加速
PUT /my_index
{
"settings": {
"index.sort.field": "createTime",
"index.sort.order": "desc"
}
}
机器与集群配置
JVM Heap 配置原则
- JVM Heap 设置为机器内存的 50%,剩余 50% 留给操作系统文件系统缓存(Lucene 依赖 OS Cache)
- Heap 不超过 32 GB(超过 32 GB JVM 指针压缩失效,反而更慢)
- 数据节点内存建议 16G 以上
机器内存 64G → JVM Heap 32G → OS Cache 32G
预热文件系统缓存
# elasticsearch.yml 或 index settings
index.store.preload: ["nvd", "dvd"]
nvd:norms(相关度评分数据)dvd:doc values(列式存储,聚合/排序需要)- 服务重启后预先将段文件加载到 OS Cache,减少冷启动时的查询抖动
分片均衡
- 确保每个数据节点都分配了某个索引的分片(查询并发遍历各节点)
- 设置单节点最大分片数,避免分片不均:
PUT /my_index/_settings
{
"index.routing.allocation.total_shards_per_node": 2
}
冷热数据隔离
- 历史数据(访问热度低)迁移到冷集群或 SATA 机器
- 高频访问数据保留在 SSD 集群
查询性能基准参考
以下为索引 50 GB、文档数 2 亿+、字段数 8 个的简单查询压测值,单个数据节点。
| 机器类型 | 配置 | Term QPS | Term avg(ms) | Match QPS | Match avg(ms) |
|---|---|---|---|---|---|
| SSD(8C 16G) | VM/物理机 | ~1000 | 12-16 | ~1000 | 11-13 |
| 高性能 SSD(16C 32G) | - | ~1000 | 15 | ~2000 | 12 |
聚合查询(Bucket Agg / Matrix Agg):
| 机器类型 | Bucket Agg QPS | Matrix Agg QPS |
|---|---|---|
| SSD(8C 16G) | ~1000 | ~1500 |
| 高性能 SSD(16C 32G) | ~1500 | ~2000 |
结论:
- 查询性能 VM/物理 SSD 相当,高性能 SSD 优势主要在 Match 和聚合查询
- 查询性能对 I/O 更敏感,高读写业务需要 SSD 机器
- 聚合查询对 I/O 要求更高于普通查询
参考资料
评论 (0)
发表评论