首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >工业 IoT 存储引擎选型:多模引擎在数据模型与存储设计上的取舍

工业 IoT 存储引擎选型:多模引擎在数据模型与存储设计上的取舍

原创
作者头像
Xxtaoaooo
修改2026-07-30 22:17:41
修改2026-07-30 22:17:41
680
举报
文章被收录于专栏:应用实践应用实践

一、问题起点:为什么"一张表存所有"会出问题

1.1 四类数据,四种访问模式

一个稍具规模的工业 IoT ,数据大致可以分成四类。它们看起来都"是时序数据",但写入和查询的形态完全不同。

数据类型

典型例子

写入特征

主要查询

高频测点时序

振动波形、温度、压力原始值

只追加,极高吞吐

按设备 + 时间段点查

设备档案 / 最新状态

设备型号、安装位置、当前工况

高频小批量更新

按主键取最新值

告警 / 工单流水

告警确认、工单流转

带状态变更的事务

按主键 + 状态过滤

历史归档 / 报表

年度报表、长周期统计

批量灌入,写后基本不动

全表 / 大跨度聚合

把这四类数据塞进同一个引擎,相当于让一个引擎同时承担"高吞吐追加 + 高频更新 + 事务 + 大扫描"四种压力。任何引擎都有自己的甜点区(sweet spot),超出甜点区的代价就是写放大、查询抖动、去重失效。

1.2 用错引擎的三种代价

代价一:高频点查退化成全表扫描

振动监测里最典型的查询是"看某台设备最近一分钟的波形"。如果底层是纯列存(没有排序键索引),引擎只能定位到时间分区,然后在该分区里逐块扫描。设备少时还能扛,设备上千台、单分区几亿行后,点查延迟会从毫秒涨到秒级。

代价二:状态更新变成"删了再插"

设备当前工况会变(待机 / 运行 / 故障)。如果用只追加的时序表存"当前状态",更新一次状态就要删除旧行、插入新行,或者依赖应用层做去重。一旦并发上来,就会出现"最新状态到底是哪条"的混乱。

代价三:事务性操作丢一致性

告警工单的"确认 → 处理中 → 关闭"是状态机,需要事务保证(不能两个工单同时确认同一条告警,不能关闭一个未确认的告警)。纯时序引擎没有事务,这类逻辑只能搬到应用层,用 Redis 或关系库再扛一层,链路又复杂了。

这三类代价,本质上都是存储结构和访问模式不匹配。DolphinDB 的多模引擎,就是用"匹配更合适的存储结构"的思路来解决这件事。


二、先把五个引擎的内部结构摆清楚

在谈选型之前,先把每个引擎"是什么"讲清楚。DolphinDB 不是把五个独立的数据库拼在一起,而是同一套分布式框架下的五种存储实现,共享分区、事务、SQL 接口,区别只在数据怎么落盘、怎么建索引。

2.1 TSDB:时序主力引擎

TSDB 是 DolphinDB 处理时序数据的默认引擎,也是 IoT 场景用得最多的一个。它的核心特征是 PAX 行列混存 + 排序键(sortKey)索引

  • PAX 行列混存:数据按块(block)组织,块内同一列的数据连续存放(列存的好处:压缩率高、按列读取快),但同一个块的元信息放在一起(行存的好处:点查定位快)。这种折中让 TSDB 既能做高效的列式扫描,又能做快速的单点查询。
  • 排序键 sortKey:建表时通过 sortColumns 指定,引擎按这个顺序在块内对数据排序,并建立 sortKey 索引。点查时引擎先用分区裁剪定位分区,再用 sortKey 索引定位块,最后块内二分查找,整体复杂度接近 O(log n)。
  • 去重策略 keepDuplicatesALL 保留所有行(高保真原始数据)、LAST 每个 sortKey 只保留最后一条(状态更新语义)、FIRST 保留第一条。这是 TSDB 区别于纯追加时序库的关键能力——它可以在写入时做轻量去重。

TSDB 的甜点区:高频追加 + 按设备+时间点查,正是 IoT 测点数据的访问形态。

2.2 OLAP:大跨度聚合的列存引擎

OLAP 是更"传统"的纯列式引擎,没有 sortKey 索引,数据按列连续存放,适合全表扫描和长跨度聚合

它的优势是压缩率极高、扫描吞吐大,劣势是没有高效的点查路径——查"某台设备某一秒"也得扫整个分区的列。所以 OLAP 适合写后基本不动、查询都是大范围聚合的场景,比如年度报表、长周期统计、历史冷数据。

2.3 PKEY:主键唯一 + CDC

PKEY 引擎围绕主键唯一性设计。它的内部维护一个按主键索引的结构,写入时如果主键已存在就覆盖旧值(upsert 语义),保证每个主键始终对应"最新的一条"。

这天然契合设备档案 / 最新状态这类数据:每台设备一条记录,属性更新时直接 upsert,查询永远拿到当前值。PKEY 还支持 CDC(Change Data Capture,变更数据捕获),可以把每次主键值的变更作为流推出来,用于驱动下游的"设备状态变化"实时逻辑。

2.4 IMOLTP:内存事务引擎

IMOLTP 是内存数据库 + 事务引擎,数据驻留内存,通过 redo log + checkpoint 做持久化,支持完整的 ACID 事务。它的甜点区是高并发更新 + 强一致性——典型代表就是告警工单、订单流转这类需要事务保证的状态机。

2.5 VECTORDB:向量检索(本文略)

VECTORDB 面向大规模向量的近似最近邻检索,主要用于 AI 场景下的相似性搜索(比如异常波形检索、设备画像聚类)。它和前四个引擎的访问模式差异较大,且偏向 AI 应用,本文不展开,只在最后一节简单提一句它的位置。


三、主力引擎深挖:TSDB 的 sortColumns 与 keepDuplicates

TSDB 是 IoT 场景的核心引擎,这一节把它拆开讲透。

3.1 建一张"对"的测点表

下面是一个高频振动测点表的建表脚本。重点不在分区,而在 sortColumnskeepDuplicates

代码语言:javascript
复制
// 复合分区:日期 VALUE + 设备 HASH,TSDB 引擎
db1 = database("", VALUE, 2026.01.01..2026.12.31)
db2 = database("", HASH, [SYMBOL, 20])
db = database("dfs://iot_ts", COMPO, [db1, db2], engine="TSDB")

// 振动原始波形表:10kHz 采样,高频追加
schema = table(1:0, `ts`deviceId`xAxis`yAxis`zAxis`temperature,
               [DATETIME, SYMBOL, DOUBLE, DOUBLE, DOUBLE, DOUBLE])

db.createPartitionedTable(
    schema, "vibration", `ts`deviceId,
    sortColumns    = `deviceId`ts,
    keepDuplicates = ALL
)

sortColumns =deviceId`ts`` 是这张表性能的关键。它决定了引擎在块内按"先设备、再时间"排序,sortKey 索引就建在这个顺序上。

3.2 sortColumns 的设计原则

排序键的设计直接决定点查性能,有几条原则:

  • 把高基数、常用于过滤的列放前面。设备 ID(deviceId)的基数高、且几乎所有查询都会带它,放第一列;时间戳 ts 放第二列。这样查询 where deviceId = "PUMP-007", ts between ... 时,sortKey 索引能直接定位到该设备在该时间段的数据块。
  • 不要把低基数列(比如工厂编号只有 3 个值)放第一个。低基数列在前会让 sortKey 区分度不够,索引退化。
  • 排序键列数不宜过多,一般 2–3 列。列数多了,写入时排序开销变大,索引也膨胀。

排序键选错了,TSDB 的点查优势就没了,退化成接近全扫描。

3.3 keepDuplicates:用写时去重替代应用层去重

keepDuplicates 是 TSDB 经常被忽略、但特别契合 IoT 的能力。它有三种取值:

代码语言:javascript
复制
// 场景 A:原始波形,要求高保真,一条都不能丢
keepDuplicates = ALL

// 场景 B:设备最新工况,同一个设备+时间点只保留最后一次写入
//          适合 MQTT 重传、边缘端补传导致的重复数据
keepDuplicates = LAST

// 场景 C:保留首次写入(较少用)
keepDuplicates = FIRST

举一个真实场景:边缘端因为网络抖动,把同一秒的状态数据重传了三次。如果用 ALL,表里就有三条重复记录,下游统计 count(*) 就会虚高;如果用 LAST,引擎在写入时按 sortKey 去重,同一个 deviceId + ts 只保留最后一条,下游拿到的就是干净数据。

这个能力把"去重"从应用层下沉到了存储引擎,省掉了写前去重表、写后清洗的一整套逻辑。

3.4 一张图理解 TSDB 的查询路径

把一次点查的路径串起来:

代码语言:javascript
复制
查询: select * from vibration
      where deviceId = "PUMP-007"
        and ts between 2026.03.15 10:00:00 and 2026.03.15 10:01:00

1. 分区裁剪 → 定位到 [2026.03.15 分区, PUMP-007 所在的 hash 桶]
2. sortKey 索引 → 在该分区内定位到 deviceId=PUMP-007 的数据块
3. 块内二分查找 → 按时间定位到 10:00:00~10:01:00 的行
4. PAX 列存读取 → 只把 xAxis/yAxis/zAxis/temperature 这几列读出来

四步下来,扫描的数据量从"整个分区几亿行"缩减到"单设备一分钟几千行"。这就是 TSDB 在 IoT 点查场景下能稳定保持毫秒级的根本原因——不是靠 brute force 的算力,是靠存储结构把"要扫描的数据"提前裁到很小。


四、按数据模型分库:多引擎落地方案

讲清楚单个引擎后,来看完整的落地方案。一个工业 IoT 按四类数据分别建库、各用各的引擎,再通过跨库关联把数据串起来。

4.1 高频测点时序 → TSDB

振动、温度、压力这类只追加、按设备+时间点查的数据,放 TSDB,设计同第三节。这是数据量最大、写入最频繁的一层。

代码语言:javascript
复制
// 测点库:TSDB,复合分区,sortColumns=[设备, 时间],keepDuplicates=ALL
dbPoint = database("dfs://iot_point", COMPO, [
    database("", VALUE, 2026.01.01..2026.12.31),
    database("", HASH, [SYMBOL, 20])
], engine="TSDB")

4.2 设备档案与最新状态 → PKEY

设备型号、安装位置、固件版本、当前工况这类数据,需要按主键更新、查最新值。放 PKEY,主键就是设备 ID。

代码语言:javascript
复制
// 设备档案库:PKEY 引擎,按设备 ID 分区,主键唯一
dbMeta = database("dfs://iot_meta", RANGE, 1..101, engine="PKEY")

profile = table(1:0,
    `deviceId`model`site`installDate`firmware`latestStatus`lastHeartbeat,
    [INT, SYMBOL, SYMBOL, DATE, SYMBOL, SYMBOL, DATETIME])

// primaryKey 声明主键,写入时自动 upsert
dbMeta.createPartitionedTable(profile, "deviceProfile", , primaryKey=`deviceId)

设备上线时 insert 一条档案;状态变化时,应用层只管 upsert 新值,引擎保证每个 deviceId 始终是最新的一条。再也不用担心"重复插入产生多条档案"或"更新时漏删旧记录"。

代码语言:javascript
复制
// 更新设备状态:PKEY 的 upsert 语义,主键相同即覆盖
t = table(101 as deviceId, `RUNNING as latestStatus, now() as lastHeartbeat)
loadTable("dfs://iot_meta", "deviceProfile").upsert!(t)

// 查询永远拿到当前值,无需关心历史快照
select * from loadTable("dfs://iot_meta", "deviceProfile") where deviceId = 101

如果需要把"状态变化"作为事件驱动下游(比如设备从 RUNNING 变成 FAULT 时触发告警),PKEY 的 CDC 可以把每次变更捕获成流,订阅消费即可。

4.3 告警工单与事务流水 → IMOLTP

告警确认、工单流转是状态机,需要事务保证:不能两个操作员同时确认同一条告警,不能关闭一个尚未确认的工单。这一层放 IMOLTP。

代码语言:javascript
复制
// 工单库:IMOLTP 引擎,内存事务
dbTxn = database("dfs://iot_txn", VALUE, 2026.01.01..2026.12.31, engine="IMOLTP")

ticket = table(1:0,
    `ticketId`alertId`deviceId`status`assignee`createdAt`updatedAt,
    [LONG, LONG, INT, SYMBOL, SYMBOL, DATETIME, DATETIME])

dbTxn.createTable(ticket, "alertTicket")

工单流转用事务包裹,保证状态机的一致性:

代码语言:javascript
复制
// 事务:确认告警 + 创建工单,要么全成功要么全回滚
def confirmAndAssign(alertId, assignee) {
    txn {
        // 1. 告警状态改为已确认
        update loadTable("dfs://iot_txn", "alertTicket")
        set status = `CONFIRMED, updatedAt = now()
        where alertId = :alertId and status = `OPEN

        // 2. 分配处理人,工单进入处理中
        update loadTable("dfs://iot_txn", "alertTicket")
        set status = `PROCESSING, assignee = :assignee, updatedAt = now()
        where alertId = :alertId and status = `CONFIRMED
    }
}

事务保证这两个 update 原子完成。如果中间任意一步失败(比如告警已经被别人确认),整个事务回滚,不会出现"告警确认了但工单没流转"的中间态。

4.4 长周期历史归档 → OLAP

年度报表、跨季度统计这类写后不动、查询都是大跨度聚合的数据,放 OLAP。它的列存压缩率最高,扫描吞吐最大,做全表 group by 比 TSDB 更划算。

代码语言:javascript
复制
// 归档库:OLAP 引擎,按年分区
dbArchive = database("dfs://iot_archive", VALUE, 2026.01M..2026.12M, engine="OLAP")

// 设备日聚合表:每天每台设备一条,用于月报/年报
daily = table(1:0, `day`deviceId`runHours`avgTemp`maxVib`alertCount,
              [DATE, INT, DOUBLE, DOUBLE, DOUBLE, INT])

dbArchive.createPartitionedTable(daily, "deviceDaily", `day)

4.5 跨引擎关联:把档案和测点串起来

分库不等于数据孤岛。最常见的需求是"查某台设备某个时间段的振动数据,同时带上它的型号和当前状态"。这就需要把 PKEY 的档案和 TSDB 的测点关联起来。

代码语言:javascript
复制
// PKEY 档案 join TSDB 测点:用设备的元信息过滤时序数据
select v.ts, v.deviceId, p.model, p.site, p.latestStatus,
       v.xAxis, v.yAxis, v.zAxis
from loadTable("dfs://iot_point", "vibration") as v
inner join loadTable("dfs://iot_meta", "deviceProfile") as p
       on v.deviceId = p.deviceId
where v.deviceId = 101
  and v.ts between 2026.03.15 10:00:00 : 2026.03.15 10:05:00
  and p.model = `PUMP-A

因为两个库都在 DolphinDB 同一个集群内,关联是库内计算,不涉及数据导出和网络搬运。这正是多模引擎的价值——分库是为了让每种数据用最优结构存储,关联时又能像在同一个库里一样无缝 join


五、设计取舍清单:什么数据选什么引擎

落到决策上,给一张工程上够用的选型表。

数据特征

推荐引擎

关键设计

只追加,按设备+时间点查(测点原始值)

TSDB

sortColumns=[设备, 时间], keepDuplicates=ALL

只追加但需写时去重(状态补传)

TSDB

keepDuplicates=LAST

高频更新,查最新值(设备档案/状态)

PKEY

primaryKey=设备ID

需捕获变更驱动下游(状态变化事件)

PKEY + CDC

订阅 CDC 流

事务性状态机(工单/告警流转)

IMOLTP

txn { ... } 包裹

写后不动,大跨度聚合(报表归档)

OLAP

按时间粗粒度分区

几条容易踩的误区:

  • 别用 TSDB 存设备档案。档案需要按主键更新,TSDB 的更新是"标记删除 + 追加",高频更新会产生大量历史版本,keepDuplicates=LAST 只能在 sortKey 粒度去重,不是主键级 upsert。该用 PKEY。
  • 别用 OLAP 做实时点查。OLAP 没有 sortKey 索引,点查会退化成列扫描。实时看板点查必须走 TSDB。
  • 别把告警工单塞进 TSDB。TSDB 没有事务,状态机的一致性保证不了。该用 IMOLTP,或者至少用 PKEY + 应用层加锁。
  • 别对归档数据用细粒度分区。OLAP 归档数据查询都是大跨度,分区太细(比如按天)会让一次年报查询跨几百个分区,元数据开销反而拖慢。按月或按季更合适。

六、写在最后

工业 IoT 的数据,从来不是单一的"时序数据",而是多种访问模式混合的数据集合。用一种存储结构通吃所有,看似简单,实则是把每种访问模式都拖离了甜点区。DolphinDB 的多模引擎,本质上是承认了这种多样性——匹配更合适的存储结构,再用统一的分布式框架和 SQL 接口把它们组织起来

落到工程上,记住一句话就够了:

先把数据按访问模式分类,再为每一类选引擎;分库不是为了隔离,是为了让每种数据都在它最擅长的结构里跑。

测点走 TSDB、档案走 PKEY、工单走 IMOLTP、归档走 OLAP,需要关联时库内 join 把它们串起来。这套分层不是过度设计,而是让每层数据都能在数据量增长后依然保持稳定的性能——因为结构本身就在帮你裁剪扫描量、保证去重、维持一致性。

存储引擎选对了,后面所有的计算、治理、分析,才有了靠谱的地基。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、问题起点:为什么"一张表存所有"会出问题
    • 1.1 四类数据,四种访问模式
    • 1.2 用错引擎的三种代价
  • 二、先把五个引擎的内部结构摆清楚
    • 2.1 TSDB:时序主力引擎
    • 2.2 OLAP:大跨度聚合的列存引擎
    • 2.3 PKEY:主键唯一 + CDC
    • 2.4 IMOLTP:内存事务引擎
    • 2.5 VECTORDB:向量检索(本文略)
  • 三、主力引擎深挖:TSDB 的 sortColumns 与 keepDuplicates
    • 3.1 建一张"对"的测点表
    • 3.2 sortColumns 的设计原则
    • 3.3 keepDuplicates:用写时去重替代应用层去重
    • 3.4 一张图理解 TSDB 的查询路径
  • 四、按数据模型分库:多引擎落地方案
    • 4.1 高频测点时序 → TSDB
    • 4.2 设备档案与最新状态 → PKEY
    • 4.3 告警工单与事务流水 → IMOLTP
    • 4.4 长周期历史归档 → OLAP
    • 4.5 跨引擎关联:把档案和测点串起来
  • 五、设计取舍清单:什么数据选什么引擎
  • 六、写在最后
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档