Skip to content

5.4 数据存储与高效查询

5.4.1 时序数据库:数据模型与写入架构

物联网数据最明显的特征是“有序”——每条记录都与一个精确的时间戳紧密绑定。以温度传感器为例,数据以固定或变化的间隔上报;GPS坐标周期性回传,振动波形以毫秒级间隔连续写入。这类数据之所以让传统关系型数据库吃力,不是因为数据结构复杂,而是因为写入负载高、累加量大。如果数据库每秒要处理大量单行INSERT,且绝大多数操作都是写入,关系数据库的B+树索引很快会成为瓶颈。

数据模型:时间戳、标签与字段

时序数据库的数据模型围绕三个核心概念设计:时间戳(timestamp)、标签(tags)和字段(fields)。

时间戳是数据的标记点,通常采用Unix毫秒级或纳秒级精度。在物联网场景中,设备上报的原始时间常为UTC,由边缘网关统一打上接收时间戳,避免设备本地时钟不同步导致的时序错乱。时间戳决定了数据在哪个时间分区落地,也驱动了基于时间的聚合和查询。

标签用键值对描述数据的元信息——设备ID、传感器类型、厂房编号、地理区域。标签是有索引的,支持高效的过滤和分组查询。例如,要查“厂房A中所有温度传感器在过去24小时的均值”,时序数据库会利用标签的倒排索引快速定位到相关序列。标签数量需要控制,通常建议10个以内,因为每个标签都会增加索引内存消耗和写入开销。

字段是真正承载测量值的部分——温度读数、湿度百分比、振动加速度、电流大小。字段的值通常是浮点数或整数,字段数量从几个到上百个不等。字段不建索引,查询时按列扫描或通过时间索引缩小范围。

表5-2 关系数据库与时序数据库数据模型对比

维度关系数据库时序数据库
代表实现MySQL、PostgreSQLInfluxDB、TimescaleDB
主键设计业务主键(ID、UUID)时间戳+标签组合(自动分区)
数据写入方式单条INSERT或批量INSERT行协议或二进制批次
数据更新频率频繁主要追加写入,极少原地更新
删除策略DELETE语句按需删除基于保留策略自动过期删除
索引机制B+树正排索引(时序)+ 倒排索引(标签)
存储侧重数据一致性、事务写入吞吐量、压缩比、降采样效率

表中显示,时序数据库从设计之初就放弃了通用性,换来了极高的写入性能和存储效率。工程师在选择数据库时,如果业务主要是设备数据上报和趋势分析,应优先考虑时序数据库。

选型视野也不必局限于上面两家。TDengine以“一个采集点一张表”的数据模型和超级表语法见长,写入去重与压缩策略激进,在国产工业、电力和能源监控语境中装机量很大;Apache IoTDB是Apache基金会孵化的物联网原生时序数据库,树形元数据贴合设备层级组织,端-边-云数据同步对车联网和工业现场比较友好;GreptimeDB则代表云原生路线,存算分离、以对象存储为底座,适合部署在Kubernetes与公有云托管环境。它们与InfluxDB、TimescaleDB的取舍逻辑一致:写入模型、查询语言和运维形态决定适配场景,没有全能选手。

写入架构:从LSM-Tree到TSM引擎

时序数据库的写入性能核心在于存储引擎。大多数现代TSDB采用Log-Structured Merge-Tree(LSM-Tree,日志结构合并树)的变体。LSM-Tree也是Apache Cassandra、HBase这类NoSQL数据库的基础,但时序场景专门做了两个改动:一是按时间分区,二是针对浮点数做列式压缩。

LSM-Tree的写入路径大致分三段。

第一段,数据先写入内存中的写缓存(memtable)。memtable按时间戳和标签排序,形成有序结构。传统B+树在每次写入时都要查找并修改索引页,高并发下产生大量随机写;而memtable只需要在内存中做一次插入,排序成本可控。当memtable大小达到阈值(通常为几兆到几十兆字节)时,会被冻结为不可变的只读结构。

第二段,冻结的memtable作为SSTable(Sorted String Table,有序字符串表)刷入磁盘。SSTable是顺序写的,磁盘I/O几乎是纯追加的,绕开了传统B+树随机写索引页的瓶颈。

第三段,后台的合并线程(compaction)定期将多个小SSTable合并为大SSTable,清理重复数据、删除过期数据,同时压缩数据块。合并操作是时序数据库写入稳定的关键:通过后台资源消耗换取了查询时不必打开大量小文件。

InfluxDB在1.x/2.x中对LSM-Tree做了进一步优化,形成了TSM(Time-Structured Merge Tree)引擎(3.x已转向Parquet存储,见5.1.2节)。TSM引擎的关键改进包括:按时间分区(shard)存放数据,每个shard内部再按列式布局存储字段值,从而获得更好的压缩比。相比通用LSM-Tree,TSM引擎的合并策略更激进,主动将时间相邻的块合并,压缩效率更高。

压缩算法:差分编码与delta-of-delta

时间序列数据有一个显著特征:相邻读数之间的差值通常很小,甚至为零。时序数据库利用这种“缓慢变化”特性,专门设计压缩算法。

时间戳压缩通常采用delta-of-delta(DDD)编码。一个设备每秒上报一次数据,时间戳序列为t₀, t₀+1000ms, t₀+2000ms……DDD先计算相邻时间戳的差值(delta):1000, 1000, 1000……然后计算这些差值的差值(delta of delta):0, 0, 0……如果设备准时上报,DDD值几乎全是0,可以用很少的比特来表示,压缩比非常高。这种算法在实际工程中能把时间戳占用从64位降到1到2位。

浮点数压缩则使用差分编码与XOR结合的框架。该方法只存储浮点数前值与当前值的异或结果:如果相邻读数接近,异或结果的高位全是0,同样可以大幅节省空间。一个时间戳+浮点数的16字节元组,在稳定场景下可以压缩到不足4字节。压缩比受数据波动程度影响:如果传感器数据剧烈变化,压缩比会下降,但总比不压缩好很多。

写入吞吐优化:批量写入与并发设计

物联网场景下,单个设备的写入速率可能很低(每分钟一次),但设备数量却可能达到十万甚至百万级。这意味着数据库每秒要处理数十万次写入。工程中主要靠两条线保证写入吞吐:批量处理与并行管道。

批量写入是所有时序数据库的标配。以InfluxDB的行协议(Line Protocol)为例,客户端将多条数据拼在一个HTTP POST请求体中发送,而非逐条写入。行协议格式如下:

text
# 示例:向InfluxDB写入两条天气数据
# 格式:<measurement>,<tags> <fields> <timestamp>
weather,location=us-midwest,sensor_id=1234 temperature=82,humidity=75 1700000000000000000
weather,location=us-west,sensor_id=5678 temperature=78,humidity=68 1700000060000000000

这个协议用换行分隔系列。标签在前(逗号分隔键值对),字段在后(逗号分隔键值对),最后是纳秒精度的Unix时间戳。服务端按批接收后,再拆解写入memtable。批量大小一般设在几百到几千条之间,过大可能导致单次请求超时,过小则无法充分利用批量优势。

并行管道解决单点瓶颈。大多数时序数据库支持多线程写入,每个shard或分区对应一个独立的写入管道。写入请求先根据标签哈希到特定分区,各分区内的写入互不影响。这种水平扩展模式让时序数据库能随硬件核数线性扩展写入吞吐。实际部署时,shard数量需要根据设备数和数据量动态调整:shard太少会导致写入竞争,太多则增加管理开销。

此外,预写日志(WAL,Write-Ahead Log)是保证数据不丢的第一道防线。所有写入先追加到WAL(顺序写),成功后返回给客户端,然后异步写入memtable和SSTable。即使服务器宕机,重启后也能从WAL恢复数据。WAL写入速度直接影响写入延迟,因此很多时序数据库会将WAL单独放在SSD上,并开启批量flush。

理解了时序数据库的核心数据模型与写入机制,下面讨论如何高效地把数据读出来——包含降采样聚合、持续查询与数据生命周期管理。这些是工程实践中每天查数据、看仪表盘时都会遇到的问题。

5.4.2 高效查询:降采样、聚合与持续查询

时序数据库解决了写入问题后,下一个瓶颈通常出现在查询侧。一个典型现象是:仪表盘加载“过去24小时温度走势”需要十几秒。原因很简单——查询扫描了上千万条原始记录,而业务真正需要的是小时级平均温度。解决思路不是让数据库跑得更快,而是让查询处理的数据量更少。降采样、预聚合和持续查询正是为此设计的三件套。

降采样:精度换时间

降采样(downsampling)将高精度原始数据按固定时间窗口聚合为粗粒度汇总数据。温度传感器每10秒上报一次,查询“过去1小时平均温度”时直接扫描原始记录不仅慢而且没必要。更好的做法是:在写入或后台自动计算每分钟的平均值、最大值、最小值,将多条记录压缩为一条聚合记录,查询读取后者即可。

降采样对存储的影响可以直接估算。以例子为例:一个中等规模的工厂部署了若干设备,每台每10秒上报温度和湿度两个字段。如果按分钟级聚合,数据量可以降至原始记录的约几分之一;若按小时级聚合,数据量可降至更低的占比。降采样不是删除数据,而是建立数据分层:高精度原始数据保留短时间用于故障排查,粗粒度聚合数据保留更长时间用于趋势分析。

图 5-8 降采样数据流程与数据量对比(例子)四级数据桶由连续查询逐级聚合,数据量逐级降低并采用分层保留策略。图 5-8 降采样数据流程与数据量对比(例子)连续查询逐级聚合;粒度变粗,数据量与长期存储成本同步下降。CQ:每分钟CQ:每小时CQ:每天原始数据桶10 秒级精度 · 短保留窗口数据量:原始基准分钟聚合桶每分钟均值 · 短时趋势数据量:显著减少小时聚合桶每小时均值 · 日报/周报数据量:大幅降低天聚合桶每天均值 · 年度趋势数据量:极小占比查询仪表盘应用层直接读取聚合数据分层保留策略原始层短期保留 · 故障回放分钟层中期保留 · 短时趋势小时层季度趋势 · 日报/周报天层长期保留 · 年度趋势实线箭头:连续查询驱动自动化聚合虚线箭头:应用层查询路径图 5-8 降采样数据流程与数据量对比:三级降采样将数据量逐级压缩,原始层短保留用于故障回放,分钟/小时/天层分别支撑短时趋势、日报与年度趋势。
图 5-8 降采样数据流程与数据量对比(例子)

持续查询:让聚合自动化

持续查询(Continuous Query, CQ)是时序数据库内置的、以固定时间间隔自动执行聚合操作的机制。用户定义一条类SQL查询,数据库后台按计划周期运行,将结果写入指定表。整个过程无需外部调度器,对应用透明。

以InfluxDB 1.x/2.x为例,创建一个连续查询,每小时自动计算所有传感器的平均温度:

influxql
CREATE CONTINUOUS QUERY "cq_1h_avg" ON "iot_platform"
BEGIN
  SELECT mean("temperature") AS avg_temp
  INTO "hourly_avg"
  FROM "sensor_data"
  GROUP BY time(1h), "device_id"
END

这条语句执行后,InfluxDB每小时整点自动查询过去一小时sensor_data表中的数据,按device_id分组计算平均温度,将结果追加到hourly_avg测量中。仪表盘读取hourly_avg时扫描的是少量聚合记录,而不是大量原始记录。持续查询与降采样天然互补:CQ是实现自动化降采样的标准工具,保留策略(Retention Policy)负责让原始数据在指定时间后过期,形成完整的数据生命周期。需要注明版本口径:上述InfluxQL持续查询语法适用于InfluxDB 1.x/2.x;InfluxDB 3.x为Rust重写版本,不再内置这类CQ,降采样需改由其处理引擎插件或外部任务调度完成。

实时聚合与窗口函数

持续查询的局限在于它的周期性——每小时才刷新一次。对于“最近5分钟平均温度”这类场景,等待CQ刷新不适用。时序数据库提供时间窗口函数,动态地对查询范围内的数据实时计算聚合。在InfluxQL中,GROUP BY time(5m)将数据分为5分钟一个桶,实时计算桶内均值。TimescaleDB中使用time_bucket('5 minutes', time)实现类似功能。以下查询实时计算过去1小时每5分钟的平均温度:

influxql
SELECT mean("temperature") AS avg_temp
FROM "sensor_data"
WHERE time > now() - 1h
GROUP BY time(5m), "device_id"

实时聚合不需要额外存储,每次查询都在原始数据上执行。但如果仪表盘面板每秒刷新,每次都跑这个查询,很快打满查询线程。工程上的做法是:通过缓存或物化视图对高频查询做裁剪——用户直接请求且频繁访问的仪表盘数据,由CQ或物化视图提供;临时的探索性分析,直接用实时窗口函数查。

工程权衡:CQ vs. 实时聚合

特性持续查询(CQ)实时窗口聚合
数据来源预计算并存储每次查询实时计算
查询响应速度毫秒级(直接读聚合表)取决于数据量和时间窗口
额外存储开销有(存储聚合结果)
适合场景高频访问的仪表盘、报警规则、固定报表临时分析、低频探索、调试

如果聚合结果每天被翻看数千次,值得用CQ提前算好;如果分析只在排查问题时使用几次,实时窗口函数更省维护成本。

分层设计实践

实际系统中,降采样很少只做一级。以下是一套分层方案,各层保留时长和数据量比为定性描述,实际项目需根据业务需求和设备规模调整:

  • 原始层:保留较短窗口(如用于故障现场回放),高精度原始数据。
  • 分钟级聚合层:保留中期窗口(如数周至数月),提供小时内的波动概览。
  • 小时级聚合层:保留较长期窗口(如数月),支撑日报和周报。
  • 天级聚合层:保留超长期窗口(如一年或更长),用于年度趋势、容量规划等场景。

每一层的数据量相比上一层显著减少。例如,若原始数据为秒级,分钟聚合可降至约几分之一,小时聚合可降至约几百分之一,天级聚合可降至约几千分之一(基于典型场景估算,非精确值)。三层结构下,一年数据中原始数据只占最早的小部分,其余都是聚合后的粗粒度信息。数据链路中的“消息队列→时序数据库→聚合”是这一设计的关键:网关上传的原始数据先经消息队列缓冲,再写入时序数据库的原始层;持续查询在数据库内部将原始层数据聚合并写入聚合层;仪表盘直接读取聚合层。这套管线与第5.1节讨论的“消息队列解耦写入压力”逻辑一致——队列用于解耦写入压力,CQ用于解耦查询压力。

实践检查清单

  • 根据业务确定各层保留窗口:原始层通常较短(如用于故障诊断),聚合层按报告周期确定(日报需小时级,年报需天级)。
  • 评估CQ执行频率:CQ对写入有额外开销,在高写入负载下应避免设置过短的执行间隔(建议根据写入负载评估,例如不低于1分钟)。
  • 验证聚合查询的精度:聚合函数(mean, max, min)需与业务含义一致,注意离群值对统计结果的影响。
  • 监控CQ延迟:如果CQ执行时间超过执行间隔,会造成数据堆积,应考虑增加计算资源或调整聚合粒度。

最后交代一句分工:本节给出的降采样、持续查询与分层保留是通用管道能力;时序数据库在工业现场的选型差异——协议适配、数据模型与行业惯例——留到第 10 章 10.3 节展开。

5.4.3 数据生命周期管理:过期删除与冷热分层

高写入吞吐解决了时序数据“存得进”的问题,但新瓶颈很快会浮出水面:磁盘容量告急。查看查询日志会发现,几个月前的数据几乎从未被访问过,却和最新数据一样占据着昂贵的存储资源。

一个工程实情是:不同时间跨度的数据,查询频率差异巨大。实时仪表盘需要毫秒级访问最近几小时的数据;月度报表只需分钟级聚合结果;而一年前的原始读数,可能只在年终回顾时才被调用一两次。把不同价值的数据放在同一层级的存储上,成本上不划算。

保留策略(Retention Policy)是最直接的成本控制手段。几乎所有时序数据库都允许为不同数据集设定独立的保留时长。一个车间部署了温度、振动和电流传感器,原始10秒级数据主要服务于实时告警和故障排查,保留7天就够了;分钟级聚合数据用于周报,保留30天;小时级聚合用于年度趋势分析,保留12个月。保留策略生效后,数据库容量会趋于稳定:新数据持续写入,到期数据被自动删除,磁盘占用不再随运行时间增长。

当业务需要保留超过三年数据时,仅靠保留策略就不够用了。删除旧数据能节省空间,但一旦删除就无法回溯。冷热分层(Cold/Hot Tiering)为更长周期的数据留存提供了另一种路径——将数据按访问频率放在不同性价比的存储介质上。

一个典型的分层方案大致是:热存储放最近7天的数据,使用本地NVMe或SSD,响应仪表盘毫秒级查询;温存储放8天到3个月的数据,迁移到普通HDD或SSD,用于月度报表;冷存储放超过3个月的数据,归档到对象存储(如MinIO、公有云S3兼容服务),用于季度回顾或算法模型训练。分层存储的核心收益在于:绝大部分查询集中在热存储上,而体积最大的冷数据存储成本可以压得很低。

表5-3 热存储与冷存储对比

维度热存储冷存储
存储介质本地NVMe / SSD对象存储(S3兼容)或HDD
查询速度毫秒级秒到分钟级
单位成本相对较高相对较低
数据格式时序数据库原生格式Parquet / Avro
典型保留窗口最近7–30天三个月至数年
访问模式实时大盘、告警触发历史分析、批量模型训练
访问频度频繁极少

冷数据的存储格式也很关键。原始时序数据导出后,通常会转换为 ParquetAvro 这类列式存储格式。按时间分区存放,目录结构类似 bucket/device_id/year/month/day/data.parquet。需要回溯某台设备某一天的数据时,查询引擎只需加载对应分区文件,不用全量扫描。

实施冷热分层时有一个常见陷阱:数据迁移本身会占用I/O和CPU。如果每天凌晨把前一天的数据从热存储迁到冷存储,在设备规模上万甚至更大时,一次性迁移很可能拖慢数据库响应。一种改进方法是分片迁移:把数据按设备号或时间段拆成小块,分批在低峰时段执行,并设置迁移速率限制。部分时序数据库产品已经支持自动冷热分层功能,用户只需配置保留窗口和存储位置,系统自行完成迁移。对于新立项的系统,优先选择这类内置分层能力的版本,能节省不少后期运维精力。

数据生命周期管理的核心命题很简单:让每一字节数据按它的查询价值来付费。热数据保持快速读取,冷数据安静归档。当存储成本不再成为瓶颈,工程师才有精力把注意力放到数据本身的分析上。

图 5-9 数据生命周期管理:保留策略与冷热分层保留策略按价值设定过期时长,冷热分层把数据按访问频率放到不同性价比的存储介质。图 5-9 数据生命周期管理:保留策略与冷热分层让每一字节数据按它的查询价值来付费保留策略:不同粒度设定独立保留时长原始 10 秒级数据服务实时告警与故障排查保留 7 天分钟级聚合数据用于周报保留 30 天小时级聚合数据用于年度趋势分析保留 12 个月冷热分层:按访问频率放在不同性价比的存储介质热存储最近 7 天 · 本地 NVMe / SSD响应仪表盘毫秒级查询,访问频繁时序数据库原生格式,单位成本相对较高实时大盘、告警触发温存储8 天~3 个月 · 普通 HDD / SSD用于月度报表,访问频度中等月度报表冷存储超过 3 个月 · 对象存储(MinIO / S3 兼容)Parquet / Avro 列式格式,按时间分区,查询只加载对应分区用于季度回顾或算法模型训练,访问极少历史分析、批量训练图 5-9 保留策略按数据粒度设定过期时长,冷热分层把数据按访问频率放到热/温/冷三级存储;分片迁移在低峰分批执行并限速,避免迁移拖慢数据库响应。
图 5-9 数据生命周期管理:保留策略与冷热分层

从工业软件到 AI 智能体 · 构建面向智能体演进的多协议、云原生、开源工业物联网平台