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

【摘要】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 子句对照速查表(这张表是主轴)

# DSL SQL 说明
1 query.bool.must WHERE 条件1 AND 条件2 多条件取交集
2 query.bool.should WHERE (条件1 OR 条件2) 必须带括号,OR 优先级低于 AND
3 query.bool.must_not WHERE NOT 条件 / col <> '值' 字段可空时补 col IS NULL OR 分支
4 query.bool.filter WHERE 里的普通条件 与 must 同样处理,无评分差异
5 term: {status: 500} status = 500 精确等值
6 terms: {method: ["GET","POST"]} method IN ('GET','POST') 多值枚举
7 range: {request_time: {gte: 1, lte: 5}} request_time >= 1 AND request_time <= 5 gte / lte 闭区间,gt / lt 开区间
8 exists: {field: "user_agent"} user_agent IS NOT NULL 字段存在性
9 must_not + exists user_agent IS NULL 取反后变成空值判断
10 aggs.terms: {field: "uri", size: 10} GROUP BY uri ORDER BY cnt DESC LIMIT 10 分组计数取 TopN,ORDER BY 不能省
11 aggs.date_histogram + calendar_interval: "1h" GROUP BY date_trunc(log_time, 'hour') 粒度从参数变表达式
12 aggs 里的 avg / sum / max / min AVG() / SUM() / MAX() / MIN() 指标聚合直接对应
13 两级 aggs 桶(a 套 b) GROUP BY a, b 扁平化;要分层 TopN 需套窗口函数
14 bucket_selector(阈值过滤桶) HAVING 聚合值 > 阈值 比的是聚合值,不能写进 WHERE
15 match: {message: "timeout error"} MATCH_ANY(message, 'timeout error') 分词后命中任一词
16 match + "operator": "and" MATCH_ALL(message, 'timeout error') 全部命中才匹配
17 match_phrase: {message: "connect timeout"} MATCH_PHRASE(message, 'connect timeout') 保持词序,需索引开 support_phrase
18 query_string: "a AND (b OR c)" SEARCH('a AND (b OR c)') 4.0 起提供的统一检索入口,兼容 query_string 风格
19 sort + from + size ORDER BY ... LIMIT size OFFSET from 浅翻页可用,offset 大了会变慢
20 search_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_operator or(默认)或 and 多词项默认布尔关系 对应 ES 的 default_operator
minimum_should_match Lucene 模式下可设 至少命中几个 SHOULD 子句 对应 ES 的同名参数
date_trunc 单位 hour / day,字符串常量 时间粒度 趋势图与按小时告警
EXPLAIN 每条翻完跑一次 确认走倒排索引 全场景

3.4 一条完整改写示例

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

css 复制代码
{
  "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" } } }
    }
  }
}
vbnet 复制代码
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 三段常用片段

sql 复制代码
-- 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 翻完之后的三步验证

sql 复制代码
-- 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 Doris Elasticsearch
布尔组合 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 聚合桶类型 + 参数
聚合后过滤 HAVING bucket_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 BY TopN 拿到任意 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
相关推荐
野生数据人1 小时前
采集链路改投 Doris 速查手册:三条路径对照、output 配置项清单、报错处理表
大数据·数据库
救救孩子把1 小时前
AI面试八股文-07_6-向量数据库索引详解
数据库·人工智能·面试·向量数据库·hnsw
李白客2 小时前
国产数据库"实际用户增长"怎么核验?先定口径,再谈第一
数据库
真上帝的左手2 小时前
27. 数据产品-数据治理-11.完整岗位体系
大数据·数据治理·数据架构
I Am a robert girl2 小时前
VideoLoop:用循环工作记忆对抗长视频 Agent 的语义抖动
大数据·人工智能·机器学习·多模态agent·长视频理解·工作记忆·语义抖动
龙亘川2 小时前
水质决策报表业务建模:从自定义报表到六时间尺度自动报表闭环
大数据·人工智能·智慧城市·开源软件
广东帝工2 小时前
给内河航道搭建立体感知网络
大数据
数据工匠老o2 小时前
一张 5 亿行的表按月分区后,清理历史数据从几小时变成了几秒
数据库·sql
hwscom3 小时前
几分钟开防护,SQL和XSS注入全被护卫神挡住
数据库·sql·xss