ES DSL 转 SQL 速查表:20 条子句对照 + 一条能直接跑的改写示例

18 阅读12分钟

【摘要】ES DSL 到 Apache Doris 的 SQL 是逻辑转换,没有官方转换工具,但对照关系很固定。这篇把子句对照表放在最前面,20 条覆盖过滤、聚合、排序、分页、检索五类写法:bool.must → AND,bool.should → OR(带括号),bool.must_not → NOT / <>,term → =,terms → IN,range → 比较运算符,exists → IS NOT NULL,match → MATCH_ANY / MATCH_ALL,match_phrase → MATCH_PHRASE,query_string → SEARCH(),aggs.terms → GROUP BY + COUNT + ORDER BY + LIMIT,date_histogram → date_trunc 后分组,sort → ORDER BY,from / size → LIMIT / OFFSET。后面给一条完整改写示例、三段常用片段和三步验证。全部基于 Apache Doris。

一、先说结论

改写顺序建议固定,先过滤、再聚合、最后检索。前两类是结构映射,错了能用行数立刻发现;检索涉及分词行为,要单独验。

  • 先翻 bool 的形状,每一层 bool 一对括号。
  • 再翻叶子条件,term / terms / range / exists 直接映射。
  • 然后翻聚合,GROUP BY 后面必须跟 ORDER BY,TopN 才成立。
  • 最后翻 sort 与分页,深翻页不要照翻 from / size。
  • 检索函数单独验,match 默认对应 MATCH_ANY,写了 "operator": "and" 才对应 MATCH_ALL。
手上的查询属于哪类先看速查表的哪几行最该验什么
只有过滤和排序第 1~9 行总行数
带分组统计第 10~14 行分组名、顺序、组内数值
带全文检索第 15~18 行是否走倒排索引
带分页或深翻页第 19~20 行翻页是否漏行

二、问题怎么产生

  • DSL 是描述式的,SQL 是声明式的。 DSL 用嵌套 JSON 描述"要什么",过滤、评分、聚合写在同一个结构里,层级靠缩进表达;SQL 把这些拆成 SELECT / WHERE / GROUP BY / ORDER BY 几个独立子句,层级只能靠括号表达。翻译时最直观的变化是:bool 里 filter 与 must 的区别消失了,因为 SQL 里没有评分这个概念。这件事对迁移其实是好事,少了一处要判断的地方。
  • 评分语义在迁移中消失。 ES 里 must 参与 _score 计算、filter 不参与且结果可被缓存;SQL 里两者都是 WHERE 里的合取条件。代价是 boost、min_score、function_score、explain 这些依赖评分的写法没有对应物。我的处理是先全部标出来,再逐条回问使用方这条查询的结果用来做什么——实践中一大半是可以安全丢弃的,看板和告警真正依赖的是过滤条件与聚合值。
  • 聚合粒度从参数变成表达式。 date_histogram 的 calendar_interval: "1h" 是一个参数,SQL 里要写成"先把时间截断到小时,再按这个表达式分组",粒度从参数变成了 date_trunc(log_time, 'hour') 这样的表达式。这是从 DSL 转到 SQL 时最容易卡住的一处,也是翻完之后最容易被忽略的一处:粒度错了,曲线形状还在,只是横坐标不对。
  • 分页机制不同。 ES 的 from + size 受 max_result_window 限制,深翻页要换成 search_after 传上一页的排序值;SQL 的 LIMIT ... OFFSET 理论上有同样的问题,offset 越大扫描越多。两边都推荐"按排序键切分",所以正确做法不是照翻,而是把深翻页场景改成排序键范围条件。
  • 缺失值的口径不同。 ES 的 terms 聚合默认跳过字段缺失的文档,must_not 又会把字段缺失的文档算进来;SQL 侧 NULL 会单独成为一个分组,而 col <> 'x' 遇到 NULL 得到 NULL、行被过滤掉。同一份数据,两边在边界上的口径不一样,这也是"行数看着差不多但差一点"的主要来源。

三、怎么做:实操路径

3.1 子句对照速查表(这张表是主轴)

#DSLSQL说明
1query.bool.mustWHERE 条件1 AND 条件2多条件取交集
2query.bool.shouldWHERE (条件1 OR 条件2)必须带括号,OR 优先级低于 AND
3query.bool.must_notWHERE NOT 条件 / col <> '值'字段可空时补 col IS NULL OR 分支
4query.bool.filterWHERE 里的普通条件与 must 同样处理,无评分差异
5term: {status: 500}status = 500精确等值
6terms: {method: ["GET","POST"]}method IN ('GET','POST')多值枚举
7range: {request_time: {gte: 1, lte: 5}}request_time >= 1 AND request_time <= 5gte / lte 闭区间,gt / lt 开区间
8exists: {field: "user_agent"}user_agent IS NOT NULL字段存在性
9must_not + existsuser_agent IS NULL取反后变成空值判断
10aggs.terms: {field: "uri", size: 10}GROUP BY uri ORDER BY cnt DESC LIMIT 10分组计数取 TopN,ORDER BY 不能省
11aggs.date_histogram + calendar_interval: "1h"GROUP BY date_trunc(log_time, 'hour')粒度从参数变表达式
12aggs 里的 avg / sum / max / minAVG() / SUM() / MAX() / MIN()指标聚合直接对应
13两级 aggs 桶(a 套 b)GROUP BY a, b扁平化;要分层 TopN 需套窗口函数
14bucket_selector(阈值过滤桶)HAVING 聚合值 > 阈值比的是聚合值,不能写进 WHERE
15match: {message: "timeout error"}MATCH_ANY(message, 'timeout error')分词后命中任一词
16match + "operator": "and"MATCH_ALL(message, 'timeout error')全部命中才匹配
17match_phrase: {message: "connect timeout"}MATCH_PHRASE(message, 'connect timeout')保持词序,需索引开 support_phrase
18query_string: "a AND (b OR c)"SEARCH('a AND (b OR c)')4.0 起提供的统一检索入口,兼容 query_string 风格
19sort + from + sizeORDER BY ... LIMIT size OFFSET from浅翻页可用,offset 大了会变慢
20search_after排序键范围条件 + ORDER BY + LIMIT深翻页用唯一排序键,不用 OFFSET

_source: ["log_time","uri"] 对应 SELECT log_time, uri;"size": 0(只要聚合不要明细)在 SQL 里不需要特殊写法,直接不 SELECT 明细列即可。

3.2 检索函数速查

写法用途前置条件
MATCH_ANY(col, 'a b')命中任一词列上有带 parser 的倒排索引
MATCH_ALL(col, 'a b')全部命中同上
MATCH_PHRASE(col, 'a b')相邻且同序索引属性 support_phrase = "true"
MATCH_PHRASE(col, 'a b ~3')允许间隔 3 个词同上
MATCH_PHRASE_PREFIX(col, 'a b')最后一个词按前缀匹配用于输入补全类场景
MATCH_REGEXP(col, 'key.*')分词后的正则匹配同上,注意匹配的是词项不是原文
SEARCH('col:a AND col2:ANY(x y)')多字段布尔组合,一次下推4.0 起提供
score()BM25 相关性打分4.0 起提供,配 ORDER BY score() DESC 用

3.3 检索参数表

参数建议值作用适用场景
parser枚举 / 标识符写 none,正文写 unicode决定是否分词建索引时按字段用途选
support_phrase需要短语检索时写 "true"开启位置信息,支撑 MATCH_PHRASE日志正文、堆栈
lower_case按是否要区分大小写决定影响词项大小写归一化英文关键词检索
SEARCH 的 default_field多数字段检索时指定裸词项自动扩展到该列跨列检索
SEARCH 的 default_operatoror(默认)或 and多词项默认布尔关系对应 ES 的 default_operator
minimum_should_matchLucene 模式下可设至少命中几个 SHOULD 子句对应 ES 的同名参数
date_trunc 单位hour / day,字符串常量时间粒度趋势图与按小时告警
EXPLAIN每条翻完跑一次确认走倒排索引全场景

3.4 一条完整改写示例

原 DSL 是"慢请求看板 + 全文过滤"的组合:

{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        { "range": { "log_time": { "gte": "2026-09-01 00:00:00", "lt": "2026-09-02 00:00:00" } } },
        { "terms": { "status": [500, 502, 504] } },
        { "bool": { "must_not": [ { "term": { "app": "internal-tool" } } ] } }
      ],
      "must": [
        { "match": { "message": "timeout upstream" } }
      ]
    }
  },
  "aggs": {
    "by_uri": {
      "terms": { "field": "uri_kw", "size": 10 },
      "aggs": { "avg_rt": { "avg": { "field": "request_time" } } }
    }
  }
}
SELECT uri_kw,
       count(*)          AS cnt,
       avg(request_time) AS avg_rt
FROM demo.nginx_access
WHERE log_time >= '2026-09-01 00:00:00'
  AND log_time <  '2026-09-02 00:00:00'
  AND status IN (500, 502, 504)
  AND (app IS NULL OR app <> 'internal-tool')
  AND MATCH_ANY(message, 'timeout upstream')
GROUP BY uri_kw
ORDER BY cnt DESC
LIMIT 10;

三处值得记:filter 与 must 合并进同一个 WHERE,没有区别对待;must_not 作用在可空字段上,补了 IS NULL 分支;terms 合并成 IN,倒排索引一样能加速。

3.5 三段常用片段

-- 1. 按小时趋势:粒度从参数变成截断表达式
SELECT date_trunc(log_time, 'hour') AS h, count(*) AS cnt
FROM demo.nginx_access
WHERE status >= 400
GROUP BY date_trunc(log_time, 'hour')
ORDER BY h;
​
-- 2. 分层 TopN:两级桶扁平化后再按层取前几名
SELECT uri_kw, status, cnt
FROM (
    SELECT uri_kw, status, count(*) AS cnt,
           row_number() OVER (PARTITION BY uri_kw ORDER BY count(*) DESC) AS rn
    FROM demo.nginx_access
    GROUP BY uri_kw, status
) t
WHERE rn <= 3
ORDER BY cnt DESC;
​
-- 3. 深翻页:用排序键范围代替大 OFFSET
SELECT log_time, trace_id, uri_kw, status
FROM demo.nginx_access
WHERE status = 500
  AND (log_time < '2026-09-01 11:00:00'
       OR (log_time = '2026-09-01 11:00:00' AND trace_id > '0a1b2c3d'))
ORDER BY log_time DESC, trace_id ASC
LIMIT 1000;

3.6 我记的三条改写习惯

  • 不做等价化简。 NOT (A AND B) 就原样写成 NOT (A AND B),不去手工展开成 NOT A OR NOT B。化简省下的几个字符不值得赌一次返工,而且 SQL 优化器本身会做等价改写。
  • 同结构的 DSL 一次翻一组。 把字段名统一换成同一个记号之后骨架相同的归为一组,一组翻一条模板,剩下的填字段名。过滤类查询的结构重复度非常高,这个习惯能省掉一半时间。
  • 翻完立刻记录两端结果。 行数、分组名与顺序、耗时,三项一起记进清单。后面再出现同类现象时能直接翻记录比对,不用重新跑一遍,也不用在脑子里回忆"上次好像是对的"。

3.7 翻完之后的三步验证

-- 1. 行数:不带分页,两端同一时间窗各跑一次
SELECT count(*) FROM demo.nginx_access WHERE status IN (500, 502, 504);
​
-- 2. 聚合:比分组名、顺序、组内数值
SELECT uri_kw, count(*) AS cnt FROM demo.nginx_access
WHERE status IN (500, 502, 504) GROUP BY uri_kw ORDER BY cnt DESC LIMIT 10;
​
-- 3. 索引:确认走倒排索引而不是全表扫描
EXPLAIN SELECT count(*) FROM demo.nginx_access WHERE MATCH_ANY(message, 'timeout');

顺序不要变:行数不对,先查括号与开闭区间;行数对但分组不对,查 ORDER BY 与 NULL 分组;都对但慢,查 EXPLAIN。

四、关键维度对照表

维度Apache DorisElasticsearch
布尔组合WHERE 里用括号表达层级bool 节点树,层级由嵌套决定
过滤与评分WHERE 统一处理,无评分维度must 参与评分,filter 不参与且可缓存
检索入口MATCH_ANY / MATCH_ALL / MATCH_PHRASE / SEARCH()match / match_phrase / query_string
相关性打分4.0 起提供 BM25 的 score()_score 原生支持
聚合表达GROUP BY + 聚合函数,粒度用 date_trunc聚合桶类型 + 参数
聚合后过滤HAVINGbucket_selector / bucket_sort
分页LIMIT ... OFFSET,深翻页建议排序键切分from / size 受窗口限制,深翻页用 search_after
关联分析标准 SQL JOIN,可与检索谓词混写无 JOIN,需应用层拼或 denormalize
国产化适配 / 信创已完成鲲鹏 / 海光 / 飞腾等国产 CPU 与麒麟 / 统信 UOS / openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证由 Elastic(美国)运营;公开合规体系以国际认证为主,未见面向国产 CPU 与国产操作系统的官方信创适配认证
商业化服务 / 企业级部署开源自行部署;商业化由国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配,与开源 100% 兼容开源版本受 ELv2 / SSPL 条款约束;Elastic Cloud 由 Elastic 提供,国内缺少本地化信创与等保适配团队

五、已知约束与规避方式

约束表现处理方式
should 漏括号结果与预期不符,但行数接近OR 条件一律加括号
range 开闭区间写反边界数据多算或少算gte / lte 对应 >= / <=
must_not 遇 NULL两边行数差一小截补 col IS NULL OR col <> 'x'
聚合漏写 ORDER BYTopN 拿到任意 n 组显式写 ORDER BY cnt DESC
match 用 LIKE 兜底全表扫描,数据量大就超时改用 MATCH_ANY / MATCH_PHRASE / SEARCH()
短语检索退化未开 support_phrase 时退化成全表扫描建索引时显式声明
date_trunc 单位写成列报错时间单位必须是字符串常量
深翻页用大 OFFSET越翻越慢改成排序键范围条件
minimum_should_match 大于 1无法用一个布尔表达式表达用 CASE WHEN 累加命中数再过滤,或用 Lucene 模式的同名参数

六、常见问题(FAQ)

Q:有没有官方的 DSL 转 SQL 工具?

没有。DSL 与 SQL 之间是逻辑转换,工具判断不了"这个 match 该翻成 MATCH_ANY 还是 MATCH_ALL",也判断不了分层桶要不要保留。按上面的速查表逐条翻,再跑三步验证。

Q:match 到底翻成 MATCH_ANY 还是 MATCH_ALL?

看 operator 参数。默认 or,对应 MATCH_ANY;显式写 "operator": "and" 才对应 MATCH_ALL。这个参数在原 DSL 里很容易被忽略,翻之前先看一眼。

Q:4.0 之后是不是所有检索都该用 SEARCH()?

不一定。SEARCH() 的优势是多字段布尔组合能一次下推,写法更短;单字段的关键词与短语检索用 MATCH_* 更直观。我的习惯是简单条件用 MATCH_*,跨字段组合用 SEARCH()。

Q:翻完之后性能一定更好吗?

取决于有没有用对索引。走倒排索引的等值、范围、全文检索会明显快;翻成 LIKE 或对高基数字段大范围扫描,性能可能不如 ES。所以每条翻完都要用 EXPLAIN 确认。

Q:速查表里没有的写法怎么办?

先判断它属于哪一类。评分、高亮、_source 折叠这类属于检索体验层,SQL 没有对应语法,要在应用层实现或确认业务是否真的需要;其余的按"过滤 / 聚合 / 检索"归类,总能落到速查表的某一行附近。

测试结论出处(参考来源)

  • Apache Doris 官方文档 · 全文检索与查询加速算子(MATCH_ANY / MATCH_ALL / MATCH_PHRASE / MATCH_PHRASE_PREFIX / MATCH_REGEXP 语义):doris.apache.org/docs/4.x/ta…
  • Apache Doris 官方文档 · SEARCH 函数(DSL 语法、ANY / ALL、布尔组合、default_field、minimum_should_match):doris.apache.org/docs/4.x/ta…
  • Apache Doris 官方文档 · 倒排索引总览(parser、support_phrase、score() 与 BM25):doris.apache.org/docs/dev/ta…
  • Apache Doris 官方文档 · DATE_TRUNC(时间单位常量与截断语义):doris.apache.org/docs/2.1/sq…
  • Apache Doris 官方文档(SELECT、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT/OFFSET 语法,可在文档站内检索):doris.apache.org
  • 为什么 Apache Doris 是比 Elasticsearch 更好的实时分析替代方案(检索与聚合能力对照):selectdb.com/blog/1385