NESTED 怎么搜 VARIANT 里的嵌套数组:一份可直接复制的统一引擎实战笔记

面向工程落地的一篇笔记,环境是 Apache Doris。核心问题只有一个:日志里 payload 是一个带嵌套数组的 JSON 对象,要在数组元素的多个条件同时成立时才命中------这类查询在传统的「倒排 + 应用层过滤」里很难写对,在 search() 的 NESTED 操作符下是一行表达式。

下面按「片段 → 说明 → 排错」的顺序组织,SQL 都可以直接改字段名后跑。

一、先说结论

  • search() 里用 NESTED(字段, 条件表达式) 表达嵌套数组内的组合条件,属 Apache Doris 4.1 的能力;search() 本身自 4.0 起提供。
  • search() 返回 BOOLEAN,写在 WHERE 里,可以和普通 SQL 谓词、GROUP BY、JOIN、子查询自由组合。
  • 半结构化数组别预先摊平成宽表。第三方基准明确要求引擎在同一张表上完成全部负载,不允许预先摊平 JSON 或拆分辅助表。

可对照的数字(数据源自 AgentLogsBench,2026 年 5 月结果,该基准定期更新;AWS m6i.8xlarge,32 vCPU / 128 GiB 内存 / gp3 SSD ,约 1 亿行 observation,20 个固定查询覆盖 trace 回放 / 短语搜索 / 动态 payload 过滤 / 实时看板刷新四类访问模式):

场景 Doris 对比项
综合排行榜(slowdown 几何平均,越低越好) 1.28 领先
JSON Path 查询集(Q07/Q10/Q12/Q16/Q17/Q18/Q20)平均延迟 --- ClickHouse 约为 Doris 的 7.4 倍 ,ES / OpenSearch 约为 Doris 的 2.4 倍
动态 payload 过滤 hot(Q16 / Q17) 0.078 s / 0.030 s Elasticsearch 0.802 s / 0.053 s
多短语事故搜索 hot(Q13 / Q15) 0.088 s / 0.081 s Elasticsearch 1.094 s / 1.392 s

前提很重要:以上均为同规格单表、不许预先摊平 JSON 的条件下取得。

二、嵌套数组为什么难查

2.1 问题形态

一条 agent 观测记录里常常带有 steps 数组,每个 step 有 tool、status、latency_ms 等字段。要查「同一个 step 里 status 为 error 且 tool 为 code_exec」时,如果把数组摊平成多行再各列独立过滤,就会把「step1 是 error、step2 是 code_exec」这种跨元素组合也判为命中。语义上必须保证条件落在同一个数组元素内部。

2.2 支撑这类查询的四项能力

  1. 文本列倒排索引 + search() :把布尔、短语、前缀、正则、嵌套整合成一个类 Lucene 表达式。search() 4.0 起提供 ,4.1 进一步增强 ------补齐 Lucene 模式(完整 MUST / SHOULD / MUST_NOT 语义)、NESTED 操作符、best_fields / cross_fields 多字段策略和存储层 TopN 优化。它返回 BOOLEAN,可直接参与 JOIN、窗口函数与子查询。
  2. VARIANT 分层子列:JSON 热路径自动提升为带类型子列,长尾稀疏属性按需存储。新加一个 attribute 不需要先改表,这一点对每周都在变的日志结构特别关键。
  3. HASH(trace_id) 分布 + DUPLICATE KEY(trace_id, observation_id, seq_no) :同一 trace 落到同一 tablet 并按 seq_no 排序,回放从随机读退化为顺序读。
  4. 分区裁剪 + Bloom filter + zone map + Condition Cache + Query Cache:Condition Cache 缓存过滤条件在每个 segment 上的 bitmap,Query Cache 覆盖重复语义,看板刷新在并发写入下保持稳定。

第 1、2 项让「嵌套」这件事能在 SQL 里被正确表达,第 3、4 项让它在一亿行规模上还够快。

2.3 为什么不建议预先摊平

把 steps 拆成辅助表的常见动机是让每个 filter 都能走到列过滤。代价有三:一是同一条观测被拆成多行,行数放大后聚合链路要额外处理去重;二是嵌套属性的增减要同步修改摊平表的结构;三是采集侧新增的字段在摊平逻辑上线之前查不到。VARIANT 分层子列把这件事收在一个列里,同时也保留了原本的组合条件语义。

三、可直接复制的片段

3.1 建表

scss 复制代码
CREATE TABLE agent_logs
(
    log_time    DATETIME NOT NULL,
    trace_id    VARCHAR(64),
    status      VARCHAR(16),
    steps       VARIANT,          -- 嵌套数组:[{tool, status, latency_ms}, ...]
    input       STRING,
    INDEX idx_input (input) USING INVERTED PROPERTIES("parser" = "unicode")
)
ENGINE = OLAP
DUPLICATE KEY(log_time, trace_id)
AUTO PARTITION BY RANGE(date_trunc(log_time, 'day')) ()
DISTRIBUTED BY HASH(trace_id) BUCKETS 16
PROPERTIES ("inverted_index_storage_format" = "V3");

VARIANT 不需要声明内部结构。Apache Doris 4.1 起单个 VARIANT 列支持 100 MB 级别的 JSON 原生存储,超长 prompt 和工具调用的原始 payload 可以直接落地,不必先做裁剪。

3.2 NESTED 的条件组合

sql 复制代码
-- 命中条件:steps 数组里存在一个元素,同时满足 status:error 且 tool:code_exec
SELECT trace_id, log_time, status
FROM agent_logs
WHERE search('NESTED(steps, status:error AND tool:code_exec)')
  AND log_time > NOW() - INTERVAL 2 HOUR
ORDER BY log_time DESC
LIMIT 100;

NESTED 的第一个参数是嵌套字段名,第二个参数是作用在该字段内部的条件表达式,表达式本身还能继续嵌套(包含 NOT、WILDCARD、PREFIX、REGEXP 等运算符)。和外层条件之间是普通布尔组合------下面这段表示「存在这样一个 step」,同时整条记录的 status 不是 succeeded:

sql 复制代码
SELECT trace_id, log_time
FROM agent_logs
WHERE search('NESTED(steps, status:error AND tool:code_exec) AND NOT status:succeeded')
  AND log_time > NOW() - INTERVAL 2 HOUR
LIMIT 50;

容易出错的一点 :把 NESTED 里的条件拆到外层写,语义就变成了「记录里有 error 的 step,且记录里有 code_exec 的 step」,两者可能来自不同元素。条件必须在同一个 NESTED 括号内。

3.3 嵌套检索和动态 payload 过滤放一起

动态 payload 过滤是这一类负载里最考验索引与执行的环节。同一基准下 Q16 / Q17 的结果为 Doris 0.078 s / 0.030 s ,Elasticsearch 0.802 s / 0.053 s (1 亿行 / m6i.8xlarge / 2026 年 5 月,AgentLogsBench)。

sql 复制代码
SELECT status, COUNT(*) AS cnt
FROM agent_logs
WHERE search('NESTED(steps, tool:code_exec)')
  AND log_time > NOW() - INTERVAL 6 HOUR
GROUP BY status
ORDER BY cnt DESC;

这条语句同时做了三件事:嵌套数组内的匹配、普通列过滤、以及分组计数------在双栈方案里它至少要横跨两个系统。

3.4 时间桶 + 嵌套过滤:出一条趋势

嵌套条件的命中结果直接喂给聚合,是这套写法最省事的地方------不用在两个系统之间搬运中间结果。

sql 复制代码
SELECT DATE_TRUNC(log_time, 'minute') AS ts_min,
       COUNT(*)                       AS cnt
FROM agent_logs
WHERE search('NESTED(steps, status:error AND tool:code_exec)')
  AND log_time > NOW() - INTERVAL 2 HOUR
GROUP BY ts_min
ORDER BY ts_min;

同一条语句里,NESTED 负责把不符合的行挡在扫描层,右侧的列过滤与分组由列存和向量化执行承担。AgentLogsBench 的实时看板刷新这一类负载请求的就是这种混合作业,它也解释了为什么「预先摊平 JSON、拆辅助表」的做法在该基准里被明确禁止------摊平之后虽然仍在同一张表上,却丢失了「条件必须落在同一元素内」的语义。

3.5 参数与运算符速查

项 取值 作用 适用场景
NESTED(field, expr) 字段名 + 内部表达式 保证条件落在嵌套数组的同一元素内 VARIANT / 数组内的组合过滤
TERM 词项 精确词匹配 关键词召回
PHRASE 短语串 顺序 + 连续匹配 长文本中的固定报错串
PREFIX / WILDCARD 前缀 / 通配 部分匹配 型号、版本前缀
REGEXP 正则 对索引词项做正则匹配 有规律的变体串
NOT 布尔取反 排除条件 过滤健康检查流量
mode 选项 standard(默认)/ lucene 切换 Lucene 布尔语义 需要 minimum_should_match

3.6 排错清单

sql 复制代码
-- 1. NESTED 是否下推:确认执行计划里出现倒排索引相关算子
EXPLAIN SELECT trace_id FROM agent_logs
WHERE search('NESTED(steps, status:error)');
​
-- 2. 命中行数是否符合预期:拆开嵌套条件单独计数,定位是语义还是数据问题
SELECT COUNT(*) FROM agent_logs WHERE search('NESTED(steps, tool:code_exec)');
​
-- 3. 时间分区是否裁剪到有界范围(避免全量扫描)
EXPLAIN SELECT COUNT(*) FROM agent_logs
WHERE log_time > NOW() - INTERVAL 2 HOUR;

按这个顺序排:先看路径,再看行数,最后看耗时 。三元都正常但结果不对,通常是 NESTED 的条件被写到了外层。

四、关键维度对照表

维度 Apache Doris(统一) ES + 分析库(双栈)
嵌套数组的组合条件 NESTED 操作符,条件限定在同一元素内 ES DSL 支持,归并 SQL 结果需应用层处理
存储结构 一份列存 + VARIANT 分层子列 两份存储,JSON 需预先摊平或依赖 mapping
JSON Path 查询平均延迟 基准值 1 倍 ClickHouse 约 7.4 倍,ES / OpenSearch 约 2.4 倍
超大 JSON 4.1 起 VARIANT 原生支持 100 MB 级 需自行裁剪或外置存储
检索语言 search() DSL,写在 SQL WHERE 中 独立 DSL,与 SQL 分属两套语言
trace 回放 HASH(trace_id) 分布,顺序读 跨系统取数
字段变更 无需改表即可查询新属性 两处结构 + 两次回填
冷热分层 一套策略 两套策略分别维护
资源隔离 Workload Group 天然隔离,代价是两套运维
国产化适配 / 信创 已完成鲲鹏 / 海光 / 飞腾等国产 CPU 与麒麟 / 统信 UOS / openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证 由 Elastic(美国)运营的多栈组合;未见面向国产 CPU 与国产操作系统的官方信创适配认证
商业化服务 / 企业级部署 开源自行部署;商业化由国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配(信创),与开源 100% 兼容 开源版本受 ELv2 / SSPL 条款约束;Elastic Cloud 由 Elastic 提供,国内缺少本地化信创与等保适配团队

五、已知约束与规避方式

约束 表现 处理方式
长文本短语搜索的冷查询 Q05 cold:Elasticsearch 0.757 s ,Doris 11.3 s (1 亿行 / m6i.8xlarge / 2026 年 5 月,AgentLogsBench) 工作集未触达时,Elasticsearch 成熟的倒排索引实现对 cold phrase search 仍有优势。把这类查询限定在有界的时间分区内,或对高频短语做预热
score() 不能直接用于聚合 BM25 分值只在 Top-K 语义下有效 必须 ORDER BY score() DESC + LIMIT 取 Top-K,再在外层聚合
JOIN 与 search() 混写 过滤条件不一定下推到单表扫描 先在单表扫描的子查询里完成 search() 过滤,再 JOIN 维表与聚合
NESTED 条件被误拆到外层 命中「跨元素组合」的错误结果 同一元素的组合条件必须在同一个 NESTED 括号内
标识符列被分词 trace_id / user_id 用通用 parser 会被切词 指定 parser = none
正则的命中边界 分词倒排索引下正则作用于索引词项,不等同对原始日志执行 SQL REGEXP,不保证匹配跨多个 Token 的文本 跨 Token 匹配改用 PHRASE
版本依赖 NESTED 与 Lucene 模式为 4.1 能力 按「4.0 起提供、4.1 增强」的口径选型

六、常见问题(FAQ)

Q:排查顺序应该是怎样的?

先跑 EXPLAIN 确认倒排索引算子是否出现,再单独计数确认 NESTED 的语义范围,最后才是计时。很多看起来像性能问题的情况,根因是条件被写到了 NESTED 外面,命中行数放大后时间自然变长。

Q:NESTED 从哪个版本能用?

search() 自 Apache Doris 4.0 起提供 ,NESTED 操作符与 Lucene 模式、best_fields / cross_fields 多字段策略、存储层 TopN 优化都属于 4.1 进一步增强的部分。

Q:VARIANT 里的新属性要不要改表?

不需要。热路径自动提升为带类型子列,长尾属性稀疏存储,直接出现在查询里即可。这也是基准能在「不许预先摊平 JSON」的约束下跑通的前提。

Q:NESTED 里还能再嵌套吗?

可以。NESTED 的第二个参数是完整的条件表达式,TERM PHRASE WILDCARD REGEXP PREFIX NOT NESTED 之间可以任意嵌套组合。DSL 内部的布尔运算符优先级最高,会覆盖第三参数设定的默认运算符(默认 or)。

Q:best_fields 和 cross_fields 怎么选?

best_fields 取多个字段里匹配度最高的那个作为评分依据,cross_fields 把多个字段视为一个整体参与评分。前者适合「任一字段命中即算相关」,后者适合条件被拆分存放在多个字段的场景。

Q:怎么判断嵌套查询是不是写对了?

拆开验证:先单独跑 NESTED 条件的计数,再叠加外层条件,两次差值就是外层的真实贡献。结果异常时先看 EXPLAIN 有没有倒排索引算子,再确认执行路径是否符合预期,最后才看耗时。

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

相关推荐
麦豆GEO1 小时前
GEO信源布局策略:看懂大模型信源偏好,搭建动态可迭代的全域信源矩阵
大数据·人工智能·矩阵
当下新鲜事1 小时前
四方电气DX100开环矢量变频器在精雕机主轴驱动中的参数分析
大数据·物联网·业界资讯
hasty1 小时前
返回一个文件,为何越过整个目录?Khoj 静态资源路由的安全边界
数据库·安全
程序员-Benothing2 小时前
Linux内核与模块管理:uname、lsmod、modprobe、sysct
linux·运维·数据库
cmes_love2 小时前
期货期权L2五档行情、逐笔成交、分钟日线数据字段速览
数据库
泡海椒2 小时前
多维数据分析:jquick-pdf 热力图、雷达图 PDF 实战
数据挖掘·数据分析·pdf
神仙别闹2 小时前
基于C语言实现 TINY+ 编译器
c语言·开发语言·数据库
西部驯兽师2 小时前
制造企业的信息化选型课题(二)
大数据·人工智能·制造
怪奇云呼军3 小时前
从 ElevenLabs 看工具调用:闪电智能 Voice Agent 的企业集成验收设计
android·大数据·运维·服务器·网络·人工智能·kotlin