TDengine 时序数据库实战笔记(20260622)

文章目录

    • [1. 为什么需要时序数据库?(与传统数据库对比)](#1. 为什么需要时序数据库?(与传统数据库对比))
    • [2. 典型应用场景](#2. 典型应用场景)
    • [3. 为什么选 TDengine?](#3. 为什么选 TDengine?)
    • [4. 核心概念速览](#4. 核心概念速览)
    • [5. 逻辑表与物理表:MySQL vs TDengine](#5. 逻辑表与物理表:MySQL vs TDengine)
      • [5.1 MySQL:逻辑表与物理表](#5.1 MySQL:逻辑表与物理表)
      • [5.2 TDengine:逻辑表与物理表(含时间分区与 vnode 分片)](#5.2 TDengine:逻辑表与物理表(含时间分区与 vnode 分片))
    • [6. 核心操作速查](#6. 核心操作速查)
      • [6.1 建库(分区 + 保留 + 冷热分层)](#6.1 建库(分区 + 保留 + 冷热分层))
      • [6.2 建超级表(带缺省值、分片)](#6.2 建超级表(带缺省值、分片))
      • [6.3 动态写入(伪代码)](#6.3 动态写入(伪代码))
      • [6.4 创建预聚合(TSMA)](#6.4 创建预聚合(TSMA))
    • [7. 性能基线](#7. 性能基线)
      • [时间裁剪示例(假设数据库 `duration 10d`)](#时间裁剪示例(假设数据库 duration 10d))
    • [8. 生产部署架构](#8. 生产部署架构)
    • [9. 版本与资源](#9. 版本与资源)

1. 为什么需要时序数据库?(与传统数据库对比)

对比维度 传统关系型数据库 (如 MySQL) 时序数据库 (以 TDengine 为例)
数据本质 离散事务,增删改查均衡 按时间顺序产生的结构化数据流,写多读少
时间处理 时间只是一个普通字段 时间是"第一公民",自动分区、索引、裁剪
写入能力 单机每秒几千~几万行 单机每秒可达数百万行
查询模式 多表关联、单条查询 按时间范围聚合、降采样、趋势分析
存储压缩 通用压缩,效率一般 时序专用压缩,压缩比可达 10~30 倍
数据保留 需手动清理历史数据 内置自动过期 (KEEP),按分区直接删除文件

核心判断标准 :如果业务是海量设备持续产生带时间戳的数据,且主要查询是看趋势或做统计,就应该选时序数据库。


2. 典型应用场景

  • 车联网:每辆车每秒上报 GPS、车速、电池状态等。
  • 工业物联网 (IIoT):工厂 PLC、传感器高频采集温度、压力、振动。
  • IT 监控:服务器 CPU、内存、网络流量等指标。
  • 金融科技:交易行情、风险指标计算。

3. 为什么选 TDengine?

  • 独特的"一设备一表"模型:单设备数据物理连续存储,顺序追加(无锁),查询极快。
  • 极简运维:内置时间分区、自动分片、冷热分层、预聚合,无需手动分库分表。
  • SQL 兼容:学习成本低,团队上手快。
  • 单机高性能:合理配置分片,4 核服务器也能支撑海量设备。
  • 开源版功能完整:分片、TSMA 预聚合、冷热分层、MQTT 订阅等全部免费。

4. 核心概念速览

概念 说明
超级表 (STable) 定义数据列结构、标签(TAGS)的模板,不存实际数据
子表 (Subtable) 每个设备对应一张物理子表,用于存放该设备的所有数据
标签 (TAGS) 设备静态属性(如位置、型号),自动建索引,用于分组过滤
时间分区 (duration) 数据库级设置,按天/周/月自动切分物理数据文件
分片 (vgroups) 超级表级设置,将子表打散到多个 vnode 并行处理,单机建议设为 CPU 核数
冷热分层 (tiered_storage) 老数据自动从 SSD 迁移到 S3 对象存储
预聚合 (TSMA) 提前计算每小时/每天的统计值,查询聚合结果时毫秒级返回
MQTT 订阅 TDengine 可作为 MQTT Broker,反向推送数据给外部系统

5. 逻辑表与物理表:MySQL vs TDengine

5.1 MySQL:逻辑表与物理表

#mermaid-svg-FAyRN4OGJt3X0hiB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FAyRN4OGJt3X0hiB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FAyRN4OGJt3X0hiB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FAyRN4OGJt3X0hiB .error-icon{fill:#552222;}#mermaid-svg-FAyRN4OGJt3X0hiB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FAyRN4OGJt3X0hiB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FAyRN4OGJt3X0hiB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FAyRN4OGJt3X0hiB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FAyRN4OGJt3X0hiB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FAyRN4OGJt3X0hiB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FAyRN4OGJt3X0hiB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FAyRN4OGJt3X0hiB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FAyRN4OGJt3X0hiB .marker.cross{stroke:#333333;}#mermaid-svg-FAyRN4OGJt3X0hiB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FAyRN4OGJt3X0hiB p{margin:0;}#mermaid-svg-FAyRN4OGJt3X0hiB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FAyRN4OGJt3X0hiB .cluster-label text{fill:#333;}#mermaid-svg-FAyRN4OGJt3X0hiB .cluster-label span{color:#333;}#mermaid-svg-FAyRN4OGJt3X0hiB .cluster-label span p{background-color:transparent;}#mermaid-svg-FAyRN4OGJt3X0hiB .label text,#mermaid-svg-FAyRN4OGJt3X0hiB span{fill:#333;color:#333;}#mermaid-svg-FAyRN4OGJt3X0hiB .node rect,#mermaid-svg-FAyRN4OGJt3X0hiB .node circle,#mermaid-svg-FAyRN4OGJt3X0hiB .node ellipse,#mermaid-svg-FAyRN4OGJt3X0hiB .node polygon,#mermaid-svg-FAyRN4OGJt3X0hiB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FAyRN4OGJt3X0hiB .rough-node .label text,#mermaid-svg-FAyRN4OGJt3X0hiB .node .label text,#mermaid-svg-FAyRN4OGJt3X0hiB .image-shape .label,#mermaid-svg-FAyRN4OGJt3X0hiB .icon-shape .label{text-anchor:middle;}#mermaid-svg-FAyRN4OGJt3X0hiB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FAyRN4OGJt3X0hiB .rough-node .label,#mermaid-svg-FAyRN4OGJt3X0hiB .node .label,#mermaid-svg-FAyRN4OGJt3X0hiB .image-shape .label,#mermaid-svg-FAyRN4OGJt3X0hiB .icon-shape .label{text-align:center;}#mermaid-svg-FAyRN4OGJt3X0hiB .node.clickable{cursor:pointer;}#mermaid-svg-FAyRN4OGJt3X0hiB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FAyRN4OGJt3X0hiB .arrowheadPath{fill:#333333;}#mermaid-svg-FAyRN4OGJt3X0hiB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FAyRN4OGJt3X0hiB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FAyRN4OGJt3X0hiB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FAyRN4OGJt3X0hiB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FAyRN4OGJt3X0hiB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FAyRN4OGJt3X0hiB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FAyRN4OGJt3X0hiB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FAyRN4OGJt3X0hiB .cluster text{fill:#333;}#mermaid-svg-FAyRN4OGJt3X0hiB .cluster span{color:#333;}#mermaid-svg-FAyRN4OGJt3X0hiB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FAyRN4OGJt3X0hiB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FAyRN4OGJt3X0hiB rect.text{fill:none;stroke-width:0;}#mermaid-svg-FAyRN4OGJt3X0hiB .icon-shape,#mermaid-svg-FAyRN4OGJt3X0hiB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FAyRN4OGJt3X0hiB .icon-shape p,#mermaid-svg-FAyRN4OGJt3X0hiB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FAyRN4OGJt3X0hiB .icon-shape .label rect,#mermaid-svg-FAyRN4OGJt3X0hiB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FAyRN4OGJt3X0hiB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FAyRN4OGJt3X0hiB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FAyRN4OGJt3X0hiB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MySQL
1:1
逻辑表:sensor_data

(一张大表,包含所有设备)
物理文件:sensor_data.ibd

(所有设备数据混合存储)

实际例子

sql 复制代码
-- 创建一张表,所有设备共用
CREATE TABLE sensor_data (
    id INT AUTO_INCREMENT PRIMARY KEY,
    device_id VARCHAR(20),
    ts TIMESTAMP,
    current FLOAT
);

-- 设备A、B、C的数据全部混插在一起
INSERT INTO sensor_data (device_id, ts, current) VALUES
('A', NOW(), 10.5),
('B', NOW(), 12.1),
('A', NOW(), 10.7),
('C', NOW(), 9.8);

-- 查设备A的数据,必须全表过滤 device_id
SELECT * FROM sensor_data WHERE device_id = 'A' AND ts > NOW() - INTERVAL 7 DAY;

5.2 TDengine:逻辑表与物理表(含时间分区与 vnode 分片)

#mermaid-svg-SuzLSaLDzVQAZXCG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SuzLSaLDzVQAZXCG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SuzLSaLDzVQAZXCG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SuzLSaLDzVQAZXCG .error-icon{fill:#552222;}#mermaid-svg-SuzLSaLDzVQAZXCG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SuzLSaLDzVQAZXCG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SuzLSaLDzVQAZXCG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SuzLSaLDzVQAZXCG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SuzLSaLDzVQAZXCG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SuzLSaLDzVQAZXCG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SuzLSaLDzVQAZXCG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SuzLSaLDzVQAZXCG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SuzLSaLDzVQAZXCG .marker.cross{stroke:#333333;}#mermaid-svg-SuzLSaLDzVQAZXCG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SuzLSaLDzVQAZXCG p{margin:0;}#mermaid-svg-SuzLSaLDzVQAZXCG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SuzLSaLDzVQAZXCG .cluster-label text{fill:#333;}#mermaid-svg-SuzLSaLDzVQAZXCG .cluster-label span{color:#333;}#mermaid-svg-SuzLSaLDzVQAZXCG .cluster-label span p{background-color:transparent;}#mermaid-svg-SuzLSaLDzVQAZXCG .label text,#mermaid-svg-SuzLSaLDzVQAZXCG span{fill:#333;color:#333;}#mermaid-svg-SuzLSaLDzVQAZXCG .node rect,#mermaid-svg-SuzLSaLDzVQAZXCG .node circle,#mermaid-svg-SuzLSaLDzVQAZXCG .node ellipse,#mermaid-svg-SuzLSaLDzVQAZXCG .node polygon,#mermaid-svg-SuzLSaLDzVQAZXCG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SuzLSaLDzVQAZXCG .rough-node .label text,#mermaid-svg-SuzLSaLDzVQAZXCG .node .label text,#mermaid-svg-SuzLSaLDzVQAZXCG .image-shape .label,#mermaid-svg-SuzLSaLDzVQAZXCG .icon-shape .label{text-anchor:middle;}#mermaid-svg-SuzLSaLDzVQAZXCG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SuzLSaLDzVQAZXCG .rough-node .label,#mermaid-svg-SuzLSaLDzVQAZXCG .node .label,#mermaid-svg-SuzLSaLDzVQAZXCG .image-shape .label,#mermaid-svg-SuzLSaLDzVQAZXCG .icon-shape .label{text-align:center;}#mermaid-svg-SuzLSaLDzVQAZXCG .node.clickable{cursor:pointer;}#mermaid-svg-SuzLSaLDzVQAZXCG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SuzLSaLDzVQAZXCG .arrowheadPath{fill:#333333;}#mermaid-svg-SuzLSaLDzVQAZXCG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SuzLSaLDzVQAZXCG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SuzLSaLDzVQAZXCG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SuzLSaLDzVQAZXCG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SuzLSaLDzVQAZXCG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SuzLSaLDzVQAZXCG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SuzLSaLDzVQAZXCG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SuzLSaLDzVQAZXCG .cluster text{fill:#333;}#mermaid-svg-SuzLSaLDzVQAZXCG .cluster span{color:#333;}#mermaid-svg-SuzLSaLDzVQAZXCG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SuzLSaLDzVQAZXCG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SuzLSaLDzVQAZXCG rect.text{fill:none;stroke-width:0;}#mermaid-svg-SuzLSaLDzVQAZXCG .icon-shape,#mermaid-svg-SuzLSaLDzVQAZXCG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SuzLSaLDzVQAZXCG .icon-shape p,#mermaid-svg-SuzLSaLDzVQAZXCG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SuzLSaLDzVQAZXCG .icon-shape .label rect,#mermaid-svg-SuzLSaLDzVQAZXCG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SuzLSaLDzVQAZXCG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SuzLSaLDzVQAZXCG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SuzLSaLDzVQAZXCG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 物理层自动管理
逻辑层
VNode_2
VNode_1
USING 动态生成
USING 动态生成
数据按时间落入
落入
散列到不同 VNode
落入
超级表 meters

模板,定义列与标签
子表 d_A
子表 d_B
分区: 6.1-6.10
分区: 6.11-6.20
分区: 6.1-6.10
分区: 6.11-6.20

实际例子

sql 复制代码
-- 1. 建库:每10天一个分区
CREATE DATABASE iot_db duration 10d;

-- 2. 建超级表:分4个vnode(单机4核)
CREATE STABLE meters (
    ts TIMESTAMP,
    current FLOAT
) TAGS (location INT) vgroups 4;

-- 3. 写入时自动生成子表
INSERT INTO d_A USING meters TAGS (1) VALUES ('2026-06-01 08:00:00', 10.5);  -- 落入某vnode的6.1-6.10分区
INSERT INTO d_A USING meters TAGS (1) VALUES ('2026-06-12 08:00:00', 11.2);  -- 落入6.11-6.20分区
INSERT INTO d_B USING meters TAGS (2) VALUES ('2026-06-01 08:00:00', 12.1);  -- 散列到另一vnode

-- 4. 查询设备A最近7天数据:只扫描d_A相关分区,无关数据自动跳过
SELECT * FROM d_A WHERE ts >= NOW - 7d;

核心区别

  • MySQL 逻辑与物理 1:1 映射,所有设备数据混杂。
  • TDengine 逻辑上一设备一子表,物理上数据被时间分区 切分文件、vnode 分片并行处理,单设备查询无需扫描其他设备数据,性能极高。

6. 核心操作速查

6.1 建库(分区 + 保留 + 冷热分层)

sql 复制代码
-- 定义 S3 磁盘(可选)
CREATE DISK s3_disk TYPE 'S3'
  s3_endpoint 'https://s3.example.com'
  s3_bucket 'mybucket'
  s3_access_key 'your_access_key'
  s3_secret_key 'your_secret_key';

-- 建库:10天一分区,保留365天,启用冷热分层
CREATE DATABASE iot_db duration 10d keep 365d tiered_storage 's3_disk';

6.2 建超级表(带缺省值、分片)

sql 复制代码
CREATE STABLE meters (
  ts TIMESTAMP,
  current FLOAT,
  voltage FLOAT DEFAULT 220   -- 不上报则自动填220
) TAGS (
  location INT,
  device_type BINARY(32)
) vgroups 4;   -- 4核单机设4

6.3 动态写入(伪代码)

python 复制代码
device_id = msg["deviceId"]
loc       = msg["locationId"]
dev_type  = msg["deviceType"]
current   = msg.get("current")
voltage   = msg.get("voltage")

columns = ["ts"]
values  = ["now"]
if current is not None:
    columns.append("current"); values.append(str(current))
if voltage is not None:
    columns.append("voltage"); values.append(str(voltage))

sql = f"INSERT INTO {device_id} USING meters TAGS ({loc}, '{dev_type}') ({', '.join(columns)}) VALUES ({', '.join(values)})"

6.4 创建预聚合(TSMA)

sql 复制代码
-- 每小时自动计算平均电流和最大电流
CREATE TSMA tsma_hourly ON meters METRICS (avg(current), max(current)) INTERVAL(1h);

-- 查询过去7天每小时平均电流,自动命中TSMA,毫秒级返回
SELECT _wstart, avg(current) FROM meters WHERE ts >= NOW - 7d INTERVAL(1h);

7. 性能基线

  • 单表容量 :无硬上限,推荐控制在千万行 级。过大时配合时间裁剪预聚合(TSMA) 解决。
  • 单行最大长度:48KB(TDengine 3.x)。
  • 慢查询优化 :务必带时间条件 WHERE ts >= ... 开启时间裁剪,配合 TSMA 避免全表扫描。

时间裁剪示例(假设数据库 duration 10d

不裁剪(全表扫描)

sql 复制代码
-- 无时间条件,扫描所有历史分区,几亿行数据全过一遍
SELECT avg(current) FROM d_A;

裁剪(只扫描必要分区)

sql 复制代码
-- 加上时间条件,只扫描最近7天对应的分区(如6.11~6.20和6.21~6.30两个分区)
SELECT avg(current) FROM d_A WHERE ts >= NOW - 7d;

效果:扫描量从几亿行骤降到几百万行,速度提升几十倍。

时间裁剪 + TSMA 联合优化

sql 复制代码
-- 查询过去30天每小时的均值
SELECT _wstart, avg(current) FROM d_A
WHERE ts >= NOW - 30d
INTERVAL(1h);
  • 时间裁剪:只扫30天内的3个分区。
  • TSMA 命中:直接读取预计算好的每小时汇总表,不碰原始数据。
  • 结果 :从几亿行原始数据 → 几千行汇总结果,毫秒级返回

8. 生产部署架构

复制代码
设备 → MQTT Broker → Kafka(削峰) → 你的程序 → TDengine(SSD热 + S3冷)
                                            ↕ (可选) MQTT订阅 → 大屏/告警

9. 版本与资源