TDengine 时序数据库学习手册
—— 从 MySQL / Redis 出发,到 Spring Boot 落地
目录
- 一句话认识 TDengine
- 为什么需要时序数据库
- 核心概念(最重要的一节)
- 三种连接方式
- TDengine SQL 速查(与 MySQL 对比)
- 实战:电池健康数据建模
- Spring Boot 集成(完整可运行示例)
- 常见坑与最佳实践
- 你的项目里怎么用
1. 一句话认识 TDengine
TDengine 是专为"时间序列数据"设计的高性能数据库,比如传感器上报的电流电压、温度、GPS 坐标、设备状态。它和你用的 MySQL 一样可以写 SQL,但它把时间戳当成"主键",围绕时间做存储、压缩、聚合,插入和聚合性能比 MySQL 高一个量级。
一句话类比:
- MySQL = 存"业务数据"(用户、订单、库存),数据可能随时改。
- Redis = 存"热数据/缓存"(验证码、Token、排行榜),追求快读快写。
- TDengine = 存"IoT 上报的监控数据"(电压、温度、SOC、SOH),只追加、几乎不改,按时间聚合查询。
你的电池健康数据就是典型时序数据:每个电池组每隔几秒/几分钟上报一条状态,日积月累上亿条,MySQL 扛不住也查询慢,TDengine 天然适合。
2. 为什么需要时序数据库
| 场景 | MySQL | TDengine |
|---|---|---|
| 写入上亿条带时间戳的记录 | 慢,索引膨胀 | 快,列式存储 + 压缩 |
| 按时间范围聚合(avg/max/min) | 慢(全表扫描) | 快(自动分区 + 时间窗口聚合) |
| 数据生命周期管理(过期清理) | 要写定时删任务 | 自动过期(KEEP 天数) |
| 数据更新 | 随意 UPDATE | 不支持改历史数据(只追加) |
| 每台设备的独立索引 | 靠 where 条件 | 子表自动分离,天然按设备隔离 |
一句话:时序数据"只写不改、按时间查",这是 TDengine 的主场。
3. 核心概念
这是和 MySQL 差别最大的地方,务必先看懂。
3.1 库(Database)
相当于 MySQL 的库。但建库时必须指定数据保留策略:
CREATE DATABASE battery_health KEEP 365 DURATION 10 BUFFER 16 WAL_LEVEL 1;
| 参数 | 含义 |
|---|---|
KEEP 365 |
数据保留365 天,超期自动删除(不用再写定时清理任务) |
DURATION 10 |
每个数据文件存 10 天,超 10 天自动滚动为新文件(影响聚合性能,一般不用动) |
BUFFER 16 |
内存池大小 MB |
WAL_LEVEL 1 |
写前日志级别,1 = 写入先落日志再落盘,兼顾性能与安全 |
如果你之前建过库,切换/重建:
DROP DATABASE battery_health;然后重新建。
3.2 超级表(STABLE)★ 最关键的概念
超级表 = "模板",定义字段结构 + 一组标签。 它本身不存数据,只是给一堆"长得一样的表"定规范。可以类比为一个"类(Class)"。
CREATE STABLE battery_health_tb (
ts TIMESTAMP, -- 时间戳(必填,第一个字段)
voltage DOUBLE, -- 总电压 V
current DOUBLE, -- 电流 A
soc DOUBLE, -- 荷电状态 %
soh DOUBLE, -- 健康状态 %
temperature DOUBLE, -- 温度 ℃
resistance DOUBLE, -- 内阻 mΩ
cycle_count INT -- 循环次数
) TAGS (
device_code NCHAR(64), -- 设备编号(标签)
battery_id NCHAR(64) -- 电池组编号(标签)
);
规则:
- 第一个字段必须是
ts TIMESTAMP,即时间戳主键。 TAGS之后的是标签(tag),用来描述"这一台设备是谁"的静态属性。
3.3 子表(Child Table)★ 真正存数据的表
子表 = 超级表的一个"实例",一张子表对应一台设备/一个电池组。 可以类比为"类的实例(对象)"。
-- 创建子表(指定它继承哪个超级表,并给它打上标签值)
CREATE TABLE dev_bat_001 USING battery_health_tb TAGS ('DEV-BMS-001', 'BAT-001');
CREATE TABLE dev_bat_002 USING battery_health_tb TAGS ('DEV-BMS-002', 'BAT-002');
也可以边插边自动建子表(推荐,省得先建表):
INSERT INTO dev_bat_001 USING battery_health_tb TAGS ('DEV-BMS-001', 'BAT-001')
VALUES (now, 48.2, 12.5, 85.3, 92.1, 35.2, 8.5, 1200);
3.4 三者的关系(类比 + 图解)
数据库 battery_health
│
└── 超级表 battery_health_tb (= 类:字段模板 + 标签定义)
│
├── 子表 dev_bat_001 (= 对象:设备1,tag=device_code='DEV-BMS-001')
│ └── 数据行:ts, voltage, current, soc... (按时间追加)
│
└── 子表 dev_bat_002 (= 对象:设备2)
└── 数据行:...
| MySQL 类比 | TDengine |
|---|---|
| 一张大表 + 每行带 device_code 字段 | 超级表 = 字段模板;每个设备一张子表 |
| where device_code='DEV-BMS-001' | 直接查子表 dev_bat_001,天然隔离 |
| 建 100 台设备的表,加索引 | 建 1 个超级表 + 自动生成的子表,不用加索引 |
3.5 为什么这么设计?
时序数据的特征是:每台设备的记录是连续的时间序列。把每台设备单独放一张子表后:
- 写入:按设备并行写,无锁冲突。
- 存储:按设备+时间天然分区,压缩率高。
- 查询:查某台设备 = 查一张小表,快;跨设备聚合 = 后台并表,也快。
重要结论:标签(tag)是"静态元数据",字段(column)是"动态测量值"。 同一个设备的所有行共享一套标签,但测量值各不相同。
4. 三种连接方式
4.1 CLI 命令行(本机已装好)
# 进交互式命令行
E:\APP\TDengine\server\taos.exe
# 或直接执行单条 SQL
E:\APP\TDengine\server\taos.exe -s "show databases;"
4.2 JDBC 原生连接(Java,走 taosd 6030)
jdbc:TAOS://127.0.0.1:6030/battery_health
4.3 REST 连接(Java,走 taosadapter 6041)
jdbc:TAOS-RS://127.0.0.1:6041/battery_health
推荐用 REST 方式(TAOS-RS):兼容性好、跨版本稳定,还支持 HTTP(curl 直接测)。Java 里用 JDBC 驱动连这个 URL 即可,底层是 HTTP 协议。
默认账号:root / taosdata
用 curl 直接测 REST(验证 taosadapter 是否正常):
curl -u root:taosdata -d "show databases;" http://127.0.0.1:6041/rest/sql
5. TDengine SQL 速查(与 MySQL 对比)
5.1 库操作
-- 建库(MySQL 没有 KEEP,TDengine 必须有)
CREATE DATABASE battery_health KEEP 365 DURATION 10 BUFFER 16 WAL_LEVEL 1;
USE battery_health;
SHOW DATABASES;
SHOW STABLES; -- 查所有超级表
SHOW TABLES; -- 查所有子表
DROP DATABASE battery_health;
5.2 建表
CREATE STABLE st (ts TIMESTAMP, val DOUBLE) TAGS (device NCHAR(32)); -- 超级表
CREATE TABLE child USING st TAGS ('dev-1'); -- 子表
5.3 写入(INSERT)
-- 单条(标签值要加引号,字符串用单引号)
INSERT INTO dev_bat_001 USING battery_health_tb TAGS ('DEV-BMS-001','BAT-001')
VALUES (now, 48.2, 12.5, 85.3, 92.1, 35.2, 8.5, 1200);
-- 批量写同一设备(性能好,推荐)
INSERT INTO dev_bat_001
VALUES (now, 48.2, 12.5, 85.3, 92.1, 35.2, 8.5, 1200)
(now + 10s, 48.1, 12.4, 85.4, 92.1, 35.3, 8.6, 1200);
-- 一次写多台设备
INSERT INTO dev_bat_001 VALUES (now, 48.2, ...)
dev_bat_002 VALUES (now, 47.9, ...);
时间戳写法:now(当前)、now - 1h、now + 10s、ISO 字符串 '2026-08-13 10:00:00'、毫秒时间戳。
5.4 查询(和 MySQL 最像的部分)
-- 最近 1 小时某设备全部数据
SELECT * FROM dev_bat_001 WHERE ts >= now - 1h;
-- 按超级表查多设备(会自动并表)
SELECT * FROM battery_health_tb WHERE ts >= now - 1h AND device_code = 'DEV-BMS-001';
-- 时间窗口聚合 ★ TDengine 特色:INTERVAL
-- 每 1 分钟算一次平均 SOC / 最高温度 / 最大内阻
SELECT avg(soc), max(temperature), max(resistance)
FROM battery_health_tb
WHERE ts >= now - 24h AND device_code = 'DEV-BMS-001'
INTERVAL(1m);
5.5 删除 / 更新(和 MySQL 最大差异)
-- TDengine 3.x 不支持 UPDATE 历史数据,也没有 delete by id
-- 可以 DROP TABLE 子表(连数据一起删)
DROP TABLE dev_bat_001;
| 操作 | MySQL | TDengine |
|---|---|---|
| UPDATE 已有行 | 支持 | 不支持(只能用新时间戳插入) |
| DELETE 按条件删 | 支持 | 支持 DELETE FROM tb WHERE ts < now - 1d(按时间删,2.x 才支持) |
| 数据过期清理 | 手动 | 建库时 KEEP 自动清理 |
6. 实战:电池健康数据建模
假设你 IoT 网关收到设备上报,报文类似:
{ "deviceCode": "DEV-BMS-001", "batteryId": "BAT-001",
"voltage": 48.2, "current": 12.5, "soc": 85.3, "soh": 92.1,
"temperature": 35.2, "resistance": 8.5, "cycleCount": 1200,
"timestamp": 1755000000000 }
建模原则
- 动态测量值(每时刻在变)→ 字段 column。
- 静态属性(设备是谁)→ 标签 tag。
- 时间戳是主键,别存成字段。
完整 SQL(照抄即可)
-- 1) 建库(保留 365 天)
CREATE DATABASE IF NOT EXISTS battery_health KEEP 365 DURATION 10 BUFFER 16 WAL_LEVEL 1;
USE battery_health;
-- 2) 建超级表
CREATE STABLE IF NOT EXISTS battery_health_tb (
ts TIMESTAMP,
voltage DOUBLE,
current DOUBLE,
soc DOUBLE,
soh DOUBLE,
temperature DOUBLE,
resistance DOUBLE,
cycle_count INT
) TAGS (
device_code NCHAR(64),
battery_id NCHAR(64)
);
-- 3) 写入(边写边自动建子表)
INSERT INTO dev_bat_001 USING battery_health_tb TAGS ('DEV-BMS-001','BAT-001')
VALUES (1755000000000, 48.2, 12.5, 85.3, 92.1, 35.2, 8.5, 1200);
-- 4) 常用查询
-- 某设备最近 10 分钟
SELECT * FROM battery_health_tb
WHERE device_code='DEV-BMS-001' AND ts >= now - 10m;
-- 按 5 分钟聚合:平均 SOC、最高温度
SELECT avg(soc), max(temperature) FROM battery_health_tb
WHERE device_code='DEV-BMS-001' AND ts >= now - 1d
INTERVAL(5m);
-- 最新一条(取每设备最后一条)
SELECT last_row(ts, soc, soh) FROM battery_health_tb WHERE device_code='DEV-BMS-001';
7. Spring Boot 集成(完整可运行示例)
推荐用 JDBC + REST 连接(TAOS-RS),最省心。三步走。
7.1 加依赖
pom.xml:
<dependency>
<groupId>com.taosdata.jdbc</groupId>
<artifactId>taos-jdbcdriver</artifactId>
<version>3.2.7</version>
</dependency>
(与本机 TDengine 3.0.4.1 兼容;3.x 驱动都能连 3.0 服务端)
7.2 配置数据源(application.yml)
tdengine:
url: jdbc:TAOS-RS://127.0.0.1:6041/battery_health
username: root
password: taosdata
注意
jdbc:TAOS-RS://...是 REST 方式(走 taosadapter 6041);如果用原生方式改成jdbc:TAOS://127.0.0.1:6030/battery_health,驱动类对应不同(见下)。
7.3 配置类(自动注入 JdbcTemplate)
@Configuration
public class TdengineConfig {
@Bean(name = "tdDataSource")
@ConfigurationProperties(prefix = "tdengine")
public DataSource tdDataSource() {
HikariDataSource ds = new HikariDataSource();
// 上面 @ConfigurationProperties 会自动注入 url/username/password
// 如果是原生 TAOS 驱动,改为:new TDengineDriver(); 并把 driver-class 设成 com.taosdata.jdbc.TDengineDriver
ds.setDriverClassName("com.taosdata.jdbc.rs.RestfulDriver");
ds.setMaximumPoolSize(5); // 时序库连接池不要开太大,5 足够
return ds;
}
@Bean(name = "tdJdbcTemplate")
public JdbcTemplate tdJdbcTemplate(@Qualifier("tdDataSource") DataSource ds) {
return new JdbcTemplate(ds);
}
}
两种驱动类:
- REST:
com.taosdata.jdbc.rs.RestfulDriver(对应jdbc:TAOS-RS://...)- 原生:
com.taosdata.jdbc.TDengineDriver(对应jdbc:TAOS://...)
7.4 实体类
public class BatteryHealthRecord {
private long ts;
private double voltage;
private double current;
private double soc;
private double soh;
private double temperature;
private double resistance;
private int cycleCount;
private String deviceCode;
private String batteryId;
// getter/setter 省略
}
7.5 写入 Repository(重点:PreparedStatement 预编译)
@Repository
public class BatteryHealthDao {
@Resource(name = "tdJdbcTemplate")
private JdbcTemplate jdbcTemplate;
/** 生成子表名:tbl_ + deviceCode 小写化,避免子表名里有非法字符 */
private String childTableOf(String deviceCode) {
return "tbl_" + deviceCode.toLowerCase().replace("-", "_");
}
/**
* 写入一条电池健康记录(边写边自动建子表)
* 注意:子表名不能作为 JDBC 占位符 ?,只能拼进 SQL(来源是自己系统的 deviceCode,安全)
*/
public void save(BatteryHealthRecord r) {
String childTable = childTableOf(r.getDeviceCode());
jdbcTemplate.update(
"INSERT INTO " + childTable + " USING battery_health_tb TAGS (?,?) VALUES (?,?,?,?,?,?,?,?)",
r.getDeviceCode(), r.getBatteryId(),
r.getTs(), r.getVoltage(), r.getCurrent(),
r.getSoc(), r.getSoh(), r.getTemperature(),
r.getResistance(), r.getCycleCount());
}
/**
* 批量写入(性能最好,上报往往是批量)
* 统一用 USING ... TAGS 语法,子表不存在也会自动创建
*/
public void batchSave(List<BatteryHealthRecord> list) {
if (list == null || list.isEmpty()) return;
String childTable = childTableOf(list.get(0).getDeviceCode());
String deviceCode = list.get(0).getDeviceCode();
String batteryId = list.get(0).getBatteryId();
jdbcTemplate.batchUpdate(
"INSERT INTO " + childTable + " USING battery_health_tb TAGS (?,?) VALUES (?,?,?,?,?,?,?,?)",
list, list.size(), (ps, r) -> {
ps.setString(1, deviceCode);
ps.setString(2, batteryId);
ps.setLong(3, r.getTs());
ps.setDouble(4, r.getVoltage());
ps.setDouble(5, r.getCurrent());
ps.setDouble(6, r.getSoc());
ps.setDouble(7, r.getSoh());
ps.setDouble(8, r.getTemperature());
ps.setDouble(9, r.getResistance());
ps.setInt(10, r.getCycleCount());
});
}
}
⚠️ 重要:子表名 / 标签值不能作为 JDBC 占位符
?的一部分。子表名只能拼字符串(如上),但标签值device_code、battery_id可以正常用?传参(上面的 INSERT_SQL 里 TAGS (?,?) 就是参数化的)。时间戳用ps.setLong传毫秒即可。
7.6 查询 Repository
@Repository
public class BatteryHealthQueryDao {
@Resource(name = "tdJdbcTemplate")
private JdbcTemplate jdbcTemplate;
/** 最近 N 条原始数据 */
public List<BatteryHealthRecord> recent(String deviceCode, int minutes, int limit) {
return jdbcTemplate.query(
"SELECT ts, voltage, current, soc, soh, temperature, resistance, cycle_count " +
"FROM battery_health_tb WHERE device_code = ? AND ts >= now - " + minutes + "m " +
"ORDER BY ts DESC LIMIT " + limit,
(rs, i) -> {
BatteryHealthRecord r = new BatteryHealthRecord();
r.setTs(rs.getTimestamp("ts").getTime());
r.setVoltage(rs.getDouble("voltage"));
// ... 其余字段照抄
r.setDeviceCode(deviceCode);
return r;
}, deviceCode);
}
/** 按时间窗口聚合:每 N 分钟的平均 SOC / 最高温度 */
public List<Map<String, Object>> aggregate(String deviceCode, int minutes) {
return jdbcTemplate.queryForList(
"SELECT _wstart AS window_start, avg(soc) AS avg_soc, max(temperature) AS max_temp " +
"FROM battery_health_tb WHERE device_code = ? AND ts >= now - 1d " +
"INTERVAL(" + minutes + "m)",
deviceCode);
}
}
7.7 Service 层调用(在 IoT 网关消息处理里)
@Service
public class BatteryHealthService {
@Resource
private BatteryHealthDao batteryHealthDao;
/**
* 从 Netty 上报消息构建记录并入库
* 建议用 @Async 或消息队列解耦,别阻塞 IO 线程
*/
public void onMessage(DeviceMessage msg) {
BatteryHealthRecord r = new BatteryHealthRecord();
r.setTs(msg.getTimestamp());
r.setDeviceCode(msg.getDeviceCode());
r.setBatteryId(msg.getBatteryId());
r.setVoltage(msg.getVoltage());
// ... 映射字段
batteryHealthDao.save(r);
}
}
7.8 完整验证流程(对照做一遍)
- 启动 TDengine:双击桌面
TDengine开关.bat→ 1。 - 确认端口:
netstat -ano | findstr ":6030 :6041"。 - CLI 建库建表:
E:\APP\TDengine\server\taos.exe -s "CREATE DATABASE ...; CREATE STABLE ...;"。 - 启动你的 Spring Boot 服务,调用上报接口。
- 回 CLI 查:
use battery_health; select * from battery_health_tb where ts >= now - 10m;
8. 常见坑与最佳实践
坑1:建库后没有数据目录 / 连不上
- 确认 taosd 在跑:
sc query taosd应显示 RUNNING。 - 用 REST 直连测试:
curl -u root:taosdata -d "show databases;" http://127.0.0.1:6041/rest/sql。 - 你本机 taosd 读的是
C:\TDengine\cfg\taos.cfg(目录联接指向 E 盘),fqdn 必须127.0.0.1。
坑2:标签值忘记加引号报错
-- 错(报语法/参数错误)
INSERT ... TAGS(DEV-BMS-001) VALUES ...
-- 对
INSERT ... TAGS('DEV-BMS-001') VALUES ...
坑3:想 UPDATE 数据 → 报"不支持"
时序数据只追加。真需要修正,用新时间戳插一条,或删表重建。
坑4:连接池别开太大
TDengine 原生连接很轻,连接池 5~10 就够,开 100 反而拖慢。
坑5:JdbcTemplate 与普通 SQL 的坑
- 表名/子表名不能参数化,只能拼接(注意防注入,子表名来源要是自己系统生成的 deviceCode)。
- INTERVAL 的窗口大小不能参数化,拼字符串(是固定业务值,安全)。
- 列名用下划线(
cycle_count),查询出来再映射成驼峰。
最佳实践清单
- 时间戳一定用设备上报的时间,不要用服务端
now(),否则乱序。 - 批量写入(batchUpdate),每批 100~1000 条,吞吐最高。
- 写库操作放在异步线程 / MQ 消费端,别堵 Netty IO 线程。
- 设备上线时不需要预建子表,用
USING ... TAGS(...)边写边建。 - 上报字段里不变的信息放 TAGS(deviceCode、batteryId),别塞进字段。
- 聚合查询永远带上时间范围
ts >= now - 1d,否则扫全库。 - 单元测试/开发时直接连本机,生产环境把 URL 放进配置中心即可切换。
9. 你的项目里怎么用
你的 dianchi-iot-gateway-service 是 Netty TCP 网关,正好是数据入口:
- 数据流:设备 → Netty(9701) → TelemetryHandler → (MQ/异步) →
BatteryHealthService.save()→ TDengine。 - 建库建表:服务启动时执行一次第 6 节的建库建表 SQL(或写在初始化脚本里)。
- 连接:走 REST 6041,
jdbc:TAOS-RS://127.0.0.1:6041/battery_health。 - 查询出口:
dianchi-ai-service(AI 服务)或监控看板,按设备 + 时间窗口聚合,喂给健康度评估/预测。
参考框架位置:dianchi-iot-gateway-service/IOT服务_小白入门完整手册.md(Netty 部分)+ 本手册(TDengine 部分),两个一起看即可打通"上报 → 存储 → 查询"全链路。
附:常用运维命令速查
# 服务开关(或用桌面 TDengine开关.bat)
sc start taosd
sc start taosadapter
sc stop taosd
sc stop taosadapter
# 状态
sc query taosd | findstr STATE
netstat -ano | findstr ":6030 :6041"
# CLI
E:\APP\TDengine\server\taos.exe
参考文档
- TDengine 官方文档:https://docs.taosdata.com/
- TDengine 官方博客:https://www.taosdata.com/blog/
- 驱动下载(Java 驱动 taos-jdbcdriver):https://mvnrepository.com/artifact/com.taosdata.jdbc/taos-jdbcdriver
评论