TDengine 数据建模实战:从数据库、超级表到虚拟表的完整建模指南
【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine
TDengine 采用"一个数据采集点一张表 + 超级表聚合 + 虚拟表分析"的三层数据建模体系,在保留关系型模型易用性的同时充分发挥时序数据的写入与查询特性。本文基于仓库 docs/zh/04-quick-start/03-data-modeling.md 的智能电表示例,完整演示如何使用 SQL 创建数据库、超级表、子表、普通表和虚拟表,并深入解析每条语句背后的源码级实现原理。读完本文,你将掌握 TDengine 数据建模的完整套路,能够根据业务场景在"多列模型 / 单列模型"与"真实表 / 虚拟表"之间做出正确选择。
建模前置:智能电表数据模型回顾
在动手写 SQL 之前,先明确本文使用的统一数据模型(详见 基本概念):假设某型号智能电表能够采集电流(current)、电压(voltage)和相位(phase)3 个模拟量,同时每块电表还具有**位置(location)和分组 ID(group_id)**等静态属性。
- 采集量:随时间不断变化的物理量,如
current、voltage、phase,数据类型可为整型、浮点型、布尔型、字符串等; - 标签:附着在采集点上的静态属性,如
location、group_id,数据量不随时间增长; - 数据采集点:
d1001、d1002、d1003、d1004等标识符即代表不同的智能电表(数据采集点); - 表 / 超级表 / 子表:每个采集点一张表;超级表把"同一类型、结构相同"的采集点聚合成逻辑上的统一表;子表则是隶属于某张超级表的具体表。
理解了这组概念,就可以开始建库建表。
创建数据库:存储策略从建库开始
TDengine 中,库(Database)是一组表的集合,不同类型的采集数据通常具有不同的采集频率、保留期限和存储特征,建议将数据特征不同的超级表创建在不同的库中。创建一个存储电表数据的数据库:
CREATE DATABASE power PRECISION 'ms' KEEP 3650 DURATION 10 BUFFER 16;各参数含义如下:
| 参数 | 示例值 | 含义 |
|---|---|---|
PRECISION | 'ms' | 该库时序数据使用的时间戳精度,毫秒(ms) |
KEEP | 3650 | 数据保留 3650 天,超过保留期的数据被自动删除 |
DURATION | 10 | 每 10 天的数据放入一个数据文件 |
BUFFER | 16 | 写入使用大小为 16 MB 的内存池 |
创建成功后,用USE切换当前数据库,之后的插入、查询等操作都在power库中进行:
USE power;参数背后的默认值与取值范围
这些参数在服务端都有明确的默认值和合法范围,定义在仓库 include/util/tdef.h,并在建库时由管理节点进行合法性校验(见 source/dnode/mnode/impl/src/mndDb.c):
BUFFER(每 vnode 写入内存池大小):默认256 MB,合法范围 3 ~ 16384 MB(TSDB_DEFAULT_BUFFER_PER_VNODE/TSDB_MIN_BUFFER_PER_VNODE/TSDB_MAX_BUFFER_PER_VNODE)。文档示例中的BUFFER 16即显式指定了一个较小的内存池;PAGESIZE(每 vnode 存储页大小):默认4 KB,范围 1 ~ 16384 KB;DURATION(数据落盘文件的时间跨度):默认10 天(TSDB_DEFAULT_DURATION_PER_FILE定义为10 * 1440分钟),范围从 60 分钟(1 天)到 3650 天;KEEP(数据保留时长):默认3650 天(TSDB_DEFAULT_KEEP定义为3650 * 1440分钟,即十年),范围为 1 天到 365000 天;WAL_LEVEL(预写日志级别):默认 2(部分构建下为 1),范围 0 ~ 2;REPLICA(副本数):默认 1,范围 1 ~ 3。
从源码可以看出,KEEP、DURATION等"天"级参数在服务端实际以分钟为最小单位存储和计算(定义处统一乘以 1440),建库时超出范围的取值会直接返回校验错误。因此,合理规划KEEP、DURATION、BUFFER、PAGESIZE、REPLICA等参数,是时序数据存储策略优化的第一步。
创建超级表:定义采集量与标签的模板
超级表(Super Table,STable)代表"一组具有相同表结构的采集点集合",它本身只作为模板,不存储任何数据。创建名为meters的超级表:
CREATE STABLE meters ( ts timestamp, current float, voltage int, phase float ) TAGS ( location varchar(64), group_id int );CREATE STABLE语法与关系型数据库建表语句类似,规则如下:
- 第 1 列必须是时间戳列:
ts timestamp表示时间戳列名为ts,数据类型为timestamp,TDengine 会基于该列建立索引并采用列式存储; - 第 2 列开始是采集量列:数据类型可为整型、浮点型、字符串等,如
current float; TAGS关键字后定义标签:标签的数据类型同样可以是整型、浮点型、字符串等,如location varchar(64);- 约束:标签名称不能与采集量列名称相同。
超级表的标签可以在建表后灵活地增加、修改或删除,且修改对所有子表立即生效;一张超级表至少包含一个时间戳列、一个或多个采集量列以及一个或多个标签列。
创建子表:用超级表作为模板实例化采集点
子表是数据采集点在逻辑上的抽象表示,通过超级表模板 + 具体标签值创建:
CREATE TABLE d1001 USING meters ( location, group_id ) TAGS ( "California.SanFrancisco", 2 );语句解析:
CREATE TABLE表示创建子表,d1001是子表名(通常直接使用数据采集点名称);USING meters表示以超级表meters作为模板;- 超级表名后的括号内列出标签列名
location、group_id; TAGS后指定该子表的标签值:位置为California.SanFrancisco,分组 ID 为2。
子表继承了超级表的全部列结构,只是标签值不同。当对超级表进行写入或查询操作时,还可以使用伪列tbname指定或输出对应的子表名——这一能力在写入侧由 SQL 解析器识别(见 source/libs/parser/src/parInsertSql.c 中对tbname列的处理),在查询侧由 vnode 元数据模块按表 ID 反查表名实现(见 source/dnode/vnode/src/meta/metaQuery.c 中的metaGetTbnameByIdIfTableNotExist)。
自动建表:边写数据边建子表
为了简化操作并确保数据顺利写入,TDengine 允许在子表尚不存在时直接写入:系统遇到不存在的子表会先自动创建,再执行写入;若子表已存在则直接写入。
INSERT INTO d1002 USING meters TAGS ( "California.SanFrancisco", 2 ) VALUES ( NOW, 10.2, 219, 0.32 );语句解析:
INSERT INTO d1002表示向子表d1002写入数据;USING meters表示使用超级表meters作为模板;TAGS ("California.SanFrancisco", 2)指定d1002的标签值;VALUES (NOW, 10.2, 219, 0.32)表示插入一行记录:NOW(当前时间戳)、10.2(电流)、219(电压)、0.32(相位)。
从源码看,USING子句是自动建表机制的核心:解析器在 source/libs/parser/src/parInsertSql.c 中通过USING_CLAUSE标志识别该子句,并对"子表不存在则自动创建"的路径执行建表权限检查(见该文件对ctb does not exist: will be auto-created的处理逻辑)。这种"写入即建表"的机制特别适合海量设备批量接入的场景,是 TDengine 数据接入效率高的关键设计之一。
创建普通表:无标签的独立表
除子表外,TDengine 还支持不带任何标签的普通表,它与关系型数据库中的表类似,独立存在、不属于任何超级表:
CREATE TABLE meter_n ( ts timestamp, current float, voltage int, phase float, location varchar(64), group_id int );该表包含ts、current、voltage、phase、location、group_id共 6 列,数据模型与关系型数据库一致。
普通表与子表的区别总结如下:
- 标签扩展性:子表在普通表基础上增加了静态标签,可携带更多元数据信息,且标签可变,可按需增加、删除或修改;
- 表归属:子表总是隶属于某张超级表,是超级表的一部分;普通表则独立存在;
- 转换限制:普通表与子表之间无法直接相互转换,表类型在创建时即确定,后期不可更改。
需要特别说明的是性能与存储差异:采用普通表作为数据模型,意味着静态标签数据(如location、group_id)会重复存储在每一行中,不仅增加存储空间消耗,而且查询时无法直接利用标签过滤,查询性能低于使用超级表 + 子表的模型。因此生产环境强烈建议优先采用超级表模型。
多列模型与单列模型:两种建模范式的取舍
TDengine 支持灵活的数据模型设计:
- 多列模型:把同一采集点同时采集、时间戳一致的多个物理量作为不同列存储在同一张超级表中(即本文
meters的写法),写入效率和存储效率通常更优,是官方推荐的做法; - 单列模型:每个采集的物理量单独建立一张表/超级表。例如电流、电压、相位这 3 种物理量分别建立 3 张超级表。
单列模型的典型适用场景是:采集量种类经常发生变化。此时若采用多列模型,频繁修改超级表结构定义会增加应用程序复杂度;单列模型可以独立管理和扩展每个物理量的表,简化应用设计。
创建虚拟表:先写入、后建模的跨表分析利器
无论选择单列模型还是多列模型,TDengine 都可通过虚拟表(Virtual Table,VTable)进行跨表运算。虚拟表不存储实际数据,其数据来源于真实存储数据的子表或普通表,查询时按时间戳排序、对齐和合并各原始表中的不同列动态生成;若多个表在同一时间戳下都有数据则组成同一行,某表在该时间戳无数据则对应列填充NULL。虚拟表不能写入或删除数据,但查询方式与真实表相同,支持虚拟超级表、虚拟子表、虚拟普通表三种形态。
从语法实现看,VIRTUAL是建表语句的一个表选项,语法规则定义在 source/libs/parser/inc/sql.y(table_options ::= table_options VIRTUAL NK_INTEGER,其中整数用于标记虚拟表类型),这也解释了文档中VIRTUAL 1的写法来源。
虚拟表机制让"先写入后建模"成为现实:数据接入阶段按最接近设备协议的方式(如单列模型)直接写入,存储后再根据分析需求通过虚拟表动态组织业务视角的数据模型。下面以智能电表为例演示两种典型场景。
场景一:单源多维度时序聚合
"单源"指数据来自同一数据采集点下的多个单列时序数据表——这些表因业务需求被拆分为多个单列存储,但通过设备标签和时间基准保持逻辑一致性。虚拟表在此场景的作用,是把一个采集点"纵向"拆分的数据还原为完整的"横向"状态。
首先按单列模型创建 3 张超级表:
CREATE STABLE current_stb ( ts timestamp, current float ) TAGS ( device_id varchar(64), location varchar(64), group_id int ); CREATE STABLE voltage_stb ( ts timestamp, voltage int ) TAGS ( device_id varchar(64), location varchar(64), group_id int ); CREATE STABLE phase_stb ( ts timestamp, phase float ) TAGS ( device_id varchar(64), location varchar(64), group_id int );为d1001、d1002、d1003、d1004这 4 个设备的电流、电压、相位采集量分别创建子表:
CREATE TABLE current_d1001 USING current_stb (device_id, location, group_id) TAGS ("d1001", "California.SanFrancisco", 2); CREATE TABLE current_d1002 USING current_stb (device_id, location, group_id) TAGS ("d1002", "California.SanFrancisco", 3); CREATE TABLE current_d1003 USING current_stb (device_id, location, group_id) TAGS ("d1003", "California.LosAngeles", 3); CREATE TABLE current_d1004 USING current_stb (device_id, location, group_id) TAGS ("d1004", "California.LosAngeles", 2); CREATE TABLE voltage_d1001 USING voltage_stb (device_id, location, group_id) TAGS ("d1001", "California.SanFrancisco", 2); CREATE TABLE voltage_d1002 USING voltage_stb (device_id, location, group_id) TAGS ("d1002", "California.SanFrancisco", 3); CREATE TABLE voltage_d1003 USING voltage_stb (device_id, location, group_id) TAGS ("d1003", "California.LosAngeles", 3); CREATE TABLE voltage_d1004 USING voltage_stb (device_id, location, group_id) TAGS ("d1004", "California.LosAngeles", 2); CREATE TABLE phase_d1001 USING phase_stb (device_id, location, group_id) TAGS ("d1001", "California.SanFrancisco", 2); CREATE TABLE phase_d1002 USING phase_stb (device_id, location, group_id) TAGS ("d1002", "California.SanFrancisco", 3); CREATE TABLE phase_d1003 USING phase_stb (device_id, location, group_id) TAGS ("d1003", "California.LosAngeles", 3); CREATE TABLE phase_d1004 USING phase_stb (device_id, location, group_id) TAGS ("d1004", "California.LosAngeles", 2);然后创建一张虚拟超级表meters_v,将这 3 种采集量聚合到一张表:
CREATE STABLE meters_v ( ts timestamp, current float, voltage int, phase float ) TAGS ( location varchar(64), group_id int ) VIRTUAL 1;为 4 个设备分别创建虚拟子表,通过列名 FROM 源表.列名指定数据来源:
CREATE VTABLE d1001_v ( current FROM current_d1001.current, voltage FROM voltage_d1001.voltage, phase FROM phase_d1001.phase ) USING meters_v TAGS ( "California.SanFrancisco", 2 ); CREATE VTABLE d1002_v ( current FROM current_d1002.current, voltage FROM voltage_d1002.voltage, phase FROM phase_d1002.phase ) USING meters_v TAGS ( "California.SanFrancisco", 3 ); CREATE VTABLE d1003_v ( current FROM current_d1003.current, voltage FROM voltage_d1003.voltage, phase FROM phase_d1003.phase ) USING meters_v TAGS ( "California.LosAngeles", 3 ); CREATE VTABLE d1004_v ( current FROM current_d1004.current, voltage FROM voltage_d1004.voltage, phase FROM phase_d1004.phase ) USING meters_v TAGS ( "California.LosAngeles", 2 );以设备d1001为例,假设其电流、电压、相位数据如下:
| current_d1001 | voltage_d1001 | phase_d1001 | |||
|---|---|---|---|---|---|
| Timestamp | Current | Timestamp | Voltage | Timestamp | Phase |
| 1538548685000 | 10.3 | 1538548685000 | 219 | 1538548685000 | 0.31 |
| 1538548695000 | 12.6 | 1538548695000 | 218 | 1538548695000 | 0.33 |
| 1538548696800 | 12.3 | 1538548696800 | 221 | 1538548696800 | 0.31 |
| 1538548697100 | 12.1 | 1538548697100 | 220 | 1538548697200 | 0.32 |
| 1538548697700 | 11.8 | 1538548697800 | 222 | 1538548697800 | 0.33 |
写入上述数据的 SQL:
INSERT INTO current_d1001 VALUES (1538548685000, 10.3), (1538548695000, 12.6), (1538548696800, 12.3), (1538548697100, 12.1), (1538548697700, 11.8) voltage_d1001 VALUES (1538548685000, 219), (1538548695000, 218), (1538548696800, 221), (1538548697100, 220), (1538548697800, 222) phase_d1001 VALUES (1538548685000, 0.31), (1538548695000, 0.33), (1538548696800, 0.31), (1538548697200, 0.32), (1538548697800, 0.33);查询虚拟表d1001_v:
SELECT * FROM d1001_v;查询结果如下——3 张单列表按时间戳对齐合并为一张宽表,缺失的时间戳对应列自动填充NULL:
| ts | current | voltage | phase |
|---|---|---|---|
| 1538548685000 | 10.3 | 219 | 0.31 |
| 1538548695000 | 12.6 | 218 | 0.33 |
| 1538548696800 | 12.3 | 221 | 0.31 |
| 1538548697100 | 12.1 | 220 | NULL |
| 1538548697200 | NULL | NULL | 0.32 |
| 1538548697700 | 11.8 | NULL | NULL |
| 1538548697800 | NULL | 222 | 0.33 |
场景二:跨源采集量对比分析
"跨源"指数据来自不同数据采集点。从不同采集点提取具有可比语义的采集量后,通过虚拟表按时间戳对齐合并,即可进行对比分析。
以分析d1001、d1002、d1003、d1004这 4 个设备的电流数据为例,创建虚拟表:
CREATE VTABLE current_v ( ts timestamp, d1001_current float FROM current_d1001.current, d1002_current float FROM current_d1002.current, d1003_current float FROM current_d1003.current, d1004_current float FROM current_d1004.current );注意:这里创建的是虚拟普通表,不基于超级表模板,直接在列定义中逐一指定来源列。
假设 4 个设备的电流数据如下:
| current_d1001 | current_d1002 | current_d1003 | current_d1004 | ||||
|---|---|---|---|---|---|---|---|
| Timestamp | Current | Timestamp | Current | Timestamp | Current | Timestamp | Current |
| 1538548685000 | 10.3 | 1538548685000 | 11.7 | 1538548685000 | 11.2 | 1538548685000 | 12.4 |
| 1538548695000 | 12.6 | 1538548695000 | 11.9 | 1538548695000 | 10.8 | 1538548695000 | 11.3 |
| 1538548696800 | 12.3 | 1538548696800 | 12.4 | 1538548696800 | 12.3 | 1538548696800 | 10.1 |
| 1538548697100 | 12.1 | 1538548697200 | 12.2 | 1538548697100 | 11.1 | 1538548697200 | 11.7 |
| 1538548697700 | 11.8 | 1538548697700 | 11.4 | 1538548697800 | 12.1 | 1538548697800 | 12.6 |
写入上述电流数据的 SQL:
INSERT INTO current_d1001 VALUES (1538548685000, 10.3), (1538548695000, 12.6), (1538548696800, 12.3), (1538548697100, 12.1), (1538548697700, 11.8) current_d1002 VALUES (1538548685000, 11.7), (1538548695000, 11.9), (1538548696800, 12.4), (1538548697200, 12.2), (1538548697700, 11.4) current_d1003 VALUES (1538548685000, 11.2), (1538548695000, 10.8), (1538548696800, 12.3), (1538548697100, 11.1), (1538548697800, 12.1) current_d1004 VALUES (1538548685000, 12.4), (1538548695000, 11.3), (1538548696800, 10.1), (1538548697200, 11.7), (1538548697800, 12.6);查询虚拟表current_v:
SELECT * FROM current_v;查询结果如下——各设备电流按时间戳对齐为一行,便于横向对比:
| ts | d1001_current | d1002_current | d1003_current | d1004_current |
|---|---|---|---|---|
| 1538548685000 | 10.3 | 11.7 | 11.2 | 12.4 |
| 1538548695000 | 12.6 | 11.9 | 10.8 | 11.3 |
| 1538548696800 | 12.3 | 12.4 | 12.3 | 10.1 |
| 1538548697100 | 12.1 | NULL | 11.1 | NULL |
| 1538548697200 | NULL | 12.2 | NULL | 11.7 |
| 1538548697700 | 11.8 | 11.4 | NULL | NULL |
| 1538548697800 | NULL | NULL | 12.1 | 12.6 |
建模选型建议
综合全文,给出 TDengine 数据建模的推荐路径:
- 默认走超级表 + 子表模型:先确定采集点的共性结构(时间戳列 + 采集量列 + 标签列),创建超级表作为模板,再用
CREATE TABLE ... USING或自动建表INSERT ... USING实例化子表,实现标签过滤与跨采集点聚合; - 采集量稳定、同点同时采集 → 多列模型,写入和存储效率更优;
- 采集量种类频繁变化 → 单列模型,配合虚拟表在分析侧按需重组;
- 复杂设备多传感器、分析视角与写入协议不一致 → 虚拟表,实现"先写入、后建模",跨表、跨库组织统一视图;
- 建库时结合数据特征配置存储参数(
KEEP、DURATION、BUFFER、PAGESIZE、REPLICA、WAL_LEVEL等),数据特征不同的业务放入不同库。
掌握这些建模手段后,可以继续阅读 数据写入与更新 体验数据的写入、更新与删除,或阅读 查询与聚合 学习基于超级表、虚拟表的检索与分析能力。
【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考