9.2 起 ES|QL 请求如果同时带
profile和文本格式(format=txt/csv),会直接报错。以前不报错——但也不是真的给了你 profile 信息。从"悄悄不给"变成"明确告诉你不行",这个变化比看起来重要。
说明
实测环境:本地起的 Elasticsearch 9.4.2 单节点实例(官方 tar 包直接跑,堆内存 512MB,安全与机器学习关闭),curl 连本地 9200 端口。本文「实测」小节的命令和输出都出自这个实例。
来源和边界先交代:本文的依据出自 v9.4.2 标签下的官方文件(9.2.0.yml、EsqlMediaTypeParser.java),文中引用的原文均来自这些文件。示例取自官方示例;哪些是文档写明、哪些是我的推断,正文里逐处标注。

图 1:官方文件 9.2.0.yml(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 9.2.0.yml
官方条目
9.2 的 changelog 标为 feature(9.2.0.yml 第 66 行 @ v9.4.2),PR 128627:
Fail `profile` on text response formats
只有一行 summary。实质内容来自源码。

图 2:官方文件 EsqlMediaTypeParser.java(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 EsqlMediaTypeParser.java
profile 和响应格式
先说这两个东西。
profile:让 ES 返回查询的执行剖析信息——每个算子花了多少时间、处理了多少行、有没有走优化。用来排查"为什么这条查询慢"。
响应格式:ES|QL 可以返回多种格式:
json(结构化,能表达嵌套);txt/csv(文本格式,扁平表格);- 还有一些其它格式。
问题就在这两个的组合上。
为什么不能一起用
profile 的输出是深度嵌套的树状结构:查询计划 → 各阶段 → 各算子 → 每个算子内部的计时和计数。
文本格式是扁平的表格——有行有列,没有嵌套。
把一棵树存成一张表,只能丢掉大部分信息。
所以源码的处理是:直接拒绝,而不是"尽力给你输出一个残缺版本"。

图 3:官方页面 PR #128627。来源:GitHub elastic/elasticsearch pull/128627
源码里的实现
校验在 EsqlMediaTypeParser.java 第 88–92 行(源码 @ v9.4.2):
private static void validateProfile(boolean profile, MediaType fromMediaType) {
if (profile && fromMediaType instanceof TextFormat) {
throw invalid("profile");
}
}
逻辑非常直白:profile 开了,且格式是文本 → 抛异常。
报错文案在下面几行的 invalid() 里:
private static IllegalArgumentException invalid(String argument) {
return new IllegalArgumentException(
"Invalid use of [" + argument + "] argument: cannot be used in combination with "
(后面还有一段说明该用哪种格式,上面只引了开头——完整文案就在同一个方法里,遇到时能直接看到。)
同一处还有另一个参数
这是翻源码时的一个额外发现,changelog 里没提。
就在 validateProfile 上面几行(第 82–86 行):
private static void validateIncludeExecutionMetadata(Boolean includeExecutionMetadata, MediaType fromMediaType) {
if (Boolean.TRUE.equals(includeExecutionMetadata) && fromMediaType instanceof TextFormat) {
throw invalid("include_execution_metadata");
}
}
include_execution_metadata 也有同样的限制——它要加的也是结构化元数据,同样塞不进扁平表格。
两个参数共用同一个 invalid() 方法,所以报错文案的措辞是一样的,只是中括号里的参数名不同。
(说明:这两个校验是否同一次改动引入的,未查证。只确认 9.4.2 里两者并存。)
为什么"静默"比"报错"更麻烦
上面那句"宁可报错"值得展开说,因为它是判断改动好坏的一个常用标准。
报错的成本:你立刻知道有问题,去查,改掉。成本是一次性的、可预期的。
静默的成本:你不知道有问题,拿着不完整的结果继续做事,直到某个下游环节出错才暴露——那时候排查范围已经扩大了很多倍。
举个本篇的具体例子:假设你在调优一条慢查询,用了 format=txt 加 profile。
- 静默的情况:返回一个表格,你看到"没有 profile 信息",可能以为是这次查询没产生剖析数据,于是换个思路继续试——时间在错误的方向上消耗掉了;
- 报错的情况:一句话告诉你"这个组合不行,换格式",十秒钟解决。
差别在于信息量:报错把原因直接给了你,静默什么都没说。
所以判断标准是——一条改动如果在"失败"和"静默"之间选了失败,通常是对的。_source mode 设置迁移、discovery-ec2 换 SDK v2 里批评过静默失效,理由就是同一个。
当然这条标准有边界:如果是正常使用路径因为一个无关紧要的事就报错,那是过度严格。本篇不属于这种情况——profile 和文本格式本来就是语义冲突的组合,不属于正常使用。
从"静默"到"报错"才是重点
标题里写了"以前是静默忽略"。这里说清楚,因为它决定了这条改动的价值。
以前:你请求 profile + format=txt,ES 不报错,返回一个表格——但里面没有 profile 信息(或者信息不完整)。
你会以为自己拿到了剖析数据,实际上没有。 基于该输出做的性能分析,可能是错的。
现在:直接报错,明确告诉你这个组合不行,让你换成 json。
这个变化属于"把静默问题变成明确失败"。 前面几篇里反复遇到这类区分:
| 篇 | 改动 | 属于哪类 |
|---|---|---|
| _source mode 设置迁移 | _source.mode 搬家 |
静默失效(坏) |
| discovery-ec2 换 SDK v2 | ec2 配置失效 | 静默失效(坏) |
| 本篇 | profile 与文本格式冲突 | 静默 → 报错(好) |
对这条的评价是正面的:它把一个可能误导人的静默行为,改成了明确的失败。结论是宁可报错,也不要给一个看起来正常但不完整的结果。
对使用者的影响
如果脚本同时用了这两者,9.2 之后会开始报错。
这听起来是坏消息,但要看清性质:以前它也没有给你 profile 数据。所以:
- 如果你依赖了该输出做分析 → 那分析基础本来就有问题,报错是在帮你发现;
- 如果你只是随手带上 profile 没真看 → 去掉这个参数就行。
真正的风险在没人注意的自动化脚本里。 比如某个巡检任务固定带 profile=true 和 format=csv,升级后突然开始失败。这类失败是好事(暴露了问题),但会造成告警——所以升级前最好扫一遍。
怎么改
要么去掉 profile,要么换成结构化格式。 取决于你要什么:
要看 profile → 用 json 格式:
curl -s 'localhost:9200/_query?format=json' -H 'Content-Type: application/json' -d '{
"query": "FROM logs | STATS c = COUNT(*) BY host",
"profile": true
}'
只要表格 → 去掉 profile:
curl -s 'localhost:9200/_query?format=csv' -H 'Content-Type: application/json' -d '{
"query": "FROM logs | STATS c = COUNT(*) BY host"
}'
(说明:上面两条命令未实际执行,参数写法以你版本的文档为准。ES|QL 的请求入口和参数名在不同版本可能有差异。)
profile 输出大概长什么样
没见过 profile 输出的话,可能不好理解为啥它塞不进表格。这里说个大概结构(以下为示意,非实际输出的逐字拷贝)。
format=json 下,profile 部分大致是分层的:
{
"profile": {
"drivers": [
{
"description": "...",
"operators": [
{
"operator": "...", "status": {
"processed_rows": 1000 } }
]
}
]
}
}
重点看它的层次:profile → drivers(数组)→ 每个 driver 有 operators(又是数组)→ 每个 operator 有 status(又是对象)。
至少三层嵌套,中间还有数组。
而 csv 格式能表达的是:
host,count
a,10
b,20
行和列,没有嵌套。
把三层结构压成两列,只能:要么把整个 profile 放进一个单元格(那不如直接看 json),要么拆成几百行(那就失去了可读性)。两条路都不合理,所以拒绝是对的。
(说明:上面这段 JSON 是构造的示意,用来说明层次问题,不是 ES 实际返回的格式。真实结构请以你版本的输出为准。)
排查建议
因为这是"新增报错"而不是"行为改变",排查方式很直接:
一,看报错信息。 文案里会明确点出是哪个参数([profile] 还是 [include_execution_metadata]),一眼能定位。
二,搜代码和脚本里这两个参数的用法。 重点找把它们和文本格式一起用的地方:
# 在脚本里找同时出现 profile 和 txt/csv 的请求
grep -rn "profile.*\(txt\|csv\)\|\(txt\|csv\).*profile" /path/to/your/scripts/
三,升级前扫一遍比升级后救火便宜。 这条改动不会造成数据问题,但会让脚本失败——在批量任务里可能触发一连串告警。
和 ES|QL 其它参数限制放一起看
ES|QL 有不少"参数之间互斥"的限制,本篇是一个例子。把能想到的同类整理一下:
| 组合 | 结果 |
|---|---|
profile + 文本格式 |
报错(本篇) |
include_execution_metadata + 文本格式 |
报错(同源码处) |
ccs_minimize_roundtrips + PIT |
报错(RestSearchAction.java:425) |
共同点:这些组合是"实现了也说不清楚",不是"不能实现"。与其给个含糊的结果,不如拒绝。
这形成了一个可以类推的判断:在 ES 里遇到"某两个参数不能一起用",通常是因为它们的组合语义不清晰,而不是技术做不到。 顺着这个思路去理解,比死记这些限制要省力。
也意味着:这类限制可能随版本增加(以前没发现冲突,后来发现了)。所以升级后出现新的参数冲突报错,先理解它为什么冲突,再决定怎么绕——不要第一反应就是"以前能用怎么现在不行了"。
这条改动怎么看
这条改动很小,但类型值得记住:它是"新增一个明确失败"来替代"原有的静默不完整"。
按改动的分类方式:
| 维度 | 判断 |
|---|---|
| 是否改变查询结果 | 否(只影响 profile 信息) |
| 是否阻塞升级 | 否(除非脚本依赖这个组合) |
| 排查成本 | 低(报错文案直接点名参数) |
| 修复成本 | 低(二选一去掉一个) |
所以它的处理优先级不高——不是升级前必须处理的类型。但如果自动化脚本用得多,值得在升级前后留意一下有没有新增失败。
改动前是不是静默忽略
changelog 只写了 "Fail",没有描述改动前的行为。 标题和正文里说"以前是静默忽略"——这是理解,不是官方说明。
支持这个理解的是:改动的措辞是 "Fail"(让某事失败),暗示它以前不失败。而"不失败"只有两种可能——要么正常返回 profile(那现在改成失败就说不通),要么静默不给。
但未实测 9.1 的行为来确认。 如果你需要确定的答案,在 9.1 上跑一次该组合看返回什么就行。
把这个不确定性写出来,是因为它影响对这条改动的定性:
- 如果以前是静默 → 这条是改进(把隐患变成明确报错);
- 如果以前其实给了某种简化版 profile → 这条是能力收缩(虽然可能性不大)。
按推断前者概率高得多,但"高概率"不等于"确认",所以用的时候可以自己验证一下。
在 9.4.2 上跑一遍
同一个查询,只换 format:

图 4:本地 9.4.2 单节点实例上的实测输出。上:默认 json 格式;下:format=txt
curl -s -X POST "http://localhost:9200/_query" \
-H 'Content-Type: application/json' -d '{"query":"ROW x = 1","profile":true}' \
| jq -c 'keys'
curl -s -X POST "http://localhost:9200/_query?format=txt" \
-H 'Content-Type: application/json' -d '{"query":"ROW x = 1","profile":true}' \
| jq -r '"HTTP \(.status): \(.error.root_cause[0].reason)"'
默认格式返回的 key 里有 profile;换成 txt 直接 400,报错是 Invalid use of [profile] argument: cannot be used in combination with [txt, csv, tsv] formats。
这句报错把可用的格式都列出来了,照着改就行。真正容易出事的是改之前:如果某个脚本一直用 format=txt 配 profile=true 跑性能分析,升级前它只是拿不到 profile 段,升级后整条查询都失败——从"少一块数据"变成"没有数据",监控上的表现完全不同。
结论是:profile 只在 json 响应里有意义,分析脚本要单独走一条路径;把 format=txt 和 profile 写在同一处的地方,升级前先搜一遍。
写在最后
很小的改动:一个参数组合从"悄悄不给"变成"明确报错"。
一,profile 和文本格式(txt/csv)不能同时用,用了会报错。 原因是 profile 输出是嵌套树,塞不进扁平表格——这个冲突是语义层面的,不是实现偷懒。
二,include_execution_metadata 有同样的限制。 这条 changelog 没提,是从源码里看到的——两个参数共用同一个 invalid() 方法,报错措辞一致,只有参数名不同。
三,这条改动是好的——它替代的是"给你一个看起来正常但不完整的输出"这种更麻烦的行为。不过"改动前是静默"这一点是理解,changelog 只写了 "Fail",9.1 的实际行为未实测。
另外诚实标注两处:两个校验是否同一次改动引入未查证(只确认 9.4.2 里并存);示例命令未执行。
参考
- changelog 9.2.0:Fail
profileon text response formats(第 66 行)@ v9.4.2 EsqlMediaTypeParser.java:validateProfile(第 88–92 行)与validateIncludeExecutionMetadata(第 82–86 行)@ v9.4.2- PR 128627
本文对照 Elasticsearch
v9.4.2标签的 changelog 与源码核对,引文与代码逐字取自原文。"以前是静默忽略"是对该改动性质的理解,changelog 只说 "Fail",未描述改动前行为;两个校验是否同一次改动引入未查证,仅确认 9.4.2 中并存;两条命令已在本地 9.4.2 单节点实例上执行,图 4 的报错为完整原文。