2.数据模型 > 05 虚拟表 --- 跨表列对齐与逻辑视图
适用版本:TDengine v3.3.6.0+(v3.4.x) | 最后更新:2026-05-23

概述
虚拟表(Virtual Table)是 TDengine v3.3.6.0 引入的逻辑表抽象------它没有物理存储,而是将多张物理表的列按时间戳对齐后组合成一个可查询的整体。虚拟表解决了两个核心场景:
- 单设备多表聚合:当一个设备的各指标分散在不同的单列表中(常见于 OPC-UA / Modbus 采集),虚拟表将它们"拼"回一张完整的设备宽表
- 跨设备对比分析:将不同设备的同类指标对齐到同一时间轴,便于横向比较
虚拟表是只读的------不能直接写入数据,数据来源于被引用的物理表,查询时动态计算。
核心概念速查表
| 概念 | 说明 |
|---|---|
| 虚拟表(Virtual Table) | 无物理存储的逻辑表,列引用自其他物理表 |
| 虚拟超级表 | 通过 CREATE STABLE ... VIRTUAL 1 创建的虚拟超级表模板 |
| 虚拟子表 | 从虚拟超级表派生的虚拟子表,每列映射到不同物理表的列 |
| 虚拟普通表 | 独立的虚拟表(不从属于超级表) |
| 时间戳对齐 | 虚拟表的时间戳列为所有引用列来源表时间戳的并集 |
| NULL 填充 | 某时刻某来源表无数据时,对应列填充 NULL |
| FROM 映射 | 指定虚拟表的列来源于哪张物理表的哪一列 |
详细解析
1. 设计动机
1.1 问题场景:单列表模型
在工业数据采集中,很多协议(OPC-UA、Modbus)按测点采集数据,每个测点一张表:
传统采集的表结构(每个测点一张表):
表 device01_current: ts | current
表 device01_voltage: ts | voltage
表 device01_phase: ts | phase
表 device01_power: ts | power
问题:查看 device01 的完整状态需要 JOIN 4 张表
随着测点增多,JOIN 操作复杂度急剧上升
1.2 虚拟表的解决方案
虚拟表将分散的测点"拼"回一张逻辑宽表:
虚拟表 device01_all:
ts | current | voltage | phase | power
(无存储) ↓ ↓ ↓ ↓
来自 来自 来自 来自
device01_ device01_ device01_ device01_
current voltage phase power
查询时:SELECT * FROM device01_all
等价于:自动按时间戳对齐合并 4 张表的数据
1.3 与传统 JOIN 的区别
| 对比 | 虚拟表 | 手动 JOIN |
|---|---|---|
| 语法复杂度 | SELECT * FROM vtable |
多表 JOIN + 时间对齐条件 |
| 性能优化 | 专用算子,流水线合并 | 通用 JOIN 算子 |
| 维护成本 | 建表一次,查询时自动 | 每次查询都要写 JOIN |
| 时间对齐 | 自动按时间戳并集对齐 | 需手动处理对齐逻辑 |
| 可用于流计算 | ✅ | 有限制 |
2. 虚拟表的三种类型
与普通表的三层结构(超级表 → 子表 → 普通表)对应,虚拟表也有三种类型:
虚拟表类型体系:
┌─────────────────────────────────────┐
│ 虚拟超级表(Virtual Super Table) │
│ - 定义列结构和 Tag 模板 │
│ - 通过 CREATE STABLE ... VIRTUAL 1 │
│ │
│ ├── 虚拟子表 A │
│ │ 每列 FROM 不同物理表.列 │
│ │ │
│ └── 虚拟子表 B │
│ 每列 FROM 不同物理表.列 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 虚拟普通表(Virtual Normal Table) │
│ - 独立存在,不从属于超级表 │
│ - 通过 CREATE VTABLE 创建 │
└─────────────────────────────────────┘
3. 创建虚拟表
3.1 创建虚拟普通表
sql
-- 创建虚拟普通表:将 3 个物理表的列组合在一起
CREATE VTABLE device01_view
ts TIMESTAMP,
(current FLOAT FROM device01_current.current,
voltage INT FROM device01_voltage.voltage,
phase FLOAT FROM device01_phase.phase);
规则:
- 第一列必须是 TIMESTAMP 类型且不能有 FROM 引用(时间戳自动计算)
- 其余列必须通过 FROM 指定来源(或稍后通过 ALTER 设置)
- 引用的列类型必须与虚拟表定义的列类型一致
3.2 创建虚拟超级表
sql
-- 创建虚拟超级表(模板)
CREATE STABLE meters_virtual (
ts TIMESTAMP,
current FLOAT,
voltage INT,
phase FLOAT
) TAGS (
location BINARY(64),
group_id INT
) VIRTUAL 1;
虚拟超级表的 VIRTUAL 1 选项标识这是一个虚拟超级表模板。
3.3 创建虚拟子表
sql
-- 创建虚拟子表:每列映射到具体的物理子表列
CREATE VTABLE vt_device01 (
current FROM d01_current.value,
voltage FROM d01_voltage.value,
phase FROM d01_phase.value
) USING meters_virtual TAGS ('California', 2);
-- 另一台设备
CREATE VTABLE vt_device02 (
current FROM d02_current.value,
voltage FROM d02_voltage.value,
phase FROM d02_phase.value
) USING meters_virtual TAGS ('Beijing', 1);
3.4 跨数据库引用
虚拟表可以引用其他数据库的物理表列:
sql
-- 引用 other_db 数据库中的表
CREATE VTABLE cross_db_view
ts TIMESTAMP,
(temp FLOAT FROM other_db.sensor01.temperature,
pressure DOUBLE FROM current_db.pressure_table.value);
限制:虚拟超级表的子表只能引用同一数据库内的物理表(不支持跨库)。虚拟普通表无此限制。
4. 时间戳对齐机制
4.1 对齐原理
虚拟表的时间戳列是所有被引用来源表时间戳的并集:
时间戳对齐示例:
来源表 A (current): 来源表 B (voltage):
10:00:01 3.5 10:00:01 220
10:00:02 3.6 10:00:03 221
10:00:03 3.7 10:00:05 219
虚拟表查询结果(SELECT * FROM vtable):
ts | current | voltage
10:00:01 | 3.5 | 220 ← 两表都有
10:00:02 | 3.6 | NULL ← B 表无数据
10:00:03 | 3.7 | 221 ← 两表都有
10:00:05 | NULL | 219 ← A 表无数据
4.2 查询列选择影响结果行数
重要特性:虚拟表的不同查询可能返回不同的行数------取决于所查询列涉及的来源表:
sql
-- 假设 A 表有 100 个时间点,B 表有 80 个时间点,其中 60 个重叠
SELECT current FROM vtable; -- 返回 100 行(只涉及 A 表的时间戳)
SELECT voltage FROM vtable; -- 返回 80 行(只涉及 B 表的时间戳)
SELECT current, voltage FROM vtable; -- 返回 120 行(A ∪ B 的时间戳并集)
这与普通表的行为不同(普通表不管查哪些列,行数都一样)。
4.3 执行原理
虚拟表查询执行:
SELECT current, voltage FROM vtable
│
▼
查询优化器识别虚拟表:
- current 来自表 A
- voltage 来自表 B
│
▼
生成物理计划:
VirtualTableScan 算子
├── TableScan(表 A) → 读取 ts, current
└── TableScan(表 B) → 读取 ts, voltage
│
▼
合并排序(Merge Sort by ts):
- 两个数据流按时间戳合并
- 相同时间戳 → 合并为一行
- 不同时间戳 → 缺失列填 NULL
│
▼
输出对齐后的结果集
5. 修改虚拟表
5.1 添加/删除列
sql
-- 添加列(指定来源)
ALTER VTABLE device01_view ADD COLUMN power FLOAT FROM device01_power.power;
-- 添加列(暂不指定来源,后续设置)
ALTER VTABLE device01_view ADD COLUMN power FLOAT;
-- 删除列
ALTER VTABLE device01_view DROP COLUMN phase;
-- 修改列名
ALTER VTABLE device01_view RENAME COLUMN current current_amp;
5.2 修改列的来源映射
sql
-- 将 current 列的来源改为另一张表
ALTER VTABLE device01_view ALTER COLUMN current SET other_table.current;
-- 取消列的来源映射(列将始终返回 NULL)
ALTER VTABLE device01_view ALTER COLUMN current SET NULL;
5.3 修改虚拟子表的 Tag
sql
ALTER VTABLE vt_device01 SET TAG location = 'NewYork';
6. 删除虚拟表
sql
-- 删除虚拟普通表
DROP VTABLE [IF EXISTS] device01_view;
-- 删除虚拟子表
DROP VTABLE [IF EXISTS] vt_device01;
-- 删除虚拟超级表(级联删除所有虚拟子表)
DROP STABLE meters_virtual;
删除虚拟表不会影响被引用的物理表及其数据。
7. 查看虚拟表信息
sql
-- 查看数据库中的虚拟表
SHOW VTABLES;
SHOW NORMAL VTABLES; -- 只看虚拟普通表
SHOW CHILD VTABLES; -- 只看虚拟子表
-- 查看虚拟表结构
DESCRIBE device01_view;
-- 查看创建语句(含 FROM 映射)
SHOW CREATE VTABLE device01_view;
-- 从系统表查询
SELECT * FROM information_schema.ins_tables
WHERE type IN ('VIRTUAL_NORMAL_TABLE', 'VIRTUAL_CHILD_TABLE');
8. 虚拟表与流计算
虚拟表可以作为流计算的数据源(v3.3.6.0+):
sql
-- 基于虚拟表创建流
CREATE STREAM power_stream INTO power_result AS
SELECT _wstart, AVG(current), MAX(voltage)
FROM device01_view
INTERVAL(1m);
流计算的限制:
- Watermark 必须设为 0
- 不支持
FILL_HISTORY 1 - 不支持
IGNORE UPDATE 0和IGNORE EXPIRED 0 - 不支持 COUNT_WINDOW
- 虚拟表的 Schema 在流运行期间不能修改
- 新增的虚拟子表不会自动加入已有的流
9. 使用限制汇总
| 类别 | 限制说明 |
|---|---|
| 写入 | 虚拟表不支持 INSERT / DELETE(只读) |
| 来源限制 | FROM 只能引用普通表或子表,不能引用超级表、视图、其他虚拟表 |
| 类型匹配 | 虚拟表列类型必须与来源列类型完全一致 |
| 复合主键 | 来源表不能使用复合主键 |
| 订阅 | 虚拟表不能作为数据订阅(TMQ)的 Topic |
| 视图 | 不能在虚拟表上创建视图,也不能从视图创建虚拟表 |
| STMT | 不支持参数绑定查询 |
| DECIMAL | 不支持 DECIMAL 类型 |
| 跨库子表 | 虚拟超级表的子表来源必须在同一数据库 |
代码示例
场景 1:OPC-UA 单列表聚合
sql
-- 原始数据:每个测点一张表(OPC-UA 采集模式)
-- plc01_temp: ts | value (温度)
-- plc01_press: ts | value (压力)
-- plc01_flow: ts | value (流量)
-- plc01_level: ts | value (液位)
-- 创建虚拟表聚合所有测点
CREATE VTABLE plc01_all
ts TIMESTAMP,
(temperature FLOAT FROM plc01_temp.value,
pressure FLOAT FROM plc01_press.value,
flow FLOAT FROM plc01_flow.value,
level FLOAT FROM plc01_level.value);
-- 现在可以像查普通宽表一样查询
SELECT * FROM plc01_all WHERE ts >= NOW() - 1h;
-- 聚合查询
SELECT _wstart, AVG(temperature), MAX(pressure)
FROM plc01_all
INTERVAL(5m);
场景 2:多设备对比(虚拟超级表)
sql
-- 每台设备有独立的电流子表
-- device01_current, device02_current, device03_current
-- 创建虚拟超级表
CREATE STABLE current_compare (
ts TIMESTAMP,
current FLOAT
) TAGS (
device_name BINARY(32)
) VIRTUAL 1;
-- 创建虚拟子表映射
CREATE VTABLE vc_dev01 (current FROM device01_current.value)
USING current_compare TAGS ('Device-01');
CREATE VTABLE vc_dev02 (current FROM device02_current.value)
USING current_compare TAGS ('Device-02');
CREATE VTABLE vc_dev03 (current FROM device03_current.value)
USING current_compare TAGS ('Device-03');
-- 多设备电流对比查询
SELECT device_name, AVG(current)
FROM current_compare
WHERE ts >= NOW() - 1d
GROUP BY device_name;
-- 使用 PARTITION BY 按设备分别统计
SELECT _wstart, device_name, AVG(current)
FROM current_compare
PARTITION BY device_name
INTERVAL(1h);
场景 3:跨库数据聚合
sql
-- 温度在 env_db 数据库,电力在 power_db 数据库
CREATE VTABLE combined_view
ts TIMESTAMP,
(temperature FLOAT FROM env_db.room01_temp.value,
power_usage FLOAT FROM power_db.room01_power.watts);
-- 联合分析温度与能耗的关系
SELECT _wstart, AVG(temperature), SUM(power_usage)
FROM combined_view
INTERVAL(1h);
性能考量
查询性能特点
| 方面 | 说明 |
|---|---|
| 无存储开销 | 虚拟表不占用磁盘空间 |
| 查询时计算 | 每次查询都从来源表实时读取并合并 |
| 并行扫描 | 各来源表可并行读取 |
| 合并排序 | 时间戳对齐使用流式合并排序,内存开销可控 |
与直接查询物理表的对比
性能对比:
直接查物理表: 只读取 1 张表 → 最快
虚拟表(2列2表): 读取 2 张表 + 合并排序 → 约 1.5~2× 开销
虚拟表(10列10表):读取 10 张表 + 合并排序 → 约 3~5× 开销
手动 JOIN: 通用 JOIN 算子 → 通常比虚拟表慢
建议:如果物理表设计合理(宽表),不需要虚拟表
只有当数据已经分散在多表时,虚拟表才是最优解
适用场景判断
| 场景 | 是否使用虚拟表 |
|---|---|
| 数据已经按设备存储在宽表中 | ❌ 不需要 |
| OPC-UA/Modbus 单测点单表 | ✅ 推荐 |
| 需要跨设备对比分析 | ✅ 推荐 |
| 需要对虚拟表写入数据 | ❌ 不支持 |
| 高频实时查询(亚毫秒) | ⚠️ 有合并开销 |
FAQ
Q1: 虚拟表的数据来源表被删除了会怎样?
来源表被删除后,虚拟表中对应的列查询将返回 NULL。虚拟表本身不会自动删除------它只是失去了该列的数据来源。可以通过 ALTER VTABLE 重新映射到其他表。
Q2: 虚拟表能写入数据吗?
不能。虚拟表是纯只读的逻辑视图。要写入数据,需要直接写入来源物理表。来源表的新数据会自动反映在虚拟表的查询结果中。
Q3: 虚拟表和 VIEW 有什么区别?
- 虚拟表:是一种特殊的表类型,支持按时间戳对齐合并多表、支持流计算、有独立的权限
- VIEW:是 SQL 查询的命名封装,不做特殊的时间对齐处理
虚拟表的时间戳对齐是其核心价值------自动处理不同频率采集的数据对齐,这是普通 VIEW 做不到的。
Q4: 所有来源表的采集频率不同怎么办?
虚拟表天然处理这种情况------时间戳列是所有来源的并集,缺失的列自动填 NULL。如果需要插值而非 NULL,可以在查询时使用 FILL 子句:
sql
SELECT _wstart, INTERP(temperature), INTERP(pressure)
FROM vtable
RANGE('2024-01-01', '2024-01-02')
EVERY(1s)
FILL(PREV); -- 用前值填充
Q5: 虚拟超级表查询时,会扫描所有虚拟子表的所有来源表吗?
不会。与普通超级表一样,虚拟超级表查询先通过 Tag 过滤确定需要的虚拟子表,然后只查询这些子表引用的来源物理表。
Q6: 虚拟表支持 LAST() 和 LAST_ROW() 吗?
支持。但由于虚拟表无物理存储,无法使用 CACHEMODEL 加速。每次 LAST()/LAST_ROW() 查询都需要实际读取来源表的最新数据。
参考
系统构架篇
- 01-《TDengine 整体架构全景》
- 02-《集群拓扑深度解析》
- 03-《MNode 内部机制深度解析》
- 04-《RPC 通信层深度解析》
- 05-《VNode 生命周期》
- 06-《RAFT 共识协议》
- 07-《端到端的消息流》
数据模型
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。