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 内完成
     - 无需 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

参考

系统构架篇

数据模型

存储引擎

查询引擎

相关推荐
春日见1 小时前
vscode的AI编程插件推荐:
大数据·ide·vscode·算法·机器学习·编辑器·ai编程
2601_959481921 小时前
CPT Markets:把信息披露习惯做到位——路径梳理与提示整理
大数据
wuhanzhanhui1 小时前
9月22日-24日,2026武汉仪器仪表展会引领智能制造未来发展方向
制造·时序数据库
Keano Reurink1 小时前
搜索API与GSC数据对比:发现数据盲区
数据库·python·数据挖掘
shushangyun_1 小时前
汽车服务行业B2B平台+AI解决方案哪家专业:2026年最新测评
java·运维·网络·数据库·人工智能·汽车
大黄说说2 小时前
深入理解 Go 协程 Goroutine:并发编程的核心精髓
java·数据库·python
小懿互联集成平台2 小时前
金蝶云星空与赛狐跨境电商ERP系统数据互通对接
大数据·金蝶云星空·数据对接·小懿互联·赛狐erp
sulikey2 小时前
数据库系统概论4 - 更新与视图 期末速成课笔记
数据库·笔记·考试·期末速成·数据库系统概论
锋行天下2 小时前
数据库安全并发控制详解:乐观锁 vs 悲观锁 vs 原子操作
前端·数据库·后端