Skip to content

5.4 数据存储与高效查询 ​

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

物联网数据最明显的特征是“以时间为主轴”,而不是天然有序。每条记录通常携带设备源时间,平台还应记录接收时间;断网补传、重试和时钟漂移都可能让到达顺序偏离事件顺序。温度值可以低频上报,GPS 坐标周期回传,振动波形则可能高频成批写入。传统关系数据库并非不能存储这些数据,真正的压力来自持续写入、累积体量、高基数维度、保留策略和时间窗口查询的组合;是否成为瓶颈必须由目标模式和基准测试判断。

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

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

时间戳是数据的时间坐标,常见表达包括 Unix 毫秒、微秒或纳秒。工程上应区分设备源时间与平台接收时间,并记录时钟质量或同步状态。网关补充接收时间只能提供第二个参照,不能修复设备时钟,也不能阻止乱序。数据库可能按其中一个时间列分区,聚合与迟到数据策略则要明确使用哪个时间语义。

标签用键值对描述序列维度——设备 ID、传感器类型、厂房编号、地理区域。许多时序数据库会为标签或序列键建立专门索引,以支持过滤和分组,但具体结构可能是倒排索引、B-tree、哈希、字典或数据跳过索引。风险不只在标签个数,更在取值基数和组合基数;“不超过 10 个”不是通用规则。应以目标产品的基数估算、内存模型和查询计划确定哪些维度可索引。

字段承载测量值——温度、湿度、振动加速度和电流等。字段可以是数值、布尔值或字符串;是否建立索引、采用列式布局或只依靠时间范围裁剪,取决于数据库实现与表设计,不能一概写成“字段不建索引”。高频过滤条件若放错位置,可能造成高基数索引或大范围扫描,因此要用真实查询验证。

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

维度关系数据库时序数据库
代表实现MySQL、PostgreSQLInfluxDB、TimescaleDB
键与分区业务主键、唯一约束与显式索引常以时间列配合序列键/标签组织;唯一键和分区键随产品而异
数据写入方式单条INSERT或批量INSERT行协议或二进制批次
数据更新频率频繁主要追加写入,极少原地更新
删除策略DELETE语句按需删除基于保留策略自动过期删除
索引机制B+tree、Hash、GIN/GiST 等,随数据库而异时间索引、标签/序列索引、分区裁剪或数据跳过索引,随产品而异
存储侧重数据一致性、事务写入吞吐量、压缩比、降采样效率

表中比较的是常见侧重点,不是互斥边界。TimescaleDB 本身建立在 PostgreSQL 之上,关系数据库也可以通过分区、批写和扩展处理时序负载;专用时序数据库则常把保留、压缩、降采样和时间窗口查询做成一等能力。选型应同时验证写入、查询、事务、运维、备份恢复、生态和总成本,而不是仅凭“设备上报”四个字预设答案。

选型视野也不必局限于上面两家。TDengine 的超级表、Apache IoTDB 的层次化路径、GreptimeDB 的分布式架构分别代表不同的数据组织与部署思路,但具体引擎、兼容接口和集群能力会随版本变化。评估时应锁定版本,以官方文档和可复现基准核对写入模型、查询语言、一致性、扩缩容、备份恢复与运维形态;产品标签和市场描述不能代替适配结论。

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

时序数据库的写入性能取决于存储引擎、分区、索引、压缩、持久化和查询负载。不同产品并不共享一条统一路径:有的采用 LSM/SSTable 类结构,有的建立在 PostgreSQL 页式存储之上,有的使用列式文件和对象存储。下面的三段路径只解释 LSM 派生引擎的典型机制,不能套用到所有 TSDB。

LSM 派生引擎的写入路径大致分三段。

第一段,数据进入内存写缓存(memtable),通常按内部键组织。达到实现设定的阈值后,活跃 memtable 会被冻结并等待刷盘。阈值、排序键和并发模型都是产品实现细节,不存在适用于所有数据库的固定范围。

第二段,冻结的 memtable 被写成不可变的有序文件,常称 SSTable。该路径倾向于把许多小更新转换成较大的顺序写,但 WAL、元数据、索引和文件系统仍会产生其他 I/O,不能描述成“几乎纯追加”。

第三段,后台 compaction 按策略合并文件、回收被覆盖或过期的数据并重写压缩块。它减少读放大,却带来写放大和资源竞争;若后台合并跟不上前台写入,尾延迟和磁盘水位都可能恶化,因此需要监控而不是假定其天然稳定。

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₀+1000 ms、t₀+2000 ms……相邻差值相同,第二次差分便连续为零,可用短码表示。这里的“少量比特”是长序列上的编码效果,不等于每个时间戳固定只占 1—2 bit;乱序、抖动和块头元数据都会改变平均压缩率。

浮点序列也可采用 XOR 类编码:保存前值与当前 IEEE 754 位模式的异或结果,并复用前导零、尾随零等结构。数值接近并不必然产生相同压缩效果,异常值、噪声、采样规律和块大小都会影响结果。不要用“16 字节必然压到 4 字节”作为容量规划;应以目标数据集、目标引擎和压缩设置实测每点字节数。

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

物联网场景下,单个设备的上报频率可能很低,但设备数与每报点数会共同放大写入。若十万台设备每分钟各写 1 个点,均值约为 100000 ÷ 60 ≈ 1667 点/秒;一百万台约为 16667 点/秒,而不是自然得到“每秒数十万”。真实入口还要乘以每报点数、突发系数与重试,并计入降采样和副本写放大。批量与并行是常用手段,但容量结论必须来自完整负载模型。

批量写入是所有时序数据库的标配。以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 时间戳;示例数值采用纳秒精度。服务端如何解析并落入内存结构取决于版本。批量大小没有通用的“几百到几千条”答案,应以请求字节数、延迟目标、服务端限制、重试成本和压测结果共同确定。

并行管道可以提高吞吐,但不同数据库按时间、序列键、租户或哈希分片,写入管道也未必与 shard 一一对应。吞吐受热点、锁、WAL、网络、副本、压缩和下游 I/O 限制,不会保证随 CPU 核数线性增长。分片过少可能形成热点,过多则增加元数据、文件和调度开销;应从真实键分布与基准结果确定。

对于采用预写日志(WAL)的引擎,WAL 是崩溃恢复的一环:数据先按实现约定写入日志和内存结构,确认时点由同步刷盘与复制策略决定,随后再写入持久文件。WAL 不能单独保证不丢数据;磁盘损坏、未同步页、错误的确认策略和副本同时故障仍需考虑。是否把 WAL 放在独立 SSD、如何批量刷盘,也应依据产品文档、故障模型和基准测试决定。

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

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)是直接的成本控制手段。不同数据集可以设定不同期限,但“原始 7 天、分钟级 30 天、小时级 12 个月”只能作为示例,实际要由告警追溯、模型训练、法规、合同和事故调查要求确定。若写入率、序列基数和副本数稳定,且过期清理能持续跟上,数据区容量才可能在某个区间趋稳;索引、WAL、压缩临时空间、备份和增长中的设备规模仍会占用额外容量。

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

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

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

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

冷数据的存储格式也很关键。Parquet 是常见的列式分析格式;Avro 通常按行组织,更适合流式交换或长期模式演进,二者不能统称为列式格式。对象存储可按日期、租户或其他稳定维度分区,例如 bucket/year/month/day/tenant/data.parquet;若把高基数 device_id 放进目录层级,要先评估小文件与元数据开销。查询能否避免全量扫描取决于分区裁剪、文件统计信息和目录设计。

实施冷热分层时,迁移会占用 I/O、CPU、网络和临时空间,集中执行可能推高在线查询尾延迟。可按时间块或稳定分区渐进迁移,设置速率限制,并对校验、失败重试、重复对象和删除源数据的时点做演练。部分产品或版本提供内置分层,另一些依赖导出任务或查询引擎;“内置”并不天然更优,仍要验证一致性、可观测性、供应商锁定、恢复时间和总体成本。

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

图 5-9 数据生命周期管理:保留策略与冷热分层保留期限由业务、合规与恢复目标决定;冷热分层按访问模式设计,并通过迁移和查询测试验证。图 5-9 数据生命周期管理:保留策略与冷热分层期限是示例,不是默认值;分层效果必须验证保留策略示例:按用途、法规与调查窗口确定期限原始 10 秒级数据服务实时告警与故障排查示例:保留 7 天分钟级聚合数据用于周报示例:保留 30 天小时级聚合数据用于年度趋势分析示例:保留 12 个月冷热分层示例:按访问模式、查询SLO与恢复目标选择介质热存储示例:最近 7 天 · 本地 NVMe / SSD响应仪表盘毫秒级查询,访问频繁时序数据库原生格式,单位成本相对较高实时大盘、告警触发温存储示例:8 天~3 个月 · HDD / SSD用于月度报表,访问频度中等月度报表冷存储示例:超过 3 个月 · 对象存储(S3 兼容)Parquet列式 / Avro行式;分区裁剪效果取决于布局用于季度回顾或算法模型训练,访问极少历史分析、批量训练图 5-9 保留与分层期限仅作示例;最终策略由业务和合规目标决定,并以迁移、查询与恢复测试验收。
图 5-9 数据生命周期管理:保留策略与冷热分层

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