工业时序数据生命周期:Schema 演进、历史查询与冷归档
数据写入成功只是开始。物模型增加字段后表结构如何变化,历史查询返回的是哪段时间,归档失败时能否删除原数据,才决定了数据是否可以继续使用。我在多存储后端的实现中,把这三个问题分别处理。
更新于 2026-09-09。本文依据项目实现与设计记录,区分现有机制和仍需加强的保证,不提供未经验证的性能结论。
配置表与业务表采用不同的变更方式
配置表结构由程序版本决定,适合用 Flyway 管理。采集业务表则由 Product 定义在运行时派生:属性对应数据列,产品增加字段时,表结构也需要协调。
项目因此把运行时业务表交给 Schema 协调器。这不是说 Flyway 不能处理动态场景,而是避免让两套机制同时决定同一张表应该长什么样。代价是需要自己处理差异检测、变更失败与可追溯性。
协调分成三个职责:
| 组件 | 职责 |
|---|---|
SchemaIntrospector |
读取数据库实际结构 |
SchemaReconciler |
比较模型与实际结构,按后端能力执行变更 |
SchemaDiffService |
返回只读差异报告,不执行 DDL |
只读 diff 与执行变更分开,让排查问题不必先修改数据库。MISSING_IN_DB 表示模型有字段但库中没有;MISSING_IN_PRODUCT 表示库中仍有历史列;类型差异则需要结合数据库能力判断。
类型归一化不等于变更安全
原实现按类型族归一化,能减少部分方言表达差异导致的误报。但仅比较类型名有盲区:VARCHAR(255) 与 VARCHAR(256) 的容量不同,缩短长度也不能当成无害差异。
我更倾向于分开讨论两件事:检测到什么差异,以及允许执行什么迁移。
| 变更 | 应核查的内容 |
|---|---|
| 新增列 | 默认值、可空性、旧记录读取行为 |
| 扩大长度或精度 | 后端支持范围、锁与重建成本 |
| 缩短长度或改变类型 | 存量数据可转换性、截断风险、失败回滚 |
| 删除字段 | 历史查询依赖、备份、保留要求 |
当前 auto-drop-columns 默认关闭,避免产品删除属性时顺带删除历史列。但它只约束删列,不能保证其他 DDL 无损。差异报告显示 CLEAN,也只代表实现所比较的维度一致,不等于完成了数据质量验收。
查询时聚合:先控制输出,再评估计算成本
一台设备每秒产生一点,七天就是 604,800 个时间点。这是采样频率推导的数量,不是本项目压测结果。让浏览器接收全部原始点通常没有必要,因此查询接口提供 interval、aggregation 和 fill。
当前选择查询时聚合,减少预聚合任务与派生表的维护。代价是每次查询都可能产生聚合成本;高并发和长时间范围下,仍需评估预聚合、缓存或限制查询范围。
TDengine 使用时间窗口能力,关系数据库使用时间桶表达式。以固定五分钟桶为例,概念公式是:
bucketStart = floor(epochSeconds / 300) × 300
只把时间格式化到分钟,不能形成五分钟桶。跨数据库比较时,还要对齐时区、区间端点和窗口起点;“每 24 小时”也不必然等于某个时区的自然日。
聚合方式必须符合字段语义。温度的均值与最大值回答不同问题;状态位不能默认按温度处理;累计计数也不能不考虑复位就求差。当前通用接口提供聚合能力,不代表已实现所有点位的语义约束。
项目 TDengine 查询默认填充策略为 LINEAR,这是需要谨慎使用的实现选择。插值值不是采集值,尤其不能用平滑曲线掩盖断线。更具体的建模与窗口讨论见TDengine 实践。
数据源一致,不只是返回相同 JSON
HistoryQueryService 当前优先查询可用存储;失败时返回内存数据,并使用 memory-fallback 标记,未启用存储时使用 memory-only。
这个标记很重要:内存回退只取最近 N 条,当前调用没有把原来的 start/end 和聚合要求传入缓存。因此它不能被解释为数据库查询的等价结果。
对前端和 AI 调用方,我的建议是:
- 正常查询显示数据来源和实际覆盖时间。
- 回退结果明确标为近期缓存,不继续声称是指定日期的完整历史。
- 如果任务要求完整统计,返回“当前无法完成该范围查询”,比用一段缓存代替更正确。
另一个容易遗漏的问题是排序:ORDER BY time ASC LIMIT 10 获取较早十条,不是最近十条。当前“最新值”的数据库回退路径也应结合底层排序继续验证,不能仅凭方法名判断行为。
冷归档:文件写入与数据库删除不是一个事务
项目以 DataArchiveService 隔离归档介质,当前实现为 CSV,提供归档、状态和回读入口。CSV 易检查、依赖少,但不具备列式分析、跨文件事务或自动 Schema 演进的全部能力。
归档后删除的关键不是“过期就删”,而是确认哪些记录已经可靠保存。设计记录中曾修正过按整个过期截止时间删除的问题:分批归档时,截止时间之前可能仍有未写出的记录。
使用本批最大时间戳收窄删除范围是一项改进,仍需要检查同时间戳多条数据是否跨批、文件是否持久化、进度标记能否恢复,以及重跑是否重复追加。进度标记存在不等于已经提供严格幂等。
对当前方案,归档默认关闭、归档后删除默认关闭。S3 或 Parquet 可以作为后续介质实现,但不能把接口可扩展写成已经支持这些后端。
从什么证据判断生命周期可用
我会分别验证三类结果:
- Schema:新增属性后可写可读;不兼容变更被明确识别;失败不会被当成协调成功。
- 查询:固定样本能验证时间边界、排序、窗口和缺测;数据库失败时来源与覆盖范围仍准确。
- 归档:回读结果与原记录对应;中断、重跑和分批边界不会造成未归档数据被删除。
当前实现提供了这些能力的入口,但文章不把它们概括成“自动治理且不丢数据”。系统架构的价值,在于让失败位置与数据含义能够被检查。
实现依据:storage/schema、storage/archive、HistoryQueryService 及各存储后端。理解它们与采集配置的关系,可先阅读多协议系统架构。