TDengine 时序数据库学习手册

—— 从 MySQL / Redis 出发,到 Spring Boot 落地

目录

  1. 一句话认识 TDengine
  2. 为什么需要时序数据库
  3. 核心概念(最重要的一节)
  4. 三种连接方式
  5. TDengine SQL 速查(与 MySQL 对比)
  6. 实战:电池健康数据建模
  7. Spring Boot 集成(完整可运行示例)
  8. 常见坑与最佳实践
  9. 你的项目里怎么用

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 - 1hnow + 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 }

建模原则

  1. 动态测量值(每时刻在变)→ 字段 column。
  2. 静态属性(设备是谁)→ 标签 tag。
  3. 时间戳是主键,别存成字段。

完整 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_codebattery_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 完整验证流程(对照做一遍)

  1. 启动 TDengine:双击桌面 TDengine开关.bat → 1。
  2. 确认端口:netstat -ano | findstr ":6030 :6041"
  3. CLI 建库建表:E:\APP\TDengine\server\taos.exe -s "CREATE DATABASE ...; CREATE STABLE ...;"
  4. 启动你的 Spring Boot 服务,调用上报接口。
  5. 回 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),查询出来再映射成驼峰。

最佳实践清单

  1. 时间戳一定用设备上报的时间,不要用服务端 now(),否则乱序。
  2. 批量写入(batchUpdate),每批 100~1000 条,吞吐最高。
  3. 写库操作放在异步线程 / MQ 消费端,别堵 Netty IO 线程。
  4. 设备上线时不需要预建子表,用 USING ... TAGS(...) 边写边建。
  5. 上报字段里不变的信息放 TAGS(deviceCode、batteryId),别塞进字段。
  6. 聚合查询永远带上时间范围 ts >= now - 1d,否则扫全库。
  7. 单元测试/开发时直接连本机,生产环境把 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