在工业现场做时序数据库选型,最容易卡在两头:存量采集代码是按「设备树」组织的,新接进来的数仓与报表同事却只想写标准 SQL 按表查,两边都不愿改,最后常常退化成同时维护两套库。
常规做法要么全走树模型、再用视图硬模拟表,要么全改表模型、把设备台账逻辑重写一遍;文件层面还常常停在 Parquet 或各家私有格式,历史数据回补要先导出转换,精度和时区一旦对不上账就很难查。
本文分享一套以 Apache IoTDB 2.0 为底座的落地拆解:一套统一存储引擎 + 树/表两种数据模型 + 一种 TsFile 文件格式,配上可直接复制的建库建表、写入与部署命令。
一、选型卡点:模型割裂与文件格式不通用
真正让人犹豫的从来不是性能数字,而是三处割裂:
- 树模型习惯:按 root.库.设备.测点 组织,天然贴合设备台账,网关按设备对齐写入最省事。
- 表模型习惯:按 库.表(标签, 时间, 字段) 组织,BI、Spark、报表工具直接对接,写 SQL 的人零学习成本。
- 文件层割裂:在线库和离线分析用不同列式格式时,回补历史数据要走一遍导出转换,容易丢精度、丢时区。
把两种诉求摆到同一张表里看,会发现差异只在「入口视角」:
| 维度 | 树模型 | 表模型 |
|---|---|---|
| 组织方式 | root.factory.line1.d01.temperature | plant.line1(device_id TAG, ts TIME, temperature FIELD) |
| 查询入口 | 设备路径 + SQL 扩展 | 标准 SQL,按标签过滤 |
| 主要角色 | 采集网关、设备管理 | 数仓、BI、报表 |
| 落盘文件 | TsFile | 同一套 TsFile |
核心结论: 模型差异是视角差异,不是两套数据库------这正是 2.0 敢把它们放进同一个引擎的前提。
二、一套引擎:元数据与存储链路怎么统一
两套模型能共存,靠的是引擎层把差异收敛到元数据与同一条读写链路:
- 元数据统一:树模型的测点与表模型的列登记在同一份 Catalog 里,库、用户、权限、时间分区只管一次。
- 写入统一:无论哪套语法,最终都先写 WAL、再进 MemTable,攒够阈值刷成 TsFile。
- 读取统一:查询计划先按时间分区裁剪,再用 TsFile 内嵌统计做块级过滤,最后归并出结果。
整条链路可以压缩成一行:
SQL 解析 → 统一计划与优化 → WAL 落日志 → MemTable 累积 → Flush 生成 TsFile → 读时归并
核心结论: 引擎只有一套,模型只是入口,落盘只有一种文件。

三、树模型落地:设备路径写入与对齐查询
树模型适合设备台账清晰、按台采集的现场,落地时抓住三点:
- 先建库再建点:测点声明 DATATYPE / ENCODING / COMPRESSION,把编码与压缩前置到写入之前。
- 设备对齐写:一条 INSERT 带上同一设备的多个测点,减少网络往返,吞吐比逐点插高一个量级。
- 查询限时间窗:查询带上时间范围才能吃到分区裁剪,别写无边界的全路径扫描。
下面这段在 CLI 里可直接执行(默认账号 root/root,语法以所用小版本文档为准):
bash
# 进入 CLI
./sbin/start-cli.sh -h 127.0.0.1 -p 6667 -u root -pw root
# 建库 + 建测点:编码与压缩在建点时就定好
CREATE DATABASE root.factory.line1;
CREATE TIMESERIES root.factory.line1.d01.temperature DATATYPE=DOUBLE, ENCODING=GORILLA, COMPRESSION=ZSTD;
CREATE TIMESERIES root.factory.line1.d01.status DATATYPE=BOOLEAN, ENCODING=PLAIN;
# 设备对齐批量写入
INSERT INTO root.factory.line1.d01(timestamp, temperature, status)
VALUES (1710000000000, 36.5, true), (1710000060000, 36.8, false);
# 带时间窗的查询
SELECT temperature, status FROM root.factory.line1.d01 WHERE time > now() - 3600000;
核心结论: 声明式压缩编码 + 设备对齐写入,是树模型吞吐的两个主要来源。
四、表模型落地:关系建表与标签字段设计
表模型让数仓同事用熟悉的 SQL 直接接进来,注意三件事:
- 三类列分工:TAG 做等值过滤,TIME 是唯一时间列,FIELD 才是随时间变化的测点。
- 控制标签基数:工单号、批次号这类高基数值塞进 TAG 会让元数据膨胀,只留真正高频过滤的列。
- 复用同一通道:同一套账号、同一个 Session,迁移存量代码主要改 DDL 与 INSERT。
下面用 Python 官方 Session 库演示建表、写入与查询:
python
from iotdb.Session import Session
s = Session(host="127.0.0.1", port=6667, username="root", password="root")
s.open()
# 关系式建表:TAG / TIME / FIELD 三类列
s.executeNonQueryStatement("""
CREATE TABLE plant.line1 (
device_id STRING TAG,
ts TIMESTAMP TIME,
temperature DOUBLE FIELD,
status BOOLEAN FIELD
)
""")
# 表模型按表 + 标签写入
s.insertRecord("plant.line1", ["d01"], 1710000000000, ["temperature", "status"], [36.5, "true"])
s.insertRecord("plant.line1", ["d01"], 1710000060000, ["temperature", "status"], [36.8, "false"])
rs = s.executeQueryStatement("SELECT temperature FROM plant.line1 WHERE device_id='d01' ORDER BY ts DESC LIMIT 10")
while rs.next():
print(rs.row_to_string())
s.close()
两种模型的差异可以压缩成下面这张对照表:
| 事项 | 树模型 | 表模型 |
|---|---|---|
| 命名空间 | root.factory.line1.d01 | plant.line1 |
| 设备标识 | 路径中的一段 | device_id TAG |
| 时间戳 | 路径下的时间轴 | ts TIMESTAMP TIME |
| 批量写入 | INSERT INTO 路径 | INSERT INTO 表(列) |
核心结论: 换了建表语法,没换引擎,也没换落盘的文件。

五、一种文件:TsFile 的落盘结构与生态复用
所有模型最终都落成 TsFile,这让在线查询和离线分析第一次对上账:
- 时序原生布局:按时间分区、分块(chunk)存放,块内保留时间与数值统计,谓词下推能直接跳过整块。
- 压缩编码内置:lz4/zstd 压缩叠加 GORILLA、RLE、TS_2DIFF 编码,高频双精度测点能压到很低的体积。
- 文件可搬运:一个 .tsfile 就是一份自洽数据,离线生成后直接 LOAD 回集群,不必重放网络流量。
离线文件回补集群只需一条命令:
bash
# 把离线生成的 TsFile 直接灌进集群(历史回补、跨集群搬运通用)
./sbin/start-cli.sh -e "LOAD '/export/backfill/2024-03-15.tsfile' INTO root.factory.line1;"
# 落盘形态:时间分区目录下成对出现主文件与 .mods 增量文件
ls data/datanode/data/full/region-0/0/0/0
核心结论: 一次落盘、多处复用:在线查询、离线回补、跨集群搬运都是同一个文件。
和通用列式格式的差异也值得列清楚:
| 对比项 | TsFile | Parquet / ORC |
|---|---|---|
| 时间分区 | 原生按时间分区 | 需自行分目录约定 |
| 块级统计 | 时间 + 数值双维统计 | 以列统计为主 |
| 增量更新 | .mods 记录删除与覆盖,读时归并 | 通常需重写文件 |
| 复用方式 | 本地库、Spark/Flink 直读 | 通用大数据生态 |
六、部署压测:从单机双进程到集群最小拓扑
落地时先把拓扑定死,再谈调参与压测,顺序反了数字就没有可比性:
- 单机两进程:ConfigNode 管元数据与成员关系,DataNode 管数据,先在一台机器跑通再横向扩。
- 奇数仲裁组:ConfigNode 3 个起步做元数据仲裁,DataNode 按写入带宽线性增加。
- 压测用官方工具:iotdb-benchmark 按「设备数 × 测点数 × 采集周期」造数,比手写脚本更接近现场。
bash
# 单机起步:先起元数据节点,再起数据节点
./sbin/start-confignode.sh -d
./sbin/start-datanode.sh -d
# 集群扩展:给 3 台机器拉起 ConfigNode 组
for ip in 10.0.0.11 10.0.0.12 10.0.0.13; do
ssh "$ip" "/opt/iotdb/sbin/start-confignode.sh -d -cn cn1"
done
# 按现场节奏造数压测:设备 2 万、每台 20 测点、1 秒一个采集周期
./tools/iotdb-benchmark.sh -td 20000 -t 20 -p 1000 -ts COMPRESSED
压测与上线阶段最常见的三类问题,可以直接对照下表处置:
| 症状 | 原因 | 处置 |
|---|---|---|
| 建表后提示库不存在 | 表模型库未创建,或库名带了 root. 前缀 | 先 CREATE DATABASE,再切换到该库执行 |
| 写入慢且 WAL 涨得快 | 逐点逐行写入,网络往返过多 | 改为设备对齐或按表批量 INSERT |
| 磁盘占用高于预期 | 小文件过多、合并未跟上 | 调大 memtable 刷盘阈值,观察合并任务 |
核心结论: 先拓扑、后参数、再压测,写入吞吐和查询延迟才有可复现的基线。

结语
回看这套方案,价值不在某一个单点功能,而在于把选型时最耗人的三件事一次性收拢:写入侧不用在树模型与表模型之间二选一,同一套引擎、同一份元数据、同一套权限都能承载;数据侧不必再为在线库和离线分析准备两套格式,TsFile 既能被集群直接查询,也能被离线工具读走;运维侧从单机双进程到 3 节点仲裁组的路径是确定的,压测数字可以横向对比。
对正在做工业物联网平台的团队来说,这套结构能直接省掉「视图模拟表」「导出转换回补」「双库对账」三类长期成本。
一套引擎、两种模型、一种文件:把选型分歧收敛成架构共识。