在工业采集系统中使用 TDengine:物模型、写入与历史查询
接入时序数据库不只是换一个 JDBC URL。设备怎样映射到表,字段与标签怎样划分,补写数据使用哪个时间,聚合结果能否代表原始采样,都会影响上层看板与 AI 回答。本文从项目实现出发,沿着一条温度数据说明这些选择。
更新于 2026-09-09。项目 pom.xml 使用 taos-jdbcdriver 3.4.0,代码采用 WebSocket JDBC 与连接池。本文是源码实践复盘与查询示例,不是本轮数据库联机或性能报告;服务端与 taosAdapter 的部署版本未在本次写作中实测确认。
先决定数据模型,再决定写入语句
项目以 Product 描述同类数据属性,在 TDengine 后端映射为 stb_{productKey};设备映射为 d_{deviceId} 子表。模板负责把具体协议地址转换成产品属性。
Product:temperature、pressure、running
↓ 公共列结构
产品超级表
/ \
设备 A 子表 设备 B 子表
时间 + 数值 时间 + 数值
时间持续变化的测量值应作为普通列。设备标识、位置等上下文可以讨论作为标签,但需要考虑是否稳定、如何变更,以及查询是否需要历史归属。
如果设备搬到另一条产线,业务要求保留“当时所属产线”,就不能不加分析地把当前标签当成每个历史时刻的属性。建模应该先回答历史语义,而不是仅考虑查询方便。
产品分组也不是越细越好。每个设备都建独立产品会降低模型复用;强行让完全不同的设备共享一张宽表,又会增加稀疏字段和解释成本。
JDBC 统一了接口,没有统一数据库语义
项目数据源显式使用 com.taosdata.jdbc.ws.WebSocketDriver,URL 形式为 jdbc:TAOS-WS://host:6041/database,凭据单独配置。官方 Java 连接器文档说明 WebSocket 连接不依赖本地客户端库,并给出驱动与功能的版本说明。Java Client Library
接入时仍要分别验证连接、参数绑定、查询结果类型与异常。不能因为 JDBC 接口相同,就默认关系数据库的事务能力、DDL 行为和所有 SQL 方言都成立。
另一个项目层面的注意点是 Spring 数据源装配。TDengine 使用独立存储连接池,关系配置库采用另一套装配机制。独立 DataSource 可能影响框架的条件自动配置,因此“配置库存 MySQL、采集数据存 TDengine”这个架构方向,还需要对实际组合配置做启动验证。
采集时间与写入时间分开
当前 save 的时间选择可简化为以下项目逻辑:
long ts = request.getTimestamp() != null
? request.getTimestamp()
: System.currentTimeMillis();
params.add(new Timestamp(ts));
它保留了请求携带的时间,适合消息转入或延迟写入。但这只证明存储层接受该时间,仍要检查上游传的是采集时刻、设备时钟还是消息接收时刻,以及单位是否为毫秒。
我会把下面这些含义写入数据契约:事件时间来自哪里,精度是什么,设备时钟如何处理,缺失时是否允许回退,展示时采用哪个时区。使用服务器当前时间可以保证有值,却会改变延迟数据的历史位置。
重复时间戳和补写也需要独立验证:它们涉及行标识与更新策略,不能把“重试一次 INSERT”直接称为幂等。
标签按名称取值,按声明顺序组织
INSERT 的标签值不能随 Map 的遍历顺序拼接。当前代码根据产品声明顺序逐项按名称取值,形成明确的字段对应;缺失值再按约定寻找默认值。
这种映射避免把“产线”写进“位置”的槽位,但它没有证明修改已有超表标签定义一定安全。配置顺序、数据库实际结构和语句指定方式必须保持一致。
测量值走参数绑定,表名和列名则不能当成普通值绑定。动态标识符需要按模型白名单选择并校验,单纯在字符串外面添加引号不能替代完整检查。
当前源码仍有 extra_data 写入分支,不能沿用旧文档“已经删除兜底列”的说法。字段未匹配时是否保留、是否可查询,应该结合实际建表结构确认。
KEEP/BUFFER 的作用域影响配置设计
项目当前按共享库的全局参数创建数据库,代码注释明确 KEEP/BUFFER 的库级作用域。不能让同一数据库下各产品轮流覆盖各自的保留设置。
如果不同数据需要不同保留周期,可以评估分库、独立归档策略或所用版本支持的其他机制。每一种方案都有跨库查询和维护代价。
另一个细节是 CREATE DATABASE IF NOT EXISTS:它用于确保存在,不能据此声称修改配置后已有数据库就同步更新。变更存量设置需要单独的管理过程。
历史查询:先说明时间范围,再说明 limit
以下是面向温度点位的 SQL 示意,假设已有设备子表 d_sensor_a,不包含建库或清理操作。时间使用 UTC 偏移,具体版本仍需验证解析与会话设置。
SELECT ts, temperature
FROM d_sensor_a
WHERE ts >= '2026-09-01T00:00:00.000Z'
AND ts < '2026-09-01T01:00:00.000Z'
ORDER BY ts DESC
LIMIT 100;
这表达“范围内最近一百条”。若图表要求升序,可以在取得最近记录后反转,不能直接将 ASC 与 LIMIT 组合后仍称为“最近”。官方迁移示例也区分最近行、LAST_ROW 与字段的 LAST;后者的非空值语义不应被误当成同一时刻的整行快照。查询与最新值示例
当前项目原始查询按 ASC 排序后取 limit,端点采用包含式上界。上面的半开区间与倒序是说明更明确契约的示例,不是已经修改后的项目输出。调用方需要知道差异,尤其是跨页与连续时间段查询。
聚合点的时间戳是什么
聚合后的一个点代表整个窗口,不是某一条原始记录。使用 _wstart 表达窗口起点,比含糊地选择原始 ts 更清楚。官方文档给出了窗口与填充的语义。Data Querying
SELECT _wstart AS window_start, AVG(temperature) AS avg_temperature
FROM d_sensor_a
WHERE ts >= '2026-09-01T00:00:00.000Z'
AND ts < '2026-09-01T01:00:00.000Z'
INTERVAL(1m)
FILL(NONE);
这同样是待在目标环境执行的示例。当前项目聚合构造使用原始 ts,需要结合实际版本核对;不能把源码拼出的字符串当成已验证的可移植 SQL。
用构造样本可以先明确期望:
| 样本时刻(UTC) | 温度 |
|---|---|
| 00:00:10 | 20 |
| 00:00:40 | 24 |
| 00:02:10 | 30 |
按一分钟窗口,00:00 桶均值应为 22,00:01 桶没有样本,00:02 桶为 30。这是按样本计算的期望值,不是数据库执行截图。可以用它检查窗口起点与缺口,然后再扩展到两台设备的分组。
项目接口默认 LINEAR,而上面示例显式 NONE,是为了保留缺测事实。温度插值可用于特定分析,状态位和计数器通常需要不同处理;插值后的值不能直接作为“传感器确实测到”的依据。
不隐藏回退,也不虚构性能
数据库查询失败时,当前上层服务可能回退近期内存数据。返回结构一致不代表范围一致,必须保留 source 与实际覆盖范围,详见数据生命周期。
进一步验证应包括:真实驱动与服务端版本、时间精度、两个设备的标签、重复写入、原始与窗口查询、数据库不可用时的返回语义。性能比较还需要给出点位数量、批次、并发、硬件和查询范围;本文没有新增这些实验,因此不声称某种写入方式最快。
TDengine 在这里承担时序存储与查询。采用完整 All-in-One 是另一层产品决策,参见平台能力与自研边界。