TDengine 连接算子 — Inner/Outer/ASOF/Window Join 的实现与使用
分类: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 内完成 - 无需 Shuffle8. 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
-- 两个超级表的时间对齐SELECTa.ts,a.current,b.powerFROMelectric_meters aJOINpower_meters bONa.ts=b.tsANDa.location=b.locationWHEREa.ts>now-1h;-- LEFT JOIN 保留所有 ASELECTa.ts,a.current,b.powerFROMelectric_meters aLEFTJOINpower_meters bONa.ts=b.tsANDa.location=b.location;ASOF JOIN
-- 高频设备对低频参考值SELECTa.ts,a.current,b.standard_voltageFROMrealtime_sensor aLEFTASOFJOINreference_sensor bONa.ts>=b.tsANDa.location=b.locationWHEREa.ts>now-1h;Window JOIN
-- 找出每个温度异常前后 1 分钟的湿度记录SELECTt.tsAStemp_ts,h.tsAShumi_ts,t.temperature,h.humidityFROMtemperature_log t WINDOWJOINhumidity_log h WINDOW(1m)ONt.location=h.locationWHEREt.temperature>40ANDt.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 逻辑计划生成》
查询引擎
- 01-《TDengine 查询引擎概览》
- 02-《TDengine SQL 解析与词法分析》
- 03-《TDengine 语义分析与 AST 重写》
- 04-《TDengine 逻辑计划生成》
- 05-《TDengine 物理计划生成》
- 06-《TDengine 扫描算子》
- 07-《TDengine 聚合算子》
