时序数据库经常面对一个现实问题:数据源比数据库 schema 变化得更快。
一批设备固件升级后多上报了 firmware tag;一个采集器从只报 temperature 扩展到同时上报 humidity;某个计数字段过去一直是整数,后来因为计算方式变化开始出现小数。如果每一次变化都要求先人工执行 DDL,再恢复写入链路,接入体验就会变得很脆。
SonnetDB 新增了受控的 schema-on-write 能力:写入路径可以在入库时自动创建 measurement,也可以为已有 measurement 自动补齐缺失的 TAG / FIELD。同时,它并不是完全无约束的 schema-less。已有列仍然会做角色和类型兼容校验,避免数据悄悄漂移成不可查询的状态。
这次改动覆盖四条主写入路径:
INSERT INTO ... VALUES也就是说,下面这种首次写入可以直接创建 schema:
INSERT INTO weather (time, station, temperature, humidity)
VALUES (1713676800000, 'beijing', 28.5, 61.2)
SonnetDB 会推断出:
CREATE MEASUREMENT weather (
station TAG,
temperature FIELD FLOAT,
humidity FIELD FLOAT
)
如果之后又写入新列:
INSERT INTO weather (time, station, firmware, pressure)
VALUES (1713676860000, 'beijing', '1.2.0', 1008.5)
已有 measurement 会被扩展为新增 firmware TAG 和 pressure FIELD FLOAT。
Line Protocol 和 JSON points 本身就区分 tags 与 fields,所以自动补 schema 非常直接。
Line Protocol:
weather,station=beijing,firmware=1.2.0 temperature=28.5,humidity=61.2 1713676800000
JSON points:
{
"m": "weather",
"points": [
{
"t": 1713676800000,
"tags": { "station": "beijing", "firmware": "1.2.0" },
"fields": { "temperature": 28.5, "humidity": 61.2 }
}
]
}
在这两种格式中:
tags 里的新 key 自动追加为 TAG STRINGfields 里的新 key 按值类型追加为 FIELD FLOAT / FIELD INT / FIELD BOOL / FIELD STRING这让 SonnetDB 更接近 InfluxDB Line Protocol、TDengine schemaless、QuestDB ILP 这类接入体验:先让数据流进来,再由数据库维护可查询的 schema。
SQL 的难点是:列名本身并不告诉数据库它是 tag 还是 field。
例如:
INSERT INTO cpu (time, host, usage)
VALUES (1, 'server-01', 0.72)
如果 cpu 还不存在,host 是字符串,usage 是浮点数。SonnetDB 当前采用一个简单且可解释的规则:
TAGFIELD因此上面的 SQL 会推断为:
CREATE MEASUREMENT cpu (
host TAG,
usage FIELD FLOAT
)
如果你确实需要一个字符串 field,比如 status FIELD STRING,建议先显式建表:
CREATE MEASUREMENT device_state (
device TAG,
status FIELD STRING
)
这样后续 INSERT 会严格按 schema 写入,不会把 status 推断成 tag。
自动补列不等于允许任意类型漂移。SonnetDB 当前支持一个明确的数值提升规则:
FLOAT 字段接收整数值:保持 FLOAT,写入前转换成浮点INT 字段接收浮点值:schema 提升为 FLOATFLOAT 不会降级为 INTBOOL、STRING、VECTOR、GEOPOINT 与其它类型之间不会自动互转示例:
INSERT INTO meter (time, device, reading)
VALUES (1, 'm1', 100);
首次写入后,reading 会是 FIELD INT。
后来出现小数:
INSERT INTO meter (time, device, reading)
VALUES (2, 'm1', 100.5);
SonnetDB 会把 reading 的 schema 提升为 FIELD FLOAT。之后查询 SQL 投影时,旧的整数值也会按浮点结果显示,避免应用层看到同一列一会儿是整数、一会儿是浮点。
这次实现里一个关键约束是:schema 变更必须先持久化,再写 WAL 和数据。
如果反过来,数据库可能在崩溃恢复后遇到一种尴尬状态:
WAL 中已经有新字段数据
measurements.tslschema 中却没有这个字段
这样数据虽然存在,但 SQL 查询、DESCRIBE MEASUREMENT、Copilot schema 感知、MCP schema resource 都可能看不到它。
现在 SonnetDB 的写入顺序是:
推断缺失列
更新 measurement schema
持久化 measurements.tslschema
写 WAL
写 MemTable
这样即使在 schema 保存后、WAL 写入前崩溃,最多只是留下一个还没有数据的新列;不会出现“数据存在但 schema 不可见”的情况。
SonnetDB 仍然坚持“受控 schema-on-write”,不是完全 schema-less。
这样设计有几个好处:
SELECT * 可以稳定展开 time + tags + fieldsWHERE tag = 'value' 可以继续依赖 tag schema 做校验和索引过滤换句话说,写入体验更像 schemaless,查询体验仍然保持 schema-first。
对于设备接入、边缘采集、日志转指标、Prometheus / OTLP 这类数据源,可以放心使用自动 schema 演进,让接入链路先跑起来。
对于业务核心表、字符串字段较多的 measurement、或者需要稳定对外 API 的场景,仍然建议显式执行 CREATE MEASUREMENT。这样可以避免 SQL 字符串列被默认推断成 tag,也能让 schema 设计更可审查。
一个实用的折中方式是:
CREATE MEASUREMENT cpu (
host TAG,
region TAG,
usage FIELD FLOAT
)
之后让自动 schema-on-write 只处理少量新增字段:
INSERT INTO cpu (time, host, region, usage, load1)
VALUES (1713676800000, 'server-01', 'cn-hz', 0.72, 1.8)
load1 会自动追加为 FIELD FLOAT,而 host / region / usage 仍按原 schema 校验。
这次 schema-on-write 升级让 SonnetDB 的写入体验更贴近真实时序数据接入:数据源可以逐步演进,数据库自动补齐缺失的 tag 与 field;同时,已有列的角色和类型仍然受到保护。
它解决的是“接入过程中的 schema 摩擦”,不是鼓励完全无约束的数据漂移。对于嵌入式时序数据库来说,这个平衡很重要:写入要顺手,查询也要可靠。