TDengine 虚拟表实现原理

2.数据模型 > 05 虚拟表 --- 跨表列对齐与逻辑视图

适用版本:TDengine v3.3.6.0+(v3.4.x) | 最后更新:2026-05-23

概述

虚拟表(Virtual Table)是 TDengine v3.3.6.0 引入的逻辑表抽象------它没有物理存储,而是将多张物理表的列按时间戳对齐后组合成一个可查询的整体。虚拟表解决了两个核心场景:

  1. 单设备多表聚合:当一个设备的各指标分散在不同的单列表中(常见于 OPC-UA / Modbus 采集),虚拟表将它们"拼"回一张完整的设备宽表
  2. 跨设备对比分析:将不同设备的同类指标对齐到同一时间轴,便于横向比较

虚拟表是只读的------不能直接写入数据,数据来源于被引用的物理表,查询时动态计算。

核心概念速查表

概念 说明
虚拟表(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 0IGNORE 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() 查询都需要实际读取来源表的最新数据。

参考

系统构架篇

数据模型

关于 TDengine

TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。

相关推荐
吃好睡好便好10 小时前
用if…elseif…end语句输出成绩等级
开发语言·前端·javascript·数据库·学习·matlab·信息可视化
努力努力再努力wz10 小时前
【Redis入门系列】:Redis 内部编码机制与 String 深度解析:SDS 底层实现、三种编码与核心命令详解
c语言·开发语言·数据结构·数据库·c++·redis·缓存
狒狒热知识10 小时前
媒体发稿软文营销行业价值升级从简单发稿到品牌全案传播服务进化
大数据·人工智能
lbb 小魔仙10 小时前
海量时序数据困局破壁:DolphinDB 如何重新定义工业物联网的数据底座
物联网
罗超驿10 小时前
21.jdbc 学习笔记:从原理到实践的全流程梳理
java·数据库·mysql·面试
楠枬10 小时前
Redis 分布式锁
数据库·redis·分布式
从此以后自律10 小时前
Git一篇
大数据·elasticsearch·搜索引擎
超人也会哭️呀10 小时前
ES 混合检索(文本+向量)中的条件处理陷阱——当权限过滤遇到关键词查询
android·大数据·elasticsearch
财经资讯数据_灵砚智能10 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年5月23日
大数据·人工智能·python·信息可视化·自然语言处理