分类 :4.查询引擎 | 篇章:08 连接算子

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-06-15
JOIN 是关系数据库的核心能力。TDengine 在标准 SQL JOIN(Inner/Left/Right/Full)之外,针对时序场景额外提供 ASOF JOIN(按时间近邻)和 Window JOIN(按时间窗口),让"两个设备时间序列对齐"这类需求一行 SQL 即可表达。
核心概念速查表
| 概念 | 说明 |
|---|---|
| Hash Join | 用哈希表的等值连接 |
| Merge Join | 输入已排序时的连接 |
| Inner Join | 仅返回两侧匹配的行 |
| Outer Join | LEFT/RIGHT/FULL 保留一侧或两侧 |
| ASOF Join | 时间近邻连接(找最接近的时间点) |
| Window Join | 时间窗口连接(同窗口内对齐) |
| Equi Join Condition | 等值连接条件(必须含时间等值) |
详细解析
1. TDengine JOIN 的时间约束
TDengine 的 JOIN 核心要求:
✓ JOIN 必须包含时间戳等值条件(普通 JOIN)
或时间相关条件(ASOF / Window JOIN)
示例(合法):
SELECT * FROM t1 JOIN t2 ON t1.ts = t2.ts;
SELECT * FROM t1 JOIN t2 ON t1.ts = t2.ts AND t1.id = t2.id;
✗ 不合法:
SELECT * FROM t1 JOIN t2 ON t1.id = t2.id;
-- 缺少时间条件
原因:
- 时序数据按时间分布
- 没有时间约束的 JOIN 是笛卡尔积级别的开销
- 强制时间条件保证可优化为窗口对齐
2. Inner Join 与 Outer Join
JOIN 类型对比:
数据示例:
t1: t2:
ts=T1, v=10 ts=T1, v=100
ts=T2, v=20 ts=T3, v=300
ts=T3, v=30
INNER JOIN (ts=ts):
T1: (10, 100)
T3: (30, 300)
[T2 不匹配,被丢弃]
LEFT JOIN:
T1: (10, 100)
T2: (20, NULL) ← 保留左侧
T3: (30, 300)
RIGHT JOIN:
T1: (10, 100)
T3: (30, 300)
[t1 中无 T2 也无影响,因为 RIGHT 保留右侧]
FULL OUTER JOIN:
T1: (10, 100)
T2: (20, NULL)
T3: (30, 300)
3. ASOF JOIN(时间近邻)
ASOF JOIN 场景:
设备 A 每秒采集,设备 B 每 5 秒采集
问题:A 的每个时间点对应的 B 最近一次采集?
数据:
A: T1, T2, T3, T4, T5, T6, T7, T8
B: T1, T6
ASOF JOIN A LEFT ASOF JOIN B ON A.ts >= B.ts:
每个 A.ts 找到 B 中 <=A.ts 的最大者
A.T1 → B.T1
A.T2 → B.T1
A.T3 → B.T1
A.T4 → B.T1
A.T5 → B.T1
A.T6 → B.T6
A.T7 → B.T6
A.T8 → B.T6
语法:
SELECT a.ts, a.v, b.v
FROM ta a
ASOF JOIN tb b
ON a.ts >= b.ts
AND a.id = b.id -- Tag 等值约束(可选);
支持的比较操作:
>, >=, <, <=
4. Window JOIN(时间窗口)
Window JOIN 场景:
问题:每对设备的同一分钟内的关联事件
Window JOIN tb b WINDOW(1m) ON ta.id = tb.id:
每个 ta 的行,找 tb 中 [ta.ts - 30s, ta.ts + 30s] 内的所有行
示例:
ta: T1=12:00:10, T2=12:00:50
tb: B1=12:00:15, B2=12:01:30
ta=T1 → tb 在 [11:59:40, 12:00:40] → B1 匹配
ta=T2 → tb 在 [12:00:20, 12:01:20] → 无匹配
语法:
SELECT * FROM ta
WINDOW JOIN tb
WINDOW(1m) -- 窗口大小
ON ta.id = tb.id;
5. Hash Join 实现
Hash Join 的两阶段:
阶段 1:构建(Build)
选择较小的表(构建侧)
读取所有行 → 构建哈希表(Key = JOIN 键)
阶段 2:探测(Probe)
扫描较大的表(探测侧)
对每行:用 JOIN 键查找哈希表
匹配则输出
示例:
SELECT * FROM big JOIN small ON big.id = small.id AND big.ts = small.ts
Build (small):
哈希表:
(id=1, ts=T1) → row_data
(id=1, ts=T2) → row_data
(id=2, ts=T1) → row_data
Probe (big):
扫描 big 每行
查询哈希表是否有匹配
特点:
✓ 适合 = 等值连接
✓ 大小表组合
✗ 构建侧必须放入内存
6. Merge Join 实现
Merge Join(输入已排序):
前提:两侧输入按 JOIN 键有序
算法:
指针 i 指向 t1 第一行
指针 j 指向 t2 第一行
while i < len(t1) and j < len(t2):
if t1[i].key == t2[j].key:
输出 (t1[i], t2[j])
i++ 或 j++(处理重复键)
elif t1[i].key < t2[j].key:
i++
else:
j++
TDengine 中的时间 JOIN 天然适合 Merge Join:
- 两侧数据都按 ts 有序
- 不需要构建哈希表
- 内存占用 O(1)
- 适合海量数据
7. JOIN 的分布式执行
跨 VGroup 的 JOIN 执行:
SELECT * FROM ta JOIN tb ON ta.ts = tb.ts AND ta.id = tb.id
ta 跨 VGroup 1, 2
tb 跨 VGroup 3, 4
执行选项:
① 广播 JOIN(适合小表):
- 小表(如 tb)拉取到所有 ta 所在节点
- 每个 ta 节点本地 Hash Join
② Shuffle JOIN(适合大表):
- 两侧都按 JOIN 键 Shuffle 到相同节点
- 各节点 Hash/Merge Join
- 适合大表 + 大表
③ 单子表 JOIN(最简单):
- 如果 ta 和 tb 都是子表
- 通常单 VGroup 内完成
- 无需 Shuffle
8. JOIN 性能调优
JOIN 性能关键点:
① 选择性优先:
先过滤再 JOIN
SELECT * FROM ta JOIN tb ON ta.ts=tb.ts
WHERE ta.location='BJ' AND tb.location='BJ'
→ 过滤下推到 Scan
→ JOIN 输入数据量减少
② 时间范围必须明确:
SELECT * FROM ta JOIN tb ON ta.ts=tb.ts WHERE ta.ts > now-1h
→ 同时限制 ta 和 tb 的时间范围
③ 数据局部性:
同 VGroup 的子表 JOIN → 无 Shuffle
跨 VGroup 的 JOIN → Shuffle 开销
代码示例
基础 JOIN
sql
-- 两个超级表的时间对齐
SELECT a.ts, a.current, b.power
FROM electric_meters a
JOIN power_meters b
ON a.ts = b.ts AND a.location = b.location
WHERE a.ts > now-1h;
-- LEFT JOIN 保留所有 A
SELECT a.ts, a.current, b.power
FROM electric_meters a
LEFT JOIN power_meters b
ON a.ts = b.ts AND a.location = b.location;
ASOF JOIN
sql
-- 高频设备对低频参考值
SELECT a.ts, a.current, b.standard_voltage
FROM realtime_sensor a
LEFT ASOF JOIN reference_sensor b
ON a.ts >= b.ts
AND a.location = b.location
WHERE a.ts > now-1h;
Window JOIN
sql
-- 找出每个温度异常前后 1 分钟的湿度记录
SELECT t.ts AS temp_ts, h.ts AS humi_ts, t.temperature, h.humidity
FROM temperature_log t
WINDOW JOIN humidity_log h
WINDOW(1m)
ON t.location = h.location
WHERE t.temperature > 40
AND t.ts > now-1d;
性能考量
JOIN 类型选择
| 场景 | 推荐 JOIN |
|---|---|
| 等频率采集对齐 | INNER JOIN ON ts |
| 不同采集频率对齐 | LEFT ASOF JOIN |
| 事件关联(同窗口内任意点) | WINDOW JOIN |
| 维度表关联 | INNER JOIN(含 Tag 等值) |
性能优化清单
- WHERE 同时限制两侧的时间范围
- WHERE 同时过滤两侧的 Tag(让数据局部化)
- 优先选具体列,避免 SELECT *
- 小表放右侧(可能影响 Build/Probe 选择)
- 大基数 JOIN 考虑 QNode
FAQ
Q1: 为什么我的 JOIN 报"missing time condition"?
TDengine 要求 JOIN 必须有时间相关条件。改写:
- 普通 JOIN:
ON ... AND t1.ts = t2.ts - 时间近邻:用
ASOF JOIN - 时间窗口:用
WINDOW JOIN
Q2: ASOF JOIN 性能如何?
输入按 ts 有序时(时序数据天然如此),用 Merge 风格算法,复杂度 O(N+M)。生产环境处理百万级数据行毫秒~秒级。
Q3: 多表 JOIN(≥3)支持吗?
支持但复杂度高,每多一张表 JOIN 次数线性增加。建议:
- 拆分为多个简单查询
- 用应用层组合
- 或预先 ETL 到宽表
Q4: JOIN 和 UNION 哪个更适合?
- JOIN:横向合并(增加列)
- UNION:纵向合并(增加行)
- 多设备同类数据汇总用 UNION
- 多种数据类型对齐用 JOIN
参考
系统构架篇
- 01-《TDengine 整体架构全景》
- 02-《集群拓扑深度解析》
- 03-《MNode 内部机制深度解析》
- 04-《RPC 通信层深度解析》
- 05-《VNode 生命周期》
- 06-《RAFT 共识协议》
- 07-《端到端的消息流》
数据模型
- 01-《数据库创建与参数详解》
- 02-《超级表/子表/普通表》
- 03-《支持数据类型深度解析》
- 04-《TDengine Tag 设计哲学与 Schema 变更机制》
- 05-《TDengine 虚拟表实现原理》
存储引擎
- 01-《TDengine 存储引擎概览》
- 02-《TDengine MemTable 深度解析》
- 03-《TDengine WAL 预写日志机制》
- 04-《TDengine 数据文件格式》
- 05-《TDengine Commit 与 Flush 机制 》
- 06-《TDengine Compaction 合并策略 》
- 07-《TDengine 数据保留与 TTL》
- 08-《TDengine 压缩编码机制》
- 09-《TDengine Cache 与 Last 查询加速》
- 10-《TDengine 逻辑计划生成》