Apache Doris Variant 动态 JSON 实战教程:从建表到跑通 100M Agent 日志查询

Agent 日志的 payload 字段千变万化,传统数据库查动态 JSON 常常慢到令人抓狂。这篇教程手把手带你用 Apache Doris 的 Variant 类型,在 100M 行 Agent 日志场景下跑通 JSON Path 查询,实测比 ClickHouse 快 7 倍。

关键词:Apache Doris · Variant · 动态 JSON · Agent 可观测性 · JSON Path 查询 · AgentLogsBench


第一步:理解为什么 Agent 日志需要 Variant

Agent 日志的核心痛点:payload 字段无法提前固定 schema。

一条 Agent observation 的 payload 可能包含:

JSON 复制代码
{
  "payload": {
    "provider": {
      "stop_reason": "tool_use",
      "cache_hit": false
    },
    "tool_result": {
      "stderr": "unable to open ...",
      "exit_code": 1
    },
    "otel": {
      "status_code": "ERROR"
    }
  }
}

不同 Agent、模型、工具会持续引入新字段,payload 越来越宽。传统方案要么提前定义所有列(不可能),要么把 JSON 存成字符串再解析(太慢)。

Doris Variant 的思路:写入时自动解析 JSON Path,推断类型,把高频路径拆成列式 subcolumns;查询时仍然写 JSON Path 语法,但执行器命中列式存储。

简单类比:传统 JSON 查询像在大文件里逐行翻找;Variant 则把常用路径提前"铺成路",走新路更快。

第二步:建表------Variant 列如何定义

创建一张 Agent observations 表,payload 列使用 VARIANT 类型:

SQL 复制代码
CREATE DATABASE IF NOT EXISTS agent_obs;

USE agent_obs;

CREATE TABLE agent_observations (
    event_time    DATETIME           NOT NULL COMMENT '事件时间',
    trace_id      VARCHAR(64)        NOT NULL COMMENT 'Trace ID',
    span_id       VARCHAR(64)        COMMENT 'Span ID',
    type          VARCHAR(32)        NOT NULL COMMENT '类型:GENERATION/TOOL/RETRIEVAL',
    model_name    VARCHAR(128)       COMMENT '模型名称',
    latency_ms    DOUBLE             COMMENT '延迟(ms)',
    total_cost    DOUBLE             COMMENT '总成本',
    input_tokens  INT                COMMENT '输入 Token 数',
    output_tokens INT                COMMENT '输出 Token 数',
    payload       VARIANT            COMMENT '动态 JSON payload',
    content       TEXT               COMMENT '内容文本'
)
DUPLICATE KEY(event_time, trace_id)
DISTRIBUTED BY HASH(trace_id) BUCKETS 32
PROPERTIES (
    "replication_num" = "1",
    -- 关键:启用 Segment V3,优化宽 Variant 场景的元数据加载
    "storage_format" = "V3"
);

关键参数解释

  • payload VARIANT:核心列,不需要预定义 JSON 内部结构
  • "storage_format" = "V3":Variant 会生成大量 subcolumns,V3 把列元数据从 Footer 外置到 CMR,按需加载,避免打开 Segment 时全量解析。实测 7000 列场景下,V2 打开 65 秒/60GB → V3 打开 4 秒/<1GB
  • DUPLICATE KEY:Agent 日志不需要主键更新,用 Duplicate 模型写入最快

第三步:导入数据------100M 行 Agent 日志

使用 Stream Load 导入 CSV/JSON 数据:

bash 复制代码
# JSON 格式导入
curl --location-trusted \
  -u root: \
  -H "format: json" \
  -H "strip_outer_array: true" \
  -T agent_observations.json \
  "http://127.0.0.1:8030/api/agent_obs/agent_observations/_stream_load"

或者用 Broker Load 从对象存储批量导入:

SQL 复制代码
LOAD LABEL agent_obs.load_100m
(
    DATA INFILE("s3://your-bucket/agent_logs/*.parquet")
    INTO TABLE agent_observations
    FORMAT AS "parquet"
    (event_time, trace_id, span_id, type, model_name,
     latency_ms, total_cost, input_tokens, output_tokens,
     payload, content)
)
WITH BROKER "hdfs_broker"
PROPERTIES (
    "max_filter_ratio" = "0.01"
);

导入完成后确认行数:

SQL 复制代码
SELECT COUNT(*) FROM agent_obs.agent_observations;
-- 预期结果:100,000,000

第四步:跑通 JSON Path 查询------6 个典型场景

Q07:按 provider.stop_reason 统计

SQL 复制代码
SELECT
    CAST(payload['provider']['stop_reason'] AS STRING) AS stop_reason,
    COUNT(*) AS cnt
FROM agent_observations
WHERE type = 'GENERATION'
GROUP BY CAST(payload['provider']['stop_reason'] AS STRING)
ORDER BY cnt DESC
LIMIT 20;

说明:CAST(payload['path'] AS STRING) 是 Doris 查询 Variant 子路径的标准语法。执行器会自动命中内部 subcolumns,不需要手动建列。

Q10:按 release_ring + customer_tier 多维聚合

SQL 复制代码
SELECT
    CAST(payload['attr']['release_ring'] AS STRING) AS release_ring,
    CAST(payload['attr']['customer_tier'] AS STRING) AS customer_tier,
    COUNT(*) AS observations,
    AVG(latency_ms) AS avg_latency_ms,
    SUM(total_cost) AS total_cost
FROM agent_observations
WHERE type IN ('GENERATION', 'TOOL', 'RETRIEVAL')
GROUP BY
    CAST(payload['attr']['release_ring'] AS STRING),
    CAST(payload['attr']['customer_tier'] AS STRING)
ORDER BY observations DESC
LIMIT 50;

这是 AgentLogsBench Q12 对应查询。在 100M 数据下,Doris 中位延迟约 0.032s,ClickHouse 约 7.4 倍于此。

Q12:错误分布统计

SQL 复制代码
SELECT
    CAST(payload['otel']['status_code'] AS STRING) AS status_code,
    CAST(payload['tool_result']['exit_code'] AS INT) AS exit_code,
    COUNT(*) AS error_count,
    AVG(latency_ms) AS avg_latency
FROM agent_observations
WHERE CAST(payload['otel']['status_code'] AS STRING) = 'ERROR'
GROUP BY
    CAST(payload['otel']['status_code'] AS STRING),
    CAST(payload['tool_result']['exit_code'] AS INT)
ORDER BY error_count DESC
LIMIT 50;

Q16:按 cache_hit 分析成本差异

SQL 复制代码
SELECT
    CAST(payload['provider']['cache_hit'] AS BOOLEAN) AS cache_hit,
    AVG(total_cost) AS avg_cost,
    AVG(latency_ms) AS avg_latency,
    COUNT(*) AS cnt
FROM agent_observations
WHERE type = 'GENERATION'
GROUP BY CAST(payload['provider']['cache_hit'] AS BOOLEAN)
ORDER BY avg_cost DESC;

Q19:全文检索 + JSON Path 联合查询

需要先给 content 列建倒排索引:

SQL 复制代码
-- 创建倒排索引
ALTER TABLE agent_observations
ADD INDEX idx_content (content) USING INVERTED
PROPERTIES ("parser" = "standard");

-- 构建索引
BUILD INDEX idx_content ON agent_observations;

查询示例:

SQL 复制代码
SELECT
    CAST(payload['attr']['release_ring'] AS STRING) AS release_ring,
    COUNT(*) AS cnt,
    SUM(total_cost) AS total_cost
FROM agent_observations
WHERE content MATCH 'timeout error'
  AND type = 'GENERATION'
GROUP BY CAST(payload['attr']['release_ring'] AS STRING)
ORDER BY total_cost DESC
LIMIT 20;

Q19 Doris 延迟 1.013s,ClickHouse 30.816s ------ 这是全文检索 + JSON 聚合的组合场景,Doris 优势明显。

Q20:跨字段成本聚合

SQL 复制代码
SELECT
    CAST(payload['attr']['customer_tier'] AS STRING) AS tier,
    CAST(payload['provider']['stop_reason'] AS STRING) AS stop_reason,
    SUM(total_cost) AS total_cost,
    AVG(latency_ms) AS avg_latency,
    COUNT(*) AS cnt
FROM agent_observations
WHERE CAST(payload['attr']['customer_tier'] AS STRING) IN ('premium', 'enterprise')
GROUP BY
    CAST(payload['attr']['customer_tier'] AS STRING),
    CAST(payload['provider']['stop_reason'] AS STRING)
ORDER BY total_cost DESC
LIMIT 50;

第五步:性能对比------Doris vs ClickHouse vs Elasticsearch

基于 AgentLogsBench 100M 公开结果,JSON Path 查询集合(Q07-Q20)的中位延迟对比:

查询 Doris ClickHouse Elasticsearch Doris 优势倍数
Q07 (stop_reason) ~0.03s ~0.22s ~0.08s 7.4× vs CH
Q10 (多维聚合) ~0.03s ~0.22s ~0.08s 7.4× vs CH
Q12 (错误统计) ~0.032s ~0.24s ~0.08s 7.4× vs CH
Q16 (cache_hit) ~0.04s ~0.30s ~0.09s 7.4× vs CH
Q19 (全文+JSON) 1.013s 30.816s ~1.0s 30× vs CH
Q20 (跨字段成本) ~1.5s ~11s ~3.6s 7.4× vs CH

核心结论:ClickHouse 在 JSON Path 聚合查询上平均延迟约为 Doris 的 7.4 倍,Elasticsearch约为 2.4 倍。但 ES 的存储占用是 Doris 的约 3 倍。

第六步:存储优化建议

1. 启用 Segment V3(宽 Variant 场景必须)

SQL 复制代码
-- 新建表默认 V3
CREATE TABLE ... PROPERTIES ("storage_format" = "V3");

-- 已有表切换(需做数据迁移)
ALTER TABLE agent_observations SET ("storage_format" = "V3");

V3 的核心收益:把 ColumnMetaPB 从 Footer 外置到 CMR,Variant 路径索引加速定位。

2. 设置 Variant sparse columns 阈值

SQL 复制代码
-- 控制哪些路径列式化,哪些进入 sparse storage
ALTER TABLE agent_observations SET (
    "variant_sparse_threshold" = "100"
);

超过阈值的低频路径会进入 sparse columns 或 DOC encoding,不影响热点路径的列式查询速度。

3. 建倒排索引(全文检索场景)

SQL 复制代码
ALTER TABLE agent_observations
ADD INDEX idx_content (content) USING INVERTED
PROPERTIES ("parser" = "standard");

Doris 同时支持全文检索(倒排索引)和 JSON Path 聚合(Variant),一个引擎解决搜索 + 分析两种需求。

常见问题

Q:Variant 和普通 JSON 列有什么区别?

A:JSON 类型保留原始 JSON 结构,查询时需要实时解析。VARIANT 写入时自动推断路径和类型,把高频字段拆成列式 subcolumns,查询走列裁剪 + 向量化执行。对于动态 payload 场景,Variant 性能远优于 JSON 类型。

Q:payload 字段超过几百条路径怎么办?

A:Doris 会自动区分热点路径和长尾路径。热点路径列式化,长尾路径进入 sparse columns。配合 Segment V3 的路径索引,查询 payload['user']['name'] 时不需要扫描全部路径。

Q:已有表用的是 V2,怎么迁移到 V3?

A:新表默认 V3。已有 V2 表可以通过数据迁移(insert into select)或 compaction 逐步转换为 V3 格式。参考 Doris 4.x 存储格式文档

Q:存储占用比 ClickHouse 大,怎么评估?

A:Doris 存储介于 ClickHouse 和 ES 之间,约为 ES 的三分之一。Doris 的取舍是同时支持 Variant 列式查询 + 倒排索引 + SQL 聚合,不是纯列式压缩。如果你同时需要搜索和分析,Doris 的存储效率比维护两个系统更优。

扩展阅读

关于 Apache Doris :Apache Doris 是高性能实时分析数据库,支持 PB 级数据亚秒级查询,广泛应用于报表分析、Ad-hoc 查询、统一数仓等场景。SelectDB 是 Apache Doris 的商业化公司,提供企业级支持和云服务。欢迎加入 Doris 社区 交流更多实践。

相关推荐
sg_knight2 小时前
MySQL 存储过程详解:从入门到实战
android·数据库·mysql·database·dba·关系型数据库·db
星空露珠3 小时前
28种颜色对应名称,
开发语言·数据库·算法·游戏·lua
踏月的造梦星球4 小时前
DM8 DSC 单机双实例部署
运维·开发语言·数据库
Database_Cool_4 小时前
企业级多模态分析计算引擎选型:首选 AnalyticDB MySQL 向量 + SQL + 实时一体化方案
数据库·sql·mysql
SelectDB4 小时前
从 Oracle、Hadoop 到 Apache Doris:哥瑞利泛半导体智能制造数据平台实践
数据库
ihuyigui4 小时前
海外签收通知短信接口
android·java·开发语言·前端·数据库·后端
海棠Flower未眠4 小时前
SpringBoot 消息死信队列(荣耀典藏版)
java·数据库·spring boot
Y3815326624 小时前
SERP API + Redis 缓存层:4 种方案对比与选型
数据库·redis·缓存
用户7783366132115 小时前
serpbase + GraphQL wrapper 实战:让 SERP 数据走 GraphQL schema
数据库·api