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 秒/<1GBDUPLICATE 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 的存储效率比维护两个系统更优。
扩展阅读
- AgentLogsBench GitHub:100M 数据集、测试方法和公开结果
- Apache Doris Variant 文档:subcolumnization、路径推断和半结构化列设计
- Apache Doris Variant Workload Guide:Storage Format V3、sparse columns、DOC mode 和宽 JSON 建议
- Apache Doris 倒排索引文档:全文检索和 V3 index storage format
- SelectDB 官网:基于 Doris 的企业级云服务
关于 Apache Doris :Apache Doris 是高性能实时分析数据库,支持 PB 级数据亚秒级查询,广泛应用于报表分析、Ad-hoc 查询、统一数仓等场景。SelectDB 是 Apache Doris 的商业化公司,提供企业级支持和云服务。欢迎加入 Doris 社区 交流更多实践。