2.数据模型 > 02 超级表/子表/普通表

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-05-16
概述
TDengine 的数据模型围绕一个核心设计理念------一个数据采集点一张表(One Table Per Data Collection Point)。这与传统关系型数据库"所有设备共享一张宽表"的思路截然不同。
为了在"一设备一表"的基础上实现高效的聚合查询和统一管理,TDengine 创造了**超级表(STable)**机制------同类设备共享 Schema,通过 Tag 区分个体。
本文深入解析三种表类型的设计理念、内部存储结构和适用场景。
核心概念速查表
| 概念 | 说明 |
|---|---|
| 超级表(Super Table) | 定义一类设备的数据模板(Schema + Tag 定义),不直接存储数据 |
| 子表(Child Table) | 从超级表派生,代表一个具体的数据采集点,存储实际时序数据 |
| 普通表(Normal Table) | 不从属于超级表的独立表,无 Tag 概念 |
| 列(Column) | 时序数据的度量值(measurement),随时间变化 |
| 标签(Tag) | 设备的静态属性(如位置、型号),一张子表的 Tag 值固定不变 |
| 时间戳列 | 每张表的第一列必须是时间戳(主键),数据按时间有序存储 |
| Schema | 表的结构定义(列名、列类型、列宽度) |
详细解析
1. 设计理念:一设备一表
1.1 为什么不用"大宽表"?
传统方案------将所有设备数据写入同一张表(通过设备 ID 列区分):
传统方案(关系型思维):
CREATE TABLE sensor_data (
ts TIMESTAMP,
device_id VARCHAR(64),
temperature FLOAT,
humidity FLOAT,
...
);
问题:
① 单表亿级行 → 索引巨大,写入/查询都慢
② 不同设备的写入相互竞争锁
③ 单设备查询需要全表扫描 + device_id 过滤
④ 时间戳不唯一(多设备同一时刻都有数据)
1.2 TDengine 的方案
TDengine 方案(一设备一表):
每个设备一张独立的子表:
d001: ts | temperature | humidity
d002: ts | temperature | humidity
d003: ts | temperature | humidity
...
优势:
① 每张表内时间戳天然有序 → 追加写入即可,无需排序
② 单设备查询 = 顺序扫描单表 → O(1) 定位 + 顺序读取
③ 各表写入互不干扰 → 高并发写入
④ 压缩率高 → 同设备数据相似度高,列式压缩效果好
⑤ 时间戳在每张表内唯一 → 天然主键
1.3 超级表解决"多设备聚合"问题
一设备一表的挑战:如何做跨设备的聚合查询?
sql
-- 问题:有 100 万张子表,如何查"某区域所有设备的平均温度"?
-- 如果没有超级表,需要 UNION ALL 100 万张表------不现实
-- 超级表的解决方案:
SELECT AVG(temperature) FROM meters WHERE location = 'Beijing';
-- 系统自动:
-- 1. 通过 Tag 索引找到 location='Beijing' 的所有子表
-- 2. 并行扫描这些子表
-- 3. 聚合结果返回
2. 超级表(Super Table)
2.1 超级表的定义
超级表是同类数据采集点的模板,定义了两部分结构:
sql
CREATE STABLE meters (
-- 列(Column):时序数据,随时间变化
ts TIMESTAMP,
current FLOAT,
voltage INT,
phase FLOAT
) TAGS (
-- 标签(Tag):设备属性,一张子表内固定不变
location BINARY(64),
group_id INT
);
2.2 超级表不存储数据
超级表本身不存储任何时序数据行。它的作用是:
| 功能 | 说明 |
|---|---|
| Schema 模板 | 子表继承超级表的列定义 |
| Tag 定义 | 定义子表可以携带的静态属性 |
| 跨子表查询入口 | SELECT ... FROM 超级表名 可聚合查询所有子表 |
| Tag 索引 | 维护所有子表的 Tag 值索引,加速 WHERE tag 过滤 |
2.3 超级表的内部表示
超级表在系统中的存储:
MNode(元数据节点)中存储:
├── 超级表基本信息:名称、UID、创建时间、所属数据库
├── 列 Schema:列名、类型、宽度、编号
├── Tag Schema:Tag 名、类型、宽度、编号
└── 版本号:Schema 变更时递增
VNode 中存储:
├── 超级表的列 Schema 副本(用于本地数据校验)
└── Tag 索引(本 VNode 管理的所有子表的 Tag 值)
2.4 超级表数量限制
- 如果数据库设置了
SINGLE_STABLE = 1,则只允许 1 张超级表 - 默认不限制超级表数量,但建议同类设备共用一张超级表
- 超级表数量过多会增加 MNode 的元数据管理压力
3. 子表(Child Table)
3.1 创建子表
sql
-- 方式 1:显式创建
CREATE TABLE d1001 USING meters TAGS ('California.SanFrancisco', 2);
-- 方式 2:写入时自动创建(推荐)
INSERT INTO d1001 USING meters TAGS ('California.SanFrancisco', 2)
VALUES (now, 10.3, 219, 0.31);
-- 方式 3:批量创建
CREATE TABLE
d1001 USING meters TAGS ('California.SanFrancisco', 2)
d1002 USING meters TAGS ('California.LosAngeles', 3)
d1003 USING meters TAGS ('Beijing.Haidian', 1);
3.2 子表与超级表的关系
超级表与子表的关系:
超级表 meters (Schema 模板)
│
├── 子表 d1001 (tags: location='California', group_id=2)
│ └── 时序数据: ts, current, voltage, phase
│
├── 子表 d1002 (tags: location='LosAngeles', group_id=3)
│ └── 时序数据: ts, current, voltage, phase
│
└── 子表 d1003 (tags: location='Beijing', group_id=1)
└── 时序数据: ts, current, voltage, phase
关键规则:
① 子表继承超级表的列 Schema(不能单独增删列)
② 子表有自己独立的 Tag 值(可以修改)
③ 子表存储实际的时序数据
④ 超级表 Schema 变更自动影响所有子表
3.3 子表的内部存储结构
子表在 VNode 内的存储:
┌────────────────────────────────┐
│ VNode (VGroup N) │
│ │
│ 元数据引擎(TDB): │
│ ├── 子表 UID → 子表信息 │
│ │ - 所属超级表 UID │
│ │ - Tag 值 │
│ │ - 创建时间 │
│ └── 超级表 UID → [子表列表] │
│ │
│ 时序存储引擎(TSDB): │
│ ├── MemTable(内存): │
│ │ 子表 UID → 跳表数据 │
│ └── 数据文件(磁盘): │
│ 按时间分组 → │
│ [文件头 | 数据块...] │
│ 数据块内按子表 UID 组织 │
└────────────────────────────────┘
3.4 子表的 VGroup 归属
每张子表属于且只属于一个 VGroup,由表名的 hash 值决定:
子表路由算法:
hash_value = MurmurHash3(表名)
vgroup_index = hash_value 落入哪个 VGroup 的 [hashBegin, hashEnd] 范围
注意:
- hash 计算使用内部全名(含数据库前缀)
- TABLE_PREFIX / TABLE_SUFFIX 参数影响参与 hash 的字符
- 一旦建表,VGroup 归属不变(除非显式迁移 VGroup)
3.5 子表数量
- 理论上限:无硬性上限,受内存和磁盘约束
- 实践参考:单集群管理上亿张子表是常见场景
- 每个 VNode 管理的子表数 ≈ 总子表数 / VGROUPS
4. 普通表(Normal Table)
4.1 定义与创建
sql
-- 普通表:不从属于任何超级表,无 Tag
CREATE TABLE cpu_summary (
ts TIMESTAMP,
cpu_avg FLOAT,
mem_used BIGINT,
disk_io FLOAT
);
4.2 普通表 vs 子表
| 对比维度 | 子表 | 普通表 |
|---|---|---|
| 从属关系 | 必须属于超级表 | 独立存在 |
| Tag | 有,继承自超级表定义 | 无 |
| Schema 管理 | 跟随超级表变更 | 独立管理 |
| 跨表聚合 | 通过超级表查询 | 需要显式 JOIN 或 UNION |
| 适用场景 | 大量同构设备 | 少量特殊用途表 |
4.3 普通表的适用场景
- 全局汇总表(存储系统级统计信息)
- 配置表(存储应用参数)
- 临时分析结果表
- 不同于主业务设备的异构数据
最佳实践 :如果有多个同类设备,始终使用超级表 + 子表,而非大量独立的普通表。超级表提供的统一 Schema 管理和跨表查询能力是普通表无法替代的。
5. 表的内部标识
5.1 UID 体系
表标识体系:
超级表:
- suid(Super Table UID):全局唯一 64 位整数
- 存储在 MNode(SDB)和所有相关 VNode
子表:
- uid(Table UID):全局唯一 64 位整数
- suid:指向所属超级表
- 存储在所属 VNode
普通表:
- uid(Table UID):全局唯一 64 位整数
- suid = 0(标识为普通表)
- 存储在所属 VNode
UID 生成:基于时间戳 + 随机数,保证全局唯一
5.2 名称与 UID 的映射
名称解析路径:
用户输入表名 "d1001"
│
▼
客户端 Catalog 缓存查找:表名 → (uid, suid, vgId, schema)
│
├── 命中 → 直接使用
└── 未命中 → 向 MNode 请求 → 缓存 → 使用
VNode 内部:通过 uid 进行所有数据操作(不再使用表名字符串)
6. 数据组织方式对比
三种表的数据组织:
┌─────────────────────────────────────────────────┐
│ Database "power" │
│ │
│ 超级表 meters: │
│ Schema: [ts, current, voltage, phase] │
│ Tags: [location, group_id] │
│ │ │
│ ├── VGroup 2 │
│ │ ├── 子表 d1001 (tags: 'CA', 2) │
│ │ │ └── [时序数据...] │
│ │ ├── 子表 d1002 (tags: 'CA', 3) │
│ │ │ └── [时序数据...] │
│ │ └── 子表 d1005 (tags: 'NY', 1) │
│ │ └── [时序数据...] │
│ │ │
│ └── VGroup 3 │
│ ├── 子表 d1003 (tags: 'BJ', 1) │
│ │ └── [时序数据...] │
│ └── 子表 d1004 (tags: 'SH', 2) │
│ └── [时序数据...] │
│ │
│ 普通表 summary: │
│ Schema: [ts, total_power, peak_current] │
│ (存储在某个 VGroup 中) │
└─────────────────────────────────────────────────┘
7. 查询行为差异
| 查询方式 | 行为 |
|---|---|
SELECT * FROM d1001 |
直接定位子表所在 VGroup,扫描单表 |
SELECT * FROM meters |
扫描所有 VGroup 的所有子表(全量) |
SELECT * FROM meters WHERE location='CA' |
通过 Tag 索引过滤,只扫描满足条件的子表 |
SELECT AVG(current) FROM meters GROUP BY location |
Tag 过滤 + 分组聚合 |
SELECT * FROM meters PARTITION BY tbname |
按子表分组输出 |
超级表查询执行流程:
SELECT AVG(current) FROM meters WHERE location = 'CA'
│
▼
① Tag 过滤:从各 VNode 的 Tag 索引中筛选 location='CA' 的子表
│
▼
② 生成子表列表:[d1001, d1002, d1005]
│
▼
③ 按 VGroup 分组下发:
VGroup 2: 扫描 d1001, d1002, d1005 → 本地部分聚合
VGroup 3: (该 VGroup 无符合条件的表,跳过)
│
▼
④ 汇总各 VGroup 的部分结果 → 最终 AVG
代码示例
完整的建模示例
sql
-- 1. 创建数据库
CREATE DATABASE power VGROUPS 4 REPLICA 3 PRECISION 'ms' KEEP 365d;
USE power;
-- 2. 创建超级表(电表模板)
CREATE STABLE meters (
ts TIMESTAMP,
current FLOAT,
voltage INT,
phase FLOAT
) TAGS (
location BINARY(64),
group_id INT
);
-- 3. 创建子表(方式一:显式创建)
CREATE TABLE d1001 USING meters TAGS ('California.SanFrancisco', 2);
-- 4. 写入时自动创建子表(方式二:推荐)
INSERT INTO d1002 USING meters TAGS ('California.LosAngeles', 3)
VALUES ('2024-01-15 10:00:00', 10.3, 219, 0.31);
-- 5. 查询单个子表
SELECT * FROM d1001 WHERE ts >= NOW() - 1h;
-- 6. 通过超级表聚合查询(Tag 过滤)
SELECT AVG(current), MAX(voltage)
FROM meters
WHERE location LIKE 'California%'
INTERVAL(10m);
-- 7. 按 Tag 分组统计
SELECT location, COUNT(*), AVG(current)
FROM meters
GROUP BY location;
-- 8. 修改子表 Tag 值
ALTER TABLE d1001 SET TAG location = 'California.Oakland';
查看表结构信息
sql
-- 查看超级表列表
SHOW power.STABLES;
-- 查看超级表结构
DESCRIBE meters;
-- 查看子表数量
SELECT COUNT(*) FROM information_schema.ins_tables
WHERE db_name = 'power' AND stable_name = 'meters';
-- 查看子表的 Tag 值
SELECT tbname, location, group_id FROM meters;
性能考量
子表数量对性能的影响
| 子表数量级 | 写入影响 | 查询影响 | 建议 |
|---|---|---|---|
| < 1万 | 无影响 | 无影响 | VGROUPS 2~4 |
| 1万~100万 | 无影响 | 全超级表扫描较慢 | VGROUPS 4~16,善用 Tag 过滤 |
| 100万~1亿 | 元数据内存增大 | 必须用 Tag 过滤 | VGROUPS 16~64,注意内存规划 |
Tag 过滤的性能
- Tag 索引存储在 VNode 内存中,Tag 过滤不走磁盘 I/O
- 过滤后只扫描匹配的子表数据,大幅减少不必要的磁盘读取
- 建议将高区分度的属性设为 Tag(如设备 ID、区域编码)
写入最佳实践
| 实践 | 原因 |
|---|---|
| 使用自动建表语法 | 一次 RPC 完成建表 + 写入,减少网络往返 |
| 批量写入多表数据 | 客户端自动按 VGroup 分组并行发送 |
| 避免频繁修改 Tag | Tag 修改会更新索引,高频修改影响性能 |
FAQ
Q1: 超级表和子表在磁盘上是什么关系?
超级表不占用数据磁盘空间(只有 MNode 中的元数据记录)。所有实际数据存储在子表中。同一 VGroup 内的多张子表数据会被合并存储到同一组数据文件中(按时间范围组织),不是每张子表一个文件。
Q2: 一个子表可以属于多个超级表吗?
不可以。子表与超级表是严格的一对多关系:一张超级表可以有无数张子表,但一张子表只能属于一张超级表。
Q3: 删除超级表会发生什么?
删除超级表会级联删除该超级表下的所有子表及其数据。这是一个不可逆操作,需要谨慎。
Q4: 普通表可以转成子表吗?
不可以。普通表和子表在创建时就确定了类型,不能互相转换。如果需要将普通表纳入超级表管理,需要重新建表并迁移数据。
Q5: 为什么不直接查子表而要通过超级表查?
通过超级表查询的优势:
- 无需知道具体子表名称(通过 Tag 条件筛选)
- 自动并行扫描多个 VGroup
- 内置分组聚合支持(
GROUP BY tag_column) - 当子表数量增长时,查询 SQL 不需要修改
如果只需要查询单个已知设备,直接查子表反而更快(跳过 Tag 过滤步骤)。
Q6: 子表的 Tag 值修改后,历史数据的查询结果会变吗?
会变。Tag 是子表的当前属性,不是时间序列数据。修改 Tag 后,所有查询(包括历史数据)都使用新的 Tag 值进行过滤。如果需要记录 Tag 值的变化历史,应将其作为列(Column)而非标签。
参考
系统构架篇
- 01-《TDengine 整体架构全景》
- 02-《集群拓扑深度解析》
- 03-《MNode 内部机制深度解析》
- 04-《RPC 通信层深度解析》
- 05-《VNode 生命周期》
- 06-《RAFT 共识协议》
- 07-《端到端的消息流》
数据模型
- 01-《数据库创建与参数详解》
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。