TDengine 开源时序数据库深度解析:从超级表到工业数采落地

1. 背景:工业与物联网海量时序数据的痛点

在 CNC 数控机床、PLC 产线、传感器网关等工业数采场景中,数据有一个共同的形态:每条数据都带一个时间戳,且按时间顺序持续产生。设备点位(主轴转速、进给速度、主轴负载、温度、振动、气压、报警代码)每秒甚至每毫秒上报一次,一台 CNC 可能有几十到几百个点位,一个车间上百台设备,一天的写入量轻松达到数亿条。这带来四类传统方案难以招架的痛点:

  1. 写入吞吐不足:关系型数据库按行存储、B+Tree 索引每次写入都要维护,单机每秒几万行的写入对工业点位量级是杯水车薪;而时序数据 90% 以上是追加写(append-only),需要针对"批量顺序追加"做专门优化。
  2. 存储膨胀与过期清理困难:点位数据大多"越老越没用",但业务又有保留窗口(如保留 90 天原始数据、5 年降采样数据)。传统数据库删旧数据要么 DELETE 加锁影响写入,要么 DBA 手工分区,维护成本极高。
  3. 查询模式错配:时序查询几乎总是"按设备(标签)+ 时间范围"的二维切片,还要做降采样(原始 1s 数据聚合成 1min 均值)。关系型数据库的二级索引对此效率低下;通用 KV(如 RocksDB)只提供 key-value 单点读写,无法表达"某时间段内某标签组所有序列的聚合"。
  4. 高基数标签维度:一个表如果按"设备 + 点位"拆列,设备数量变化就要改表结构;如果塞进一张大宽表,又会出现大量稀疏列。时序数据天然需要"实体(标签)+ 多个测量列(指标)"的分层模型。

TDengine(涛思数据,GitHub 开源,核心存储引擎由 C 语言实现)正是围绕这些痛点设计:一设备一子表(table)、一类设备一超级表(STable)、标签与指标分离、按时间戳主键排序存储、内置保留策略与时间窗口聚合。它对外提供标准 SQL(类 MySQL 语法),让熟悉 SQL 的工程师可以零门槛上手,同时把时序场景的写入、压缩、过期清理都内化到引擎层。

为什么选"数据库"而不是"消息队列"或"文件"?因为数采链路中数据到达后需要可查询、可聚合、可告警------MQTT/Kafka 负责传输,TDengine 这类时序库负责"落库后可分析",二者是上下游关系(与既有 kafka-go / librdkafka / paho MQTT 篇的采集链路直接衔接)。


2. 定位差异:与关系型数据库、通用 KV 及主流库对比

2.1 与关系型数据库(MySQL/PostgreSQL/SQLite)的定位差异

维度 关系型数据库 TDengine
数据形态 行式存储,支持任意关联查询 列式 + 按时间有序存储,天然面向时间切片
写入模型 随机/批量插入,事务开销 时序追加写优化,批量写入吞吐可达数百万条/秒(单机)
主键 任意列,唯一约束 强制时间戳 + 子表标签组合(v3 支持联合主键),时序语义
过期数据 手动 DELETE / 分区维护 数据库级 keep 保留策略自动过期
降采样 手写 GROUP BY 时间桶,性能差 INTERVAL 原生窗口聚合,扫描即算
适合场景 交易、账务、强一致性关联数据 监控、遥测、点位历史、趋势分析

2.2 与通用 KV(RocksDB)的定位差异

RocksDB(第 44 篇已写)是嵌入式 LSM-Tree 键值库:写放大低、适合海量 KV 随机/顺序写,但没有 SQL、没有时间窗口聚合、没有标签维度索引。TDengine 虽然底层也吸收了 LSM/有序存储的思想,但对外是完整的"数据库":建表、SQL、聚合、保留策略、连接器都是开箱即用的。RocksDB 是"存储引擎零件",TDengine 是"时序数据库整机"。

2.3 与嵌入式分析库 DuckDB / SQLite 的对比

库 类型 数据模型 典型用途 与 TDengine 关系
DuckDB 嵌入式 OLAP 列存 关系表,可直接查 CSV/Parquet 单机分析、跨文件查询 分析端互补:TDengine 负责时序写入与在线查询,DuckDB 可对导出的历史快照做深度分析
SQLite 嵌入式 OLTP 行存 关系表 + B-Tree 本地配置、单机业务 不适合高频时序追加与时间窗口聚合;TDengine 为服务端时序库
RocksDB 嵌入式 KV(LSM) Key-Value 缓存、状态存储、底层引擎 无 SQL/无聚合,是引擎零件而非完整 DB
TDengine 服务端时序数据库 库 + 超级表 + 子表 + 标签 工业/物联网时序采集与查询

2.4 与 InfluxDB 的对比(同类时序库选型)

维度 InfluxDB TDengine
数据模型 measurement + tag set + field(无强制 schema) 强 schema:超级表/子表/标签/列
查询语言 InfluxQL / Flux 标准 SQL(类 MySQL),学习成本低
集群与生态 集群版商业授权 开源版支持集群(3.0 起重构)
高基数处理 tag 基数过高内存压力大 标签独立索引 + 子表按序存储,高基数友好
定位 监控时序老牌选手 国产开源、SQL 友好、工业场景深耕

核心结论:TDengine 的差异点是"用 SQL 表达时序语义"------把"设备 + 时间 + 指标"三要素固化成库/超级表/子表三层模型,让工业工程师用最熟悉的 SQL 完成写入、窗口聚合与保留管理。


3. 核心概念与 API 说明

3.1 数据模型:库、超级表、子表、标签、列、时间戳主键

TDengine 的数据组织是四层结构:

复制代码
DATABASE(库)
 └── STable(超级表,一类设备的模板)
      ├── 标签 Tag(静态属性:device_id / line / type,可增删改)
      └── 子表 Table(一个具体设备,继承超级表结构 + 指定标签值)
           └── 列(第一列必须是时间戳主键 ts,其后为指标列)
  • 库(Database):物理存储与策略容器。建库时指定保留策略 keep、时间戳精度 PRECISION、副本数 REPLICA、缓存 CACHEMODEL 等。所有表归属某个库。
  • 超级表(STable) :一类设备的模板,定义列结构 (时间戳 + 指标列)与标签 schema(设备维度字段)。一个超级表下有多个子表。
  • 子表(Table) :实际存储数据的表,继承超级表的列结构,并绑定一组具体标签值(如 device_id='CNC-001', line='L1')。每个子表内部按时间戳有序存储,这就是时序追加快的根源。
  • 标签(Tag) :静态维度元数据,建子表时指定。标签不是数据列,不随每次写入变化;查询时可 WHERE 过滤、GROUP BY 分组、PARTITION BY 分区。标签值修改用 ALTER TABLE ... SET TAG。
  • 列(Column):除首列时间戳外的数据列,类型支持 BOOL / TINYINT / SMALLINT / INT / BIGINT / FLOAT / DOUBLE / VARCHAR / NCHAR / TIMESTAMP / JSON(v3 支持 JSON 类型列)等。
  • 时间戳主键(Timestamp) :每张表第一列必须为时间戳,精度在建库时指定(ms/us/ns,默认 ms)。时间戳必须是严格递增或允许等值(由建库参数控制,见常错点)。

心智模型:超级表 = 关系数据库的"表结构 + 分区维度" ,子表 = 一个具体实体的时间序列 ,标签 = 关系数据库的索引列/分区键。查询时你写 SQL 操作超级表,TDengine 自动路由到所有子表并合并结果。

3.2 SQL:CREATE DATABASE / STABLE / TABLE

sql 复制代码
-- 建库:keep 保留 365 天;PRECISION 'ms' 时间戳毫秒;WAL 与缓存参数按需
CREATE DATABASE IF NOT EXISTS iot_db KEEP 365 DAYS 10 BLOCKS 4 CACHEMODEL 'both';
USE iot_db;

-- 建超级表:cnc_metrics 含时间戳 ts、指标列 spindle_speed / spindle_load / temp
CREATE STABLE IF NOT EXISTS cnc_metrics (
    ts TIMESTAMP,
    spindle_speed FLOAT,      -- 主轴转速 rpm
    spindle_load FLOAT,       -- 主轴负载 %
    temp FLOAT                -- 主轴温度 ℃
) TAGS (
    device_id VARCHAR(32),    -- 设备编号
    line VARCHAR(16),         -- 产线
    model VARCHAR(32)         -- 设备型号
);

-- 建普通表(不建超级表也可以,直接建子表/普通表,但不推荐丢失标签维度)
CREATE TABLE IF NOT EXISTS cnc_metrics_001 (
    ts TIMESTAMP,
    spindle_speed FLOAT,
    spindle_load FLOAT,
    temp FLOAT
);

要点:

  • CREATE DATABASE 的 KEEP 决定数据保留天数(默认 3650 天,可设 KEEP 90)。
  • CREATE STABLE 时列结构定义在前、TAGS 定义在后,标签类型与列类型集合不同(标签不支持 TIMESTAMP)。
  • 一个库可建多个超级表;超级表数量、子表数量在 v3 中大幅放宽(万级超级表、亿级子表可管理)。

3.3 INSERT 自动建表

TDengine 最常用的写法是 INSERT INTO ... USING STable TAGS (...) VALUES (...) 自动建表:写入时若子表不存在,引擎自动按超级表模板 + 标签值建子表,然后插入数据。这是多设备批量接入的"零手工建表"模式:

sql 复制代码
-- 自动建子表 cnc_metrics_001(不存在则建),标签绑定后写入一条
INSERT INTO cnc_metrics_001 USING cnc_metrics TAGS ('CNC-001','L1','Mazak') 
    VALUES ('2026-09-24 08:00:00.000', 4200.0, 62.5, 41.2);

-- 批量多值
INSERT INTO cnc_metrics_001 USING cnc_metrics TAGS ('CNC-001','L1','Mazak') VALUES
    ('2026-09-24 08:00:00.100', 4201.0, 62.6, 41.3),
    ('2026-09-24 08:00:00.200', 4200.5, 62.4, 41.2);

-- 多表多值一次提交(推荐批量写法)
INSERT INTO cnc_metrics_001 USING cnc_metrics TAGS ('CNC-001','L1','Mazak') VALUES (...)
       cnc_metrics_002 USING cnc_metrics TAGS ('CNC-002','L1','DMG') VALUES (...);

3.4 SELECT 聚合与时间窗口 INTERVAL / SLIDING、GROUP BY tags

时序查询两大支柱:标签过滤 与时间窗口聚合(降采样)。

sql 复制代码
-- 按标签过滤 + 时间范围
SELECT device_id, ts, spindle_load FROM cnc_metrics
WHERE line = 'L1' AND ts >= '2026-09-24 08:00:00' AND ts < '2026-09-24 09:00:00';

-- 降采样:每 5 分钟一个窗口,窗口滑动 1 分钟(重叠窗口)
SELECT _wstart, _wend, AVG(spindle_load), MAX(spindle_load), COUNT(*)
FROM cnc_metrics
WHERE ts >= '2026-09-24 08:00:00' AND ts < '2026-09-24 12:00:00'
INTERVAL(5m) SLIDING(1m)
GROUP BY device_id;

-- 按标签分组统计(不按时间窗口)
SELECT device_id, COUNT(*), AVG(temp) FROM cnc_metrics
WHERE ts >= '2026-09-24 08:00:00' AND ts < '2026-09-24 09:00:00'
GROUP BY device_id;

-- 按多个标签维度分组
SELECT line, model, AVG(spindle_load) FROM cnc_metrics
WHERE ts >= '2026-09-24 08:00:00' AND ts < '2026-09-24 09:00:00'
GROUP BY line, model;

要点:

  • INTERVAL(5m) 定义窗口长度,SLIDING(1m) 定义窗口推进步长(默认等于 INTERVAL)。_wstart / _wend 是窗口起止伪列。
  • INTERVAL 查询中若需要按设备分别开窗,必须配合 GROUP BY 标签 或 PARTITION BY;否则所有子表数据会合并进同一窗口。
  • 窗口聚合对告警(如"最近 5 分钟平均负载 > 90%")非常自然。

3.5 保留策略 keep

sql 复制代码
-- 建库时指定保留 90 天
CREATE DATABASE iot_db KEEP 90;
-- 修改已有库
ALTER DATABASE iot_db KEEP 180;
  • keep 单位是天,超过保留期的数据由系统后台自动删除(按 vnode 时间有序删除,代价低)。
  • 生产实践:原始点位数据 keep 90,降采样结果单独建库/表 keep 1095(3 年),兼顾容量与追溯。

3.6 REST 接口(端口 6041)

TDengine 从 2.x 起内置 taosAdapter ,在 6041 端口提供 HTTP/REST API,兼容"类 SQL"请求体:

复制代码
POST http://<host>:6041/rest/sql/<dbname>
Authorization: Basic base64(user:pass)
Body: SQL 语句字符串(如 "SELECT * FROM cnc_metrics LIMIT 1")

返回 JSON:

sql 复制代码
{
  "code": 0,
  "desc": "succ",
  "column_meta": [["ts","TIMESTAMP",8],["spindle_speed","FLOAT",4]],
  "data": [["2026-09-24T08:00:00.000+0800", 4200.0]]
}
  • REST 适合跨语言、跨平台的轻量接入(脚本、浏览器、边缘网关、无 SDK 的语言)。
  • 支持 GET /rest/sql(仅查询)与 POST /rest/sql(读写均可);/rest/sqlt 返回时间戳为可读格式。
  • 默认账号 root / taosdata(生产务必修改)。
  • 端口 6041 与原生连接端口 6030 的区别见常错点 19。

3.7 Go 连接器:driver-go / taosSql

TDengine 官方 Go 连接器为 github.com/taosdata/driver-go/v3,提供两层 API:

  1. taosSql(database/sql 驱动):实现 database/sql/driver 接口,可用标准 sql.Open / Query / Exec 编程,SQL 语法与 TDengine 一致。
  2. taosRestful:走 REST(6041)的 database/sql 驱动,适合无原生端口(6030)暴露的跨网络环境。
Go 复制代码
import (
    "database/sql"
    _ "github.com/taosdata/driver-go/v3/taosSql" // 原生驱动:6030
    // _ "github.com/taosdata/driver-go/v3/taosRestful" // REST 驱动:6041
)

db, err := sql.Open("taosSql", "root:taosdata@tcp(localhost:6030)/iot_db")
if err != nil { panic(err) }
defer db.Close()

// 查询
rows, err := db.Query(`SELECT device_id, AVG(spindle_load) FROM cnc_metrics
                       WHERE ts >= NOW - 1h INTERVAL(5m) GROUP BY device_id`)
// 迭代 rows:rows.Next() + rows.Scan(&deviceID, &avgLoad)
  • DSN 格式:user:pass@tcp(host:port)/dbname。
  • 原生(taosSql)与 REST(taosRestful)二选一 import,二者不能同时注册默认名。
  • 大数据量查询注意 rows.Close(),批量写入建议用参数绑定(taosSql 也支持 stmt)。

3.8 C 连接器:taos_connect / taos_query / stmt 参数绑定

TDengine 核心为 C 实现,C 连接器即官方 libtaos:

cpp 复制代码
#include <taos.h>

taos_init();
TAOS *taos = taos_connect("localhost", "root", "taosdata", "iot_db", 6030);
if (taos == NULL) { fprintf(stderr, "connect failed: %s\n", taos_errstr(taos)); return -1; }

// 简单查询
TAOS_RES *res = taos_query(taos, "SELECT COUNT(*) FROM cnc_metrics");
if (taos_field_count(res) == 0) { /* 非查询语句 */ }
TAOS_ROW row;
while ((row = taos_fetch_row(res)) != NULL) {
    printf("count = %s\n", (char *)row[0]);
}
taos_free_result(res);
taos_close(taos);
taos_cleanup();

参数绑定(stmt)批量写入是 C 侧高性能推荐路径(v3 提供 taos_stmt_* 系列):

cpp 复制代码
TAOS_STMT *stmt = taos_stmt_init(taos);
const char *sql = "INSERT INTO ? USING cnc_metrics TAGS (?, ?, ?) VALUES (?, ?, ?, ?)";
taos_stmt_prepare(stmt, sql, (unsigned long)strlen(sql));

// 依次 bind 表名、三个 tag、四个 value(ts, speed, load, temp)
taos_bind_param(stmt, 1, TAOS_BIND_PARAM_TBNAME, "cnc_metrics_001", 0);
taos_bind_param(stmt, 2, TAOS_BIND_PARAM_TAG, "CNC-001", 0);
/* ... 其余 tag 与 value ... */
taos_stmt_add_batch(stmt);          // 每行 add_batch
taos_stmt_execute(stmt);            // 批量提交
taos_stmt_close(stmt);

3.9 Python 连接器:taospy

官方 Python 连接器 taospy(对标 DB-API 2.0):

python 复制代码
import taospy

conn = taospy.connect(host="localhost", user="root", password="taosdata",
                      database="iot_db", port=6030)
cur = conn.cursor()
cur.execute("SELECT device_id, AVG(spindle_load) FROM cnc_metrics "
            "WHERE ts >= NOW - 30m INTERVAL(5m) GROUP BY device_id")
for row in cur.fetchall():
    print(row)
cur.close()
conn.close()
  • taospy 走原生 6030;另有 taospyrest 走 REST。
  • 与 pandas 配合:cur.execute(...) 后用 taospy 提供的结果可转 DataFrame 做进一步分析(衔接 NumPy 篇)。

4. 详细使用说明

4.1 Docker 安装启动

bash 复制代码
# 拉取并启动(默认端口 6030 原生 / 6041 REST)
docker run -d --name tdengine \
  -p 6030:6030 -p 6041:6041 \
  -v tdengine-data:/var/lib/taos \
  tdengine/tdengine:3.3.0.0

# 进入容器执行 taos CLI
docker exec -it tdengine taos

# 查看版本与运行状态
taos> SELECT server_version();
taos> SHOW DNODES;
  • 生产部署建议使用 taosdump 备份、配置 TAOS_FQDN 与 firstEp;单机试用上述命令即可。
  • 防火墙需放行 6030/6041。

4.2 REST 建库建表插入查询(curl 实战)

bash 复制代码
BASE=http://localhost:6041
AUTH="root:taosdata"

# 建库
curl -u "$AUTH" -d "CREATE DATABASE IF NOT EXISTS iot_db KEEP 90" $BASE/rest/sql

# 建超级表
curl -u "$AUTH" -d "CREATE STABLE IF NOT EXISTS iot_db.cnc_metrics (
    ts TIMESTAMP, spindle_speed FLOAT, spindle_load FLOAT, temp FLOAT
) TAGS (device_id VARCHAR(32), line VARCHAR(16), model VARCHAR(32))" $BASE/rest/sql

# 插入(自动建子表)
curl -u "$AUTH" -d "INSERT INTO iot_db.cnc_metrics_001 USING iot_db.cnc_metrics
    TAGS ('CNC-001','L1','Mazak') VALUES
    ('2026-09-24 08:00:00.000', 4200.0, 62.5, 41.2),
    ('2026-09-24 08:00:00.100', 4201.0, 62.6, 41.3)" $BASE/rest/sql/iot_db

# 查询
curl -u "$AUTH" -d "SELECT _wstart, AVG(spindle_load), MAX(temp)
    FROM iot_db.cnc_metrics
    WHERE ts >= '2026-09-24 08:00:00' AND ts < '2026-09-24 12:00:00'
    INTERVAL(5m) GROUP BY device_id" $BASE/rest/sql/iot_db

返回 JSON 中 code == 0 表示成功,column_meta 描述列类型,data 为行数组。

4.3 Go 最小可运行示例

文件:main.go

Go 复制代码
package main

import (
	"database/sql"
	"fmt"
	"log"

	_ "github.com/taosdata/driver-go/v3/taosSql"
)

func main() {
	dsn := "root:taosdata@tcp(localhost:6030)/iot_db"
	db, err := sql.Open("taosSql", dsn)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 建库建表(幂等)
	mustExec(db, "CREATE DATABASE IF NOT EXISTS iot_db KEEP 90")
	mustExec(db, "CREATE STABLE IF NOT EXISTS iot_db.cnc_metrics ("+
		"ts TIMESTAMP, spindle_speed FLOAT, spindle_load FLOAT, temp FLOAT"+
		") TAGS (device_id VARCHAR(32), line VARCHAR(16), model VARCHAR(32))")

	// 插入(自动建子表)
	mustExec(db, `INSERT INTO iot_db.cnc_metrics_001 USING iot_db.cnc_metrics
		TAGS ('CNC-001','L1','Mazak') VALUES
		('2026-09-24 08:00:00.000', 4200.0, 62.5, 41.2),
		('2026-09-24 08:00:00.100', 4201.0, 62.6, 41.3),
		('2026-09-24 08:00:00.200', 4200.5, 62.4, 41.2)`)

	// 窗口降采样查询
	rows, err := db.Query(`SELECT _wstart, device_id, AVG(spindle_load), MAX(temp)
		FROM iot_db.cnc_metrics
		WHERE ts >= '2026-09-24 08:00:00' AND ts < '2026-09-24 08:30:00'
		INTERVAL(5m) GROUP BY device_id`)
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()

	for rows.Next() {
		var wstart string
		var dev string
		var avg float64
		var mx float64
		if err := rows.Scan(&wstart, &dev, &avg, &mx); err != nil {
			log.Fatal(err)
		}
		fmt.Printf("window=%s device=%s avg_load=%.2f max_temp=%.2f\n",
			wstart, dev, avg, mx)
	}
	if err := rows.Err(); err != nil {
		log.Fatal(err)
	}
}

func mustExec(db *sql.DB, sqlText string) {
	if _, err := db.Exec(sqlText); err != nil {
		log.Fatalf("exec %q: %v", sqlText, err)
	}
}

运行:

bash 复制代码
go mod init tdengine-demo
go get github.com/taosdata/driver-go/v3@v3.5.0
go run main.go

4.4 C 最小可运行示例(含参数绑定批量写入)

文件:demo.c

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <taos.h>

int main(void) {
    taos_init();
    TAOS *taos = taos_connect("localhost", "root", "taosdata", "", 6030);
    if (taos == NULL) {
        fprintf(stderr, "connect failed\n");
        return -1;
    }

    // 建库建表(幂等)
    taos_query(taos, "CREATE DATABASE IF NOT EXISTS iot_db KEEP 90");
    taos_query(taos, "USE iot_db");
    taos_query(taos,
        "CREATE STABLE IF NOT EXISTS cnc_metrics ("
        "ts TIMESTAMP, spindle_speed FLOAT, spindle_load FLOAT, temp FLOAT"
        ") TAGS (device_id VARCHAR(32), line VARCHAR(16), model VARCHAR(32))");

    // 参数绑定批量写入:INSERT INTO ? USING stb TAGS (?,?,?) VALUES (?,?,?,?)
    TAOS_STMT *stmt = taos_stmt_init(taos);
    const char *sql =
        "INSERT INTO ? USING cnc_metrics TAGS (?, ?, ?) VALUES (?, ?, ?, ?)";
    taos_stmt_prepare(stmt, sql, (unsigned long)strlen(sql));

    const char *tbname = "cnc_metrics_001";
    const char *dev    = "CNC-001";
    const char *line   = "L1";
    const char *model  = "Mazak";

    // 3 行批量(时间戳毫秒;float 值直接传 float*)
    long long ts[3] = {1727143200000LL, 1727143200100LL, 1727143200200LL};
    float speed[3] = {4200.0f, 4201.0f, 4200.5f};
    float load[3]  = {62.5f, 62.6f, 62.4f};
    float temp[3]  = {41.2f, 41.3f, 41.2f};

    for (int i = 0; i < 3; i++) {
        taos_bind_param(stmt, 1, TAOS_BIND_PARAM_TBNAME, tbname, 0);
        taos_bind_param(stmt, 2, TAOS_BIND_PARAM_TAG, dev, 0);
        taos_bind_param(stmt, 3, TAOS_BIND_PARAM_TAG, line, 0);
        taos_bind_param(stmt, 4, TAOS_BIND_PARAM_TAG, model, 0);
        taos_bind_param(stmt, 5, TAOS_BIND_PARAM_INT64, &ts[i], 0);
        taos_bind_param(stmt, 6, TAOS_BIND_PARAM_FLOAT, &speed[i], 0);
        taos_bind_param(stmt, 7, TAOS_BIND_PARAM_FLOAT, &load[i], 0);
        taos_bind_param(stmt, 8, TAOS_BIND_PARAM_FLOAT, &temp[i], 0);
        taos_stmt_add_batch(stmt);
    }
    int32_t affected = 0;
    taos_stmt_execute(stmt);
    taos_stmt_affected_rows(stmt, &affected);
    printf("batch inserted rows: %d\n", affected);

    // 查询验证
    TAOS_RES *res = taos_query(taos,
        "SELECT device_id, COUNT(*), AVG(spindle_load) FROM cnc_metrics "
        "WHERE ts >= '2026-09-24 08:00:00' AND ts < '2026-09-24 08:30:00' "
        "GROUP BY device_id");
    TAOS_ROW row;
    while ((row = taos_fetch_row(res)) != NULL) {
        printf("device=%s count=%s avg_load=%s\n",
               (char *)row[0], (char *)row[1], (char *)row[2]);
    }
    taos_free_result(res);
    taos_stmt_close(stmt);
    taos_close(taos);
    taos_cleanup();
    return 0;
}

编译(以 Linux/Windows MinGW 为例,链接 libtaos):

bash 复制代码
# Linux
gcc demo.c -o demo -ltaos -I/usr/local/include/taos
# Windows(装好客户端后)
gcc demo.c -o demo.exe -ltaos -I"C:\TDengine\include" -L"C:\TDengine\driver"

注意:taos_stmt_execute 返回 TAOS_SUCESS(0) 即成功;批量行数通过 taos_stmt_affected_rows 获取。v2 旧接口 taos_stmt_bind_param_batch 在新版本仍可用,但推荐 v3 的 taos_bind_param 逐行绑定 + add_batch。

4.5 Python 示例(taospy)

文件:demo.py

python 复制代码
import taospy

conn = taospy.connect(host="localhost", user="root", password="taosdata",
                      database="iot_db", port=6030)
cur = conn.cursor()

# 建库建表
cur.execute("CREATE DATABASE IF NOT EXISTS iot_db KEEP 90")
cur.execute("USE iot_db")
cur.execute("""CREATE STABLE IF NOT EXISTS cnc_metrics (
    ts TIMESTAMP, spindle_speed FLOAT, spindle_load FLOAT, temp FLOAT
) TAGS (device_id VARCHAR(32), line VARCHAR(16), model VARCHAR(32))""")

# 插入
cur.execute("""INSERT INTO cnc_metrics_001 USING cnc_metrics
    TAGS ('CNC-001','L1','Mazak') VALUES
    ('2026-09-24 08:00:00.000', 4200.0, 62.5, 41.2),
    ('2026-09-24 08:00:00.100', 4201.0, 62.6, 41.3),
    ('2026-09-24 08:00:00.200', 4200.5, 62.4, 41.2)""")

# 窗口降采样
cur.execute("""SELECT _wstart, device_id, AVG(spindle_load), MAX(temp)
    FROM cnc_metrics
    WHERE ts >= '2026-09-24 08:00:00' AND ts < '2026-09-24 08:30:00'
    INTERVAL(5m) GROUP BY device_id""")
for row in cur.fetchall():
    print(row)

cur.close()
conn.close()
bash 复制代码
pip install taospy
python demo.py

4.6 贴合场景:CNC/PLC 设备点位历史数据、趋势降采样与告警

以一条"CNC 车间点位采集 → 边缘网关 → TDengine → 趋势/告警"链路为例:

  1. 点位模型设计:一类设备一个超级表,点位指标做列,设备维度做标签。不要"一个点位一张表",否则子表数量爆炸(见常错点 8)。
sql 复制代码
-- PLC 数字量/模拟量统一入一个超级表(按设备子表隔离)
CREATE STABLE IF NOT EXISTS plc_points (
    ts TIMESTAMP,
    temp FLOAT,        -- 温度模拟量
    pressure FLOAT,    -- 压力模拟量
    status BOOL,       -- 运行状态数字量
    alarm_code INT     -- 报警代码
) TAGS (device_id VARCHAR(32), line VARCHAR(16), station VARCHAR(16));
  1. 历史数据查询(设备点位回放):
sql 复制代码
SELECT ts, temp, pressure, status FROM plc_points
WHERE device_id = 'PLC-01' AND ts >= '2026-09-23 00:00:00' AND ts < '2026-09-24 00:00:00'
ORDER BY ts ASC;
  1. 趋势降采样(1s 原始 → 1min 均值,用于大屏趋势图):
sql 复制代码
SELECT _wstart, AVG(temp) AS avg_temp, MAX(temp) AS max_temp
FROM plc_points
WHERE device_id = 'PLC-01'
  AND ts >= '2026-09-24 08:00:00' AND ts < '2026-09-24 09:00:00'
INTERVAL(1m);
  1. 告警规则(最近 5 分钟平均负载超阈值):
sql 复制代码
SELECT device_id, AVG(spindle_load) AS avg_load
FROM cnc_metrics
WHERE ts >= NOW - 5m
INTERVAL(5m) GROUP BY device_id
HAVING avg_load > 90;
  1. 落库后衔接分析:TDengine 在线查询 + taosdump/导出 CSV 到 DuckDB 做深度分析;告警结果回流 Kafka。

5. 常错点 / 坑(20 条)

坑点 说明与规避
1 时间戳精度与时区 建库时 PRECISION 'ms' 决定全库时间戳粒度(ms/us/ns),改精度需重建库;客户端传入字符串带时区(+08:00)时按本地时区解析,无时区字符串按数据库 timezone 配置解析。建议统一用毫秒时间戳 + UTC 存储、展示时转本地,避免跨时区部署混乱。
2 标签与列混淆 标签是静态维度(WHERE 过滤、GROUP BY 分组),列是随时间变化的测量值。把动态指标建为标签会导致标签被频繁修改(性能差且语义错误);把设备维度建成列则每个新设备都要改表结构。原则:变化的做列,不变的做标签。
3 自动建表语法写错 正确形态 INSERT INTO 子表名 USING 超级表 TAGS (tag值...) VALUES (...)。常见错误:忘记 USING、TAGS 与 VALUES 数量/顺序不匹配、先建普通表再 INSERT 同名子表导致结构冲突。
4 重复时间戳被忽略/报错 默认同一子表时间戳必须严格递增(或允许等值,取决于建库参数 TIMESTAMP 精度与 v3 的 ... 配置);重复时间戳写入会被忽略或返回错误。采集端去重/重试逻辑必须处理"同 ts 同值重发",推荐在应用侧用幂等键或忽略重复。
5 逐条 INSERT 性能差 每条 SQL 一条 VALUES 是最大性能杀手。必须批量(多 VALUES / 多子表一次提交)或用 stmt 参数绑定;TDengine 的写入吞吐依赖批量追加,单条写入可能只有几千行/秒,批量可达百万行/秒。
6 表数量膨胀与 tags 设计 每个设备一个子表;若把点位建表(如 table_per_point),设备 × 点位会指数膨胀。正确做法:设备=子表、点位=列;标签基数过高(如把随机值/时间戳做标签)会导致元数据膨胀,标签应是低基数的稳定维度。
7 keep 保留策略误用 KEEP 0 表示永久保留(不推荐);修改 KEEP 只影响新建/后续清理,不立即删除已有超期数据(后台周期性清理)。降采样结果要单独建表并设更长 keep,否则原始数据过期时降采样也一起没了。
8 REST 返回格式解析 REST 返回 {code, desc, column_meta, data},code != 0 即失败;column_meta 与 data 一一对应。常见错误:直接取 data 不查 code;把毫秒时间戳当秒;字符串列在 JSON 里是字符串但数值列是 number。
9 taosRestful vs taosSql 混用 driver-go 中 taosSql 走原生 6030(高性能、支持 stmt),taosRestful 走 REST 6041(跨网络友好但少部分特性受限)。二者驱动名不同,不能同时 _ "..." 注册同一别名;按部署拓扑选其一。
10 NaN/Null 聚合语义 FLOAT/DOUBLE 列允许 NaN;聚合函数(AVG/SUM/COUNT)对 NaN 与 NULL 的计数语义不同(COUNT(*) 计数含 NULL 行、COUNT(col) 只计非 NULL)。查询前先确认是否需要 IS NOT NULL 过滤,避免均值被 NaN 污染。
11 v2/v3 差异 v3 重构了集群架构与存储(vnode/tsdb 演进),语法大体兼容但:v3 中 CREATE DATABASE 参数(如 CACHEMODEL、WAL_RETENTION_PERIOD)、taos_stmt_* 绑定接口、部分系统表(information_schema)均有变化。按 3.x 文档写新代码,迁移旧脚本先跑 taos -s "SELECT server_version()" 确认版本。
12 端口 6030 与 6041 混淆 6030 是原生 taosc 连接端口(C/Go/Python 原生驱动),6041 是 REST/taosAdapter 端口。防火墙只开 6041 时原生驱动连不上;SDK 客户端配置 port=6030 却走 REST 驱动也会失败。
13 Windows 连接问题 Windows 上原生驱动需安装 TDengine Windows 客户端(提供 taos.dll/taosc 库);或直接用 REST 6041(curl/taosRestful/HTTP SDK)免装客户端。另外 Windows 下 taos.exe CLI 默认读 C:\TDengine\cfg\taos.cfg,firstEp 配错会连不上。
14 忘记 USE 库/表名不带库前缀 连接器 DSN 指定了 database 时自动 USE;但 REST 的 /rest/sql/<db> 与 /rest/sql(无库)行为不同,SQL 中未加库前缀会报 "Table does not exist"。建议 SQL 一律写 库名.表名 或显式 USE。
15 INTERVAL 不配 GROUP BY 标签 多设备数据在一个超级表里,INTERVAL 聚合不写 GROUP BY device_id 时全部设备混在一个窗口序列,趋势图完全错乱。窗口聚合必须显式按标签分组。
16 字符串类型 VARCHAR 长度与 NCHAR VARCHAR 按字节计、NCHAR 按字符计;中文字段建议 NCHAR,且注意 VARCHAR(n) 长度不足导致插入截断/报错。标签/列名长度、字符集(utf8)在不同版本有差异,跨版本先查文档。
17 ORDER BY 时序查询性能陷阱 时间有序是引擎优势:按 ts 范围查询无需全表扫描;但 ORDER BY 非时间列、JOIN 子表间关联、无时间过滤的聚合可能触发全扫描。查询务必带时间范围下界。
18 未做写入失败重试与幂等 网络抖动丢一批写入,若直接重放整批可能引入重复时间戳/重复数据;配合采集端缓冲(Kafka/MQTT 消费 offset)实现 at-least-once + 应用侧去重,避免数据空洞或重复。
19 默认账号未改/安全裸奔 默认 root/taosdata、REST 走 Basic 明文;生产必须:改密码、限制 IP、必要时走 HTTPS/内网隔离。
20 备份缺失 TDengine 没有像 MySQL binlog 一样默认全量备份;用 taosdump 定期备份库/表,配合 keep 策略规划恢复窗口。

6. 总结

6.1 适用场景表

场景 是否推荐 TDengine 说明
工业 CNC/PLC 点位历史数据采集与回放 ✅ 强推 设备=子表、点位=列、标签=维度,时间窗口查询天然匹配
物联网传感器遥测(温湿度/电量/GPS) ✅ 强推 高写入吞吐 + 自动建表 + keep 过期
监控指标(服务器 CPU/内存、业务 QPS) ✅ 推荐 对标 Prometheus/InfluxDB,SQL 更通用
交易/账务/强一致关系数据 ❌ 不适用 用 MySQL/PostgreSQL
单机嵌入式小规模存储 ⚠️ 看情况 数据量小且要离线分析可用 SQLite/DuckDB
通用 KV 缓存/状态存储 ❌ 不适用 用 Redis/RocksDB
跨文件/Parquet 深度分析 ⚠️ 分析端 导出后交给 DuckDB 等 OLAP

6.2 工业数采链路建议

复制代码
CNC/PLC 设备
  └─(Modbus RTU/TCP / OPC UA)─→ 边缘采集网关(C/Go 采集器)
       └─(MQTT QoS1 / Kafka)─→ 消息总线
            └─(消费落库)─→ TDengine(iot_db:原始点位 keep 90,降采样 keep 1095)
                 ├─ REST/SQL 在线查询 → 大屏趋势 / 历史回放
                 ├─ INTERVAL 窗口聚合 → 告警规则(HAVING 阈值)
                 └─ taosdump/CSV 导出 → DuckDB 深度分析 / PyTorch+ONNX 预测质检
  • 子表命名规范:cnc_metrics_001(设备唯一),标签必含产线/型号等分组维度。
  • 原始数据与降采样分离建库(不同 keep),避免降采样被原始策略误删。
  • 告警尽量用窗口聚合(5m 均值)而非单点阈值,减少抖动误报。

6.3 FAQ 速查表

问题 答案
TDengine 是什么? 涛思数据开源的时序数据库,核心 C 语言实现,SQL 友好,面向 IoT/工业海量时序数据。
端口 6030 / 6041 分别是什么? 6030 原生连接(C/Go/Python 原生驱动);6041 REST(taosAdapter)。
超级表和子表什么关系? 超级表是一类设备的模板(列+标签 schema),子表是具体设备实例(继承结构+绑定标签值),数据按子表时间有序存储。
标签和列的区别? 标签是静态维度(过滤/分组),列是动态测量值;变化的做列、不变的做标签。
如何自动建表? INSERT INTO 子表 USING 超级表 TAGS (...) VALUES (...),表不存在自动创建。
怎么做降采样? SELECT ... INTERVAL(5m) SLIDING(1m) GROUP BY device_id,配合 _wstart/_wend 伪列。
如何设置数据保留时间? 建库 CREATE DATABASE ... KEEP 90(天);ALTER DATABASE ... KEEP 180 可改。
Go 怎么连? sql.Open("taosSql", "root:taosdata@tcp(localhost:6030)/db");REST 用 taosRestful 驱动 6041。
C 怎么批量写? stmt 参数绑定:taos_stmt_prepare + taos_bind_param + taos_stmt_add_batch + taos_stmt_execute。
Python 怎么连? pip install taospy,taospy.connect(host, user, password, database, port=6030)。
时间戳精度怎么选? 建库 PRECISION 'ms';全库统一,改精度需重建库;跨时区建议 UTC + 毫秒。
写入慢怎么排查? 看是否逐条 INSERT;改用批量 VALUES/stmt;检查网络往返、WAL 参数、是否触发了重复时间戳丢弃。
表太多怎么办? 避免点位建表;设备=子表、点位=列;标签保持低基数。
与 InfluxDB 比有什么优势? SQL 标准、标签/子表模型直观、国产开源社区活跃;InfluxDB 生态更老牌,选型看团队栈。
支持 JSON 数据吗? v3 支持 JSON 类型列,可存半结构化点位附加信息。
Windows 上怎么用? 装 Windows 客户端后用原生驱动;或直接 REST 6041 免客户端。
需要备份吗? 需要,用 taosdump 定期导出;keep 只管过期删除不管容灾。

本文代码基于 TDengine 3.x(3.3.x)语法;如使用 2.x 请注意 taos_stmt_*、建库参数与系统表差异(见常错点 11)。文中示例均可在本地 Docker 部署后直接运行验证。 (内容由AI生成,仅供参考)

相关推荐
颜颜yan_1 小时前
从 InfluxDB 到 DolphinDB:一场国产时序数据库的替代与超越
数据库·时序数据库
分布式存储与RustFS2 小时前
GitLab 对接 RustFS 实战:OIDC 控制台 SSO + 制品存储落 S3 对象存储
运维·云原生·开源·对象存储·分布式存储·s3
SL-staff2 小时前
JVS私有化交付技术解析:如何通过全栈开源与引擎解耦实现真正可控的源码级交付
开源·私有化部署·springboot·信创适配·jvs·spi架构
miofly3 小时前
GitHub 日榜趋势速报 | 2026-09-29
开源·github
resh_people3 小时前
开源鸿蒙平台 KMP/CMP 三方库「kotlinx-datetime」适配全流程
华为·开源·harmonyos
芯祥联3 小时前
STM32 上跑 MQTT 怎么选?嵌入式 MQTT 客户端 C 语言实现对比
c语言·stm32·单片机·嵌入式硬件·网络协议·开源·broker
Java后端的Ai之路11 小时前
SSE 接口设计 vs Agent UI:四个开源项目,把「模型吐词」和「界面更新」拆开后,我看懂了差距
ui·开源·sse·agui·agentui
千桐科技11 小时前
数据中台 ETL 编排实践:基于 qData 开源版实现数据接入、转换与调度执行
数据仓库·开源·etl
也非非也12 小时前
DeepSeek没发公告,但它把Agent装进了你的电脑
人工智能·开源·agent·deepseek·dsh