@Greptime: 所有 GreptimeDB 摄取 SDK 现在都可以写入 JSON2 列:Go、Java、Rust、TypeScript、.NET 和 Erlang。JSON2(GreptimeDB 1.2 中的新功能…

X AI KOLs Following 产品

摘要

GreptimeDB 1.2 在所有摄取 SDK 中引入了 JSON2 列,允许将热门 JSON 路径存储为独立列,与之前的 JSONB 类型相比,实现了更高效的查询和更少的资源使用。

所有 GreptimeDB 摄取 SDK 现在都可以写入 JSON2 列:Go、Java、Rust、TypeScript、.NET 和 Erlang。 JSON2(GreptimeDB 1.2 中的新功能)将热门 JSON 路径存储为自己的列,因此对 http.status 的筛选会读取该列,而不是每个完整文档。 工作原理如下:
查看原文
查看缓存全文

缓存时间: 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 vs JSON2 读取模式对比](JSONB 为每行读取完整文档;JSON2 仅读取查询路径对应列)

为 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 将路径分为三层:

  1. 静态类型路径:通过类型提示显式声明。通常是类型稳定且查询频繁的路径,它们始终拥有独立的 Parquet 列。
  2. 动态路径:在预算内自动扩展。提供列式加速,但无法保证在整个表中固定不变。
  3. 余量路径:超出预算的长尾字段。写入标准 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 文档,且每个路径仅出现在一处。

![JSON2 分层存储架构](JSON2 分层存储:静态类型路径、预算内的动态路径,以及长尾数据的 Parquet Variant 余量区)

查询类型具体化:让查询决定结构

原文链接

分层存储涵盖了 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 文档。

总结与后续步骤

原文链接

相似文章