ES|QL 的FORK和INLINE STATS都已转正:前者把一条查询劈成几路并行跑,后者聚合完还能保住原始行。本文讲清两者的用法、版本状态和各自的使用边界。
说明
本文描述问题及解决方法同样适用于 腾讯云 Elasticsearch Service(ES)。
它们解决的是同一类问题
ES|QL 有一批命令是在解决同一类问题:有些事以前要跑多条查询、或者在应用层拼,现在一条就够了。这篇讲两个都已转正的命令:FORK 和 INLINE STATS。
先看版本,两个都转正了,别按老印象用
| 命令 | 状态 |
|---|---|
FORK | 9.1–9.3 预览,9.4+ GA(serverless GA) |
INLINE STATS | 9.2 预览,9.3+ GA(serverless GA) |
依据是文档头部的 applies_to:FORK 写的是 stack: preview 9.1-9.3, ga 9.4+;INLINE STATS 写的是 stack: preview =9.2, ga 9.3+。
一、FORK:把一条查询劈成几条并行跑
痛点很具体:同一批数据,想同时算几个不同口径,以前只能拆成多条查询各跑一遍,回应用层自己拼。
FORK 的定义是(6–7 行):创建多个执行分支,对同一份输入数据做处理,再把结果合并到一张输出表里。
FORK ( <processing_commands> ) ( <processing_commands> ) ... ( <processing_commands> )
_fork 判别列
合并时会加一列 _fork,标明每行来自哪个分支,取值是 fork1、fork2、fork3……按分支定义顺序(42–43 行)。
输出行为
三条要记住(47–53 行):
- 各分支可以输出不同的列。
- 同名列的类型必须在所有分支里一致,缺失的列填
null。 - 每个分支内部保持行顺序,但不同分支的行可能交错。想按分支分组看结果,用
SORT _fork。
一个很实在的例子
官方有个例子是"一条查询同时拿 Top 5 明细和总数"(simpleForkWithStats @ v9.5.4):
FROM books METADATA _score
| WHERE author:"Faulkner"
| EVAL score = round(_score, 2)
| FORK (SORT score DESC, author | LIMIT 5 | KEEP author, score)
(STATS total = COUNT(*))
| SORT _fork, score DESC, author
结果是:
| author:text | score:double | _fork:keyword | total:long |
|---|---|---|---|
| Colleen Faulkner | 2.18 | fork1 | null |
| William Faulkner | 2.18 | fork1 | null |
| Danny Faulkner | 2.02 | fork1 | null |
| Paul Faulkner | 2.02 | fork1 | null |
| William Faulkner | 2.02 | fork1 | null |
| null | null | fork2 | 18 |
fork1 是评分最高的 5 本,fork2 是总数 18,对不上的列填 null。一次性拿回"明细"和"汇总",不用跑两遍。
9.4 起,隐式 LIMIT 没了
这是个重要的行为变化。文档分版本写得很清楚(25–31 行):
- 9.1–9.3(预览):每个
FORK分支会加一个隐式的LIMIT 1000。 - 9.4+(GA):
FORK分支不再有隐式LIMIT 1000。
也就是说,同样一条查询,从 9.3 升到 9.4,每个分支返回的行数上限从 1000 变成了不限。如果你的分支本来就在靠这个隐式 LIMIT 兜底,升级后返回量可能会变大,得自己补上显式的 LIMIT。
限制
(57–59 行)
- 最多 8 个分支。
- 9.3.0 之前,
FORK不支持配合远端集群引用。 - 一个查询里只能有一个
FORK。
另外:FORK 是混合搜索的基础
FORK 和 FUSE 命令配合,就能做 hybrid search——把多路检索的结果合并起来重新打分(20 行)。比如一个分支走 match 全文、一个分支走 kNN 向量,再 FUSE 融合。
二、INLINE STATS:聚合完还能保住原始行
STATS 的问题:聚合之后原始行就没了,只剩分组结果。想给每行带上"本组的平均值""全表的最大值"这种数,以前要么自己再 JOIN 回去,要么查两次。
INLINE STATS 补的就是这个。官方定义(6–10 行):
TheINLINE STATSprocessing command groups rows according to a common value and calculates one or more aggregated values over the grouped rows. The results are appended as new columns to the input rows.
The command is identical toSTATSexcept that it preserves all the columns from the input table.
跟 STATS 一样分组聚合,但结果作为新列追加到原来的行上,行数不变。 写过 SQL 的话,这个感觉接近窗口函数。
语法(15–20 行):
INLINE STATS [column1 =] expression1 [WHERE boolean_expression1][,
...,
[columnN =] expressionN [WHERE boolean_expressionN]]
[BY [grouping_name1 =] grouping_expression1[,
...,
[grouping_nameN = ] grouping_expressionN]]
注意每个聚合值后面都能跟一个 WHERE,用来筛选参与这个聚合的行——这是它比 STATS 灵活的地方。
几个行为要点
(48–59 行)
- 输出表和输入表行数相同。
- 只新增列,或者覆盖同名列。列名重叠时,已有列的值可能被覆盖,列顺序也可能变——新列会按
INLINE STATS里定义的顺序排。 - 省略
BY就是对全表聚合。 - 分组函数支持
BUCKET和TBUCKET(68–69 行)。 - 计算聚合时,单个
null值会被跳过(42 行)。
限制
- 不支持
CATEGORIZE分组函数。 INLINE STATS之前不能用LIMIT(显式或隐式都不行),因为会导致结果不符合预期。INLINE STATS之前不能用FORK。
第三条正好跟上面 FORK 那条对上:虽然 9.4 起 FORK 分支不再加隐式 LIMIT 1000,但 v9.5.4 的文档里这条限制仍然写着,用之前留意一下。
三、什么时候用哪个
按我的理解:
- 要把一份数据按不同口径分别算,最后一起看 →
FORK。典型是"明细 + 汇总",或者混合搜索的多路召回。 - 要给每一行补一个聚合出来的参照值,行数不能变 →
INLINE STATS。比如给每条订单补一个"该用户的平均订单金额"。
两者不能串着用(FORK 后不能接 INLINE STATS),所以别指望"分叉之后再各自补聚合列"。
写在最后
这两个命令的共同点是把原来要在应用层拼的活收回到一条查询里。用的时候有一件事别忘了:它们都已经转正,但行为在转正过程中改过(比如 FORK 分支的隐式 LIMIT 1000 在 9.4 没了),别再按老印象写。
"收回到一条查询里"有什么好处:以前要做多件事,得发多条查询、在应用层合并结果——网络往返和应用代码都是成本。收回之后这些都没了,而且计算可以由引擎统一优化。
但也不是所有情况都该收:如果各分支的结果要按完全不同的方式处理(比如一边画图、一边导出),合成一条反而让应用逻辑变复杂。判断依据是"这些结果是不是要一起用"——一起看就合成一条,各走各的就分开。
关于 FORK 的隐式 LIMIT 被去掉这件事,我觉得值得提:它说明转正不等于冻结。预览期的设计会按实际反馈调整,而这类调整往往不显眼——你不看 changelog 就会一直按老写法写,然后奇怪为什么结果条数不对。
我的建议:任何预览期用过的功能,转正之后都该重新确认一次当前行为。这个习惯成本很低(瞄一眼文档),但能避免一类很隐蔽的问题:代码没改、查询没改、结果变了——这种问题排查起来特别费劲,因为你会怀疑数据而不是版本。
参考
- FORK 命令文档 @ v9.5.4
- INLINE STATS 命令文档 @ v9.5.4
- FORK 官方示例(simpleForkWithStats)@ v9.5.4
- 视图 / 子查询 / FORK 对比 @ v9.5.4
- 9.1.0 highlights(FORK 首次发布)@ v9.3.0
本文对照 Elasticsearchv9.5.4标签源码与v9.3.0标签发布说明核对,示例取自官方示例文件。撰写时未启动集群、未运行测试。