@Greptime: 所有 GreptimeDB 摄取 SDK 现在都可以写入 JSON2 列:Go、Java、Rust、TypeScript、.NET 和 Erlang。JSON2(GreptimeDB 1.2 中的新功能…
摘要
GreptimeDB 1.2 在所有摄取 SDK 中引入了 JSON2 列,允许将热门 JSON 路径存储为独立列,与之前的 JSONB 类型相比,实现了更高效的查询和更少的资源使用。
查看缓存全文
缓存时间: 2026/09/29 01:40
现在所有 GreptimeDB 数据摄入 SDK 均可写入 JSON2 列,包括 Go、Java、Rust、TypeScript、.NET 和 Erlang。JSON2(GreptimeDB 1.2 新增)将高频访问的 JSON 路径作为独立列存储,因此查询 http.status 时只需读取该列,而非每个完整文档。本文将深入解析其工作原理。
JSON 不再是黑箱:深入解析 GreptimeDB 1.2 的新型 JSON 类型
来源:https://greptime.com/blogs/2026-09-23-greptimedb-json2-deep-dive
日志、追踪和事件流中的 JSON 数据通常行间相似但极少完全相同,且多数分析查询仅涉及少数路径。例如统计 5xx 响应只需提取一个值:http.status。GreptimeDB 原始的 JSON 类型将整个文档编码为 JSONB 并作为单一二进制值存储。要读取 http.status,存储引擎需要读取每行的完整 JSONB 值,然后查询引擎逐一解析寻找字段。查询仅需一个字段,引擎却要读取解析整个文档,导致大量 I/O 和 CPU 被浪费在无关字段上。
当通常需要读取完整对象,或模式确实无法预测时,JSONB 仍是简单可靠的选择。但对于日志和追踪分析场景,其开销过大。
GreptimeDB 1.2(https://greptime.com/blogs/2026-09-08-greptimedb-v1-2-0-release)为此类数据新增 JSON2 类型。用户仍能看到完整的 JSON 对象,而存储和查询引擎则能通过路径剪枝访问结构化数据:高频路径存储为独立列,查询仅需读取所需列。
本文将先探讨 JSONB 的局限,再介绍 JSON2 的分层存储机制、基于查询推导类型的方法以及类型提示。文中示例均基于最新稳定版 v1.2.1(https://github.com/GreptimeTeam/greptimedb/releases/tag/v1.2.1)运行。
从全文档 JSONB 到结构化 JSON
JSONB 的存储单元是整个 JSON 文档。以统计 5xx 响应为例:
SELECT COUNT(*) FROM application_logs
WHERE json_get_int(attrs, 'http.status') >= 500;
对存储引擎而言,attrs 是二进制列。查询仅需 http.status,但存储引擎必须返回每行的完整 attrs 值才能提取目标字段。它无法只返回查询所需的片段。

为 JSONB 添加索引是常规解决方案。PostgreSQL JSONBench 测试结果展示了其实际效果。
PostgreSQL JSONB 在 JSONBench 的表现
JSONBench(https://jsonbench.com/)使用约十亿条来自 Bluesky 的真实 JSON 事件对数据库进行基准测试。该数据集字段繁多、嵌套层次深且行间模式差异显著。整体包含大量不同 JSON 路径,但每行仅涉及其中一小部分。这是典型的宽表稀疏半结构化数据。
PostgreSQL 16.6 在十亿行数据集上的测试结果显示:加载约 8.04 亿文档,磁盘数据占用约 512 GB,索引额外占用 148 GB,总计约 660 GB。五个查询的最佳结果耗时在 1.1 至 1.4 小时之间(PostgreSQL JSONBench 结果)。
这些数据更多反映了负载特性,而非 PostgreSQL JSONB 或 GIN 实现的质量。索引仅能指示可能匹配的行,无法改变 JSONB 按完整文档存储的事实。定位候选行后,PostgreSQL 仍需读取每个 JSONB 值并提取目标字段。JSONBench 查询需扫描或聚合大量匹配行,而 PostgreSQL 无法像列式数据库那样直接读取特定 JSON 路径的值,也无法单独压缩该路径或使用向量化执行处理。
GreptimeDB 早期的 JSONBench 提交(https://greptime.com/blogs/2025-03-18-jsonbench-greptimedb-performance)采用了不同策略:将高频 JSON 路径提取为普通表列。查询性能良好,但增加了额外存储开销且降低了表的易用性。JSON2 旨在解决双重困境:数据不再存储为单一 JSONB 二进制值,用户也无需预先提取公共字段到表列。
JSON2:基于静态列式格式的动态模式
JSON2 通过将路径扩展为独立列(在限定范围内)来存储 JSON,使查询能利用 GreptimeDB 的列式存储和查询引擎。
列式转换成本:每个 JSON 路径都可能成为列
GreptimeDB 存储引擎基于 Arrow 和 Parquet 构建,两者都要求静态模式。Arrow 的 StructArray 中每行共享相同字段集,Parquet 文件在页脚记录固定的列定义集。而 JSON 允许路径行间出现或消失,同一路径甚至可能改变类型。
JSON2 首要解决的问题是在静态容器中表示动态结构。最直接的方法是分解(shredding):将每个 JSON 路径转换为可独立读取的列。给定以下两个文档:
{"kind":"post","record":{"text":"hello"}}
{"kind":"like","record":{"subject":"at://example/post/1"}}
SST 文件可采用如下物理结构存储:
Struct<
kind: Utf8,
record: Struct<
subject: Utf8,
text: Utf8
>
>
查询 record.text 时只需读取对应的 Parquet 列,无需解析完整 JSON。每列独立压缩,过滤和聚合操作可通过向量化执行。
分解提升了读取速度,但未解决最终可能产生的列数问题。当 JSON 具有少量稳定字段时,扩展每个路径效果良好。但类似 JSONBench 的真实数据可能包含数千个路径,每行仅包含少数路径。每个新路径都会向 Arrow 模式和 Parquet 模式添加字段,两者很快变得难以管理。
以此方式存储时,JSON 的开销取决于整个数据集历史上包含的不同路径数量,而非每行写入的值数量。JSON 路径数量无界,而物理模式必须有界。每行包含少数路径,但数据集内的路径并集使物理模式异常宽泛。
JSON2 目前采用有界自动分解:对能获得独立列的路径数量设限,确保物理模式不会无限增长。超出限制的路径通过下文所述的分层存储处理。
分层存储:热路径获得列,长尾进入余量区
JSON 存储格式需支持两类访问:高频路径应存储在独立列中以获得可预测的读取性能;其他所有内容应存入有界的共享区域,使任何新字段都能写入而无需无限扩展 Arrow 和 Parquet 模式。
JSON2 将路径分为三层:
- 静态类型路径:通过类型提示显式声明。通常是类型稳定且查询频繁的路径,它们始终拥有独立的 Parquet 列。
- 动态路径:在预算内自动扩展。提供列式加速,但无法保证在整个表中固定不变。
- 余量路径:超出预算的长尾字段。写入标准 Parquet Variant,保留原始嵌套和值类型。
物理布局如下:
Struct<
静态类型字段,
动态字段,
remainder: Variant
>
自动扩展动态路径的上限(即“预算”)由 max_auto_expanded_paths 设置,默认值为 100。该参数位于 JSON2 列定义内,与类型提示并列:
attrs JSON2 (
max_auto_expanded_paths = 0,
trace_id STRING
)
预算为零时,仅通过类型提示声明的路径获得独立列,其余所有内容进入余量区。预算为 N 时,系统最多扩展 N 个额外动态路径。余量区使用 Parquet 的 Variant 类型,这是一种与旧版 JSONB 不同的格式。Variant 列能在单个有界列中存储不同结构和类型的值,代价是查询冷路径时需先读取余量区再提取值。
JSON2 目前接受冷路径的额外开销,以换取三个优势:物理模式有界、长尾字段永不丢失、热路径仍可下推至 Parquet。
在 GreptimeDB 1.2 中,类型提示和预算仅能在建表时设置。用于修改现有 JSON2 列设置的 ALTER TABLE ... MODIFY COLUMN 语法已合并到主分支(#9029)但未包含在 1.2.x 版本中。一旦可用,变为高频查询的冷路径可提升为静态类型路径。
无论路径最终属于哪一层,静态类型字段、动态字段和余量区共同构成完整的 JSON 文档,且每个路径仅出现在一处。

查询类型具体化:让查询决定结构
分层存储涵盖了 JSON 的写入方式,但查询引擎仍需知道如何读取。查询规划器通常从表模式开始,若要将 JSON 作为结构化数据读取,其结构必须作为表元数据的一部分。但 JSON 结构动态变化,难以在静态表元数据中一致表示。
JSON2 采用不同方法:直接从 SQL 语句推导所需的结构化类型。我们称之为查询类型具体化。查询引擎从 SQL 中收集两项信息:
- 查询访问的 JSON 路径;
- 每个路径预期返回的 SQL 类型。
例如:
SELECT data.commit.collection FROM bluesky
WHERE data.time_us > 1720000000000000;
查询引擎可推断仅需 commit.collection 和 time_us。前者可为字符串,后者用于整数比较。因此 data 列的结构化类型为:
Struct<
time_us: Int64,
commit: Struct<
collection: Utf8,
>
>
该类型足以支撑查询执行。存储引擎据此仅从 SST 中读取这些路径对应的列。
根本限制在于推导的类型完全来自 SQL 表达式。对于类型行间变化的动态数据,当值无法转换为推导类型时返回 NULL 是合理的。但问题在于推导本身可能出错。假设上述查询误写为:
SELECT data.commit.collection + 1 FROM bluesky
WHERE data.time_us > 1720000000000000;
+ 1 会使查询引擎推断 commit.collection 为数值类型,即使实际值都是字符串,导致查询无意义。
类型提示正是为解决此问题。
类型提示:固定 JSON 路径的类型
定义 JSON2 列时,可为特定 JSON 路径声明固定类型。下例中 attrs 列包含四个路径类型提示:
CREATE TABLE application_logs (
ts TIMESTAMP TIME INDEX,
attrs JSON2 (
trace_id STRING,
http.status BIGINT,
latency_ms DOUBLE,
error BOOLEAN DEFAULT false
)
) WITH (
append_mode = 'true'
);
含 JSON2 列的表必须设置 append_mode = 'true',否则 CREATE TABLE 会失败。类型提示是静态的,存储在表模式中。查询引擎推导 JSON 列的结构化类型时会使用类型提示。
在以下查询中,attrs.http.status 根据类型提示作为 BIGINT 读取,然后与整数 200 比较:
SELECT attrs.trace_id FROM application_logs
WHERE attrs.http.status = 200;
带类型提示的 JSON 路径始终存储为静态类型路径(参见前文分层存储部分),拥有独立列和最佳读取性能。我们建议为类型稳定且查询频繁的字段添加类型提示。
类型提示还附带约束:JSON2 会拒绝类型提示字段值类型不符的写入。例如,以下插入将 http.status 写为字符串:
INSERT INTO application_logs VALUES
(3, '{"trace_id":"8f3a1e","http":{"status":"oops"},"latency_ms":1.0}');
将失败并返回:
Invalid JSON: JSON value at http.status does not match JSON2 type hint Int64
SQL 语法:像访问常规结构体一样访问 JSON
访问 JSON2 最直接的方式是点号语法:
SELECT attrs.trace_id, attrs.http.status, attrs.latency_ms
FROM application_logs
WHERE attrs.http.status >= 500;
JSON 路径可出现在 SELECT、WHERE、GROUP BY 及普通表达式中。比较和算术运算同样为查询类型具体化提供预期类型。
当需要精确控制返回类型并避免无意义查询时,可使用 json_get 配合类型转换:
SELECT json_get(attrs, 'http.path')::STRING AS path,
AVG(json_get(attrs, 'latency_ms')::DOUBLE) AS avg_latency_ms
FROM application_logs
GROUP BY json_get(attrs, 'http.path')::STRING;
两种形式查询能力相同。点号语法适合固定的可读字段路径;json_get 适合显式类型转换和生成式查询。
完整示例:
CREATE TABLE application_logs (
ts TIMESTAMP TIME INDEX,
attrs JSON2 (
trace_id STRING,
http.status BIGINT,
latency_ms DOUBLE,
error BOOLEAN DEFAULT false
)
) WITH (
append_mode = 'true'
);
INSERT INTO application_logs VALUES
(1, '{"trace_id":"8f3a1c","http":{"method":"POST","path":"/v1/orders","status":200},"latency_ms":42.8}'),
(2, '{"trace_id":"8f3a1d","http":{"method":"POST","path":"/v1/orders","status":500},"latency_ms":71.2,"error":true}');
SELECT attrs.http.path AS path,
COUNT(*) AS requests,
SUM(CASE WHEN attrs.error THEN 1 ELSE 0 END) AS errors
FROM application_logs
GROUP BY attrs.http.path;
预期输出:
+------------+----------+--------+
| path | requests | errors |
+------------+----------+--------+
| /v1/orders | 2 | 1 |
+------------+----------+--------+
path 和 error 都是 JSON 内部字段,但过滤和分组操作基于具体 SQL/Arrow 类型值。无需逐行反序列化完整 JSONB 文档。
总结与后续步骤
相似文章
@Greptime: JSON2 已在 GreptimeDB v1.2.0 中发布。何时应使用它而非 JSON?JSON 与 JSON2:下方有视觉指南。JSON2 仍在测试版中。
JSON2 是 GreptimeDB v1.2.0 中的一种新型 JSON 类型,专为日志和半结构化数据的高效存储和查询设计,采用列式优化。目前处于测试版,支持仅追加表。
@Greptime: GreptimeDB v1.2.0-beta.1 发布。205 项更改,259 次提交,22 位贡献者。重点内容:JSON2 作为数据类型,P…
GreptimeDB v1.2.0-beta.1 已发布,主要特性包括:JSON2 作为结构化列类型、支持原生直方图的 Prometheus Remote Write v2、字典编码的序列键以加快查询速度,以及其他加固和破坏性更改。
@Greptime:更多生产部署正在重塑 GreptimeDB 的路线图。我们正投入更多精力于稳定性、性能、安全……
本文回顾了 GreptimeDB 2026 路线图的年中进展,重点介绍了已发布的功能,如在线重新分区和 JSON2 类型,并解释了由于生产部署导致的优先级变化。
@Greptime: GreptimeDB 六月的大部分工作归结为一个想法:如果过滤器无法到达数据,那么它就没用。在分布式查询中…
GreptimeDB 通过启用远程动态过滤器在运行时下推到 datanode 扫描,并优化优化器使其在 MergeScan 包装远程计划之前运行,从而确保过滤器到达数据,提升了分布式查询性能。JSON v2 列现在支持类型提示。
@Greptime: GreptimeDB 的扁平格式查询现在可以在任何列上预过滤——标签、字段、时间戳——而不仅仅是主键。而…
GreptimeDB 的扁平格式查询现在支持在任何列(标签、字段、时间戳)上预过滤,而不仅仅是主键,性能提升高达 4.5 倍。此外,mito2 存储引擎移除了其遗留扫描路径,清理了约 1800 行代码。