Doris Join 概述:从概念到执行流

各位同学,欢迎来到《Doris多表关联查询性能调优》的第一章。我是你们的老朋友,一个跟分布式数据库打了十几年交道的工程师。今天咱们先不急着调优,先把 Join 这件事本身聊透。

你想想看,数据分析里最常干的事是什么?就是把几张表拼在一起。订单表关联用户表,行为日志关联商品信息……说白了,Join 就是数据世界的「拼图游戏」。但拼不好,性能就崩了。

Join 的基本概念

Join 的核心逻辑,就是根据两个表之间的关联条件,把符合条件的行组合成一行输出。我习惯把参与 Join 的表分成两类:

  • 左表(Left Table):驱动表,通常是数据量较小的那个
  • 右表(Right Table):被驱动表,通常是数据量较大的那个

举个例子,订单表(orders)关联用户表(users),条件就是 orders.user_id = users.id。Doris 会拿着左表的每一行,去右表里找匹配的行。嗯,这里要注意——匹配不上怎么办?这就引出了不同的 Join 类型。

核心要点:Join 的本质是「行组合」操作。理解了这个,后面看执行计划才不会懵。

Doris 支持的 Join 类型

Doris 支持五种标准 Join 类型。我在项目中见过不少同学把 Left Join 和 Full Join 搞混,咱们一次性说清楚。

Join 类型 语义说明 返回行数 典型场景
Inner Join 只返回两表匹配的行 ≤ 左表行数 订单与支付记录匹配
Left Join 返回左表所有行,右表无匹配则补 NULL ≥ 左表行数 用户列表及其订单(无订单也保留用户)
Right Join 返回右表所有行,左表无匹配则补 NULL ≥ 右表行数 商品列表及其销量(无销量也保留商品)
Full Join 返回两表所有行,无匹配侧补 NULL 两表行数之和 数据对账、差异分析
Cross Join 笛卡尔积,两表行数相乘 左表行数 × 右表行数 生成测试数据、枚举组合

避坑指南:我曾经接手过一个慢查询,开发同学写了个 Left Join 但条件里加了右表的过滤,结果 Doris 自动转成了 Inner Join,数据对不上。记住——右表过滤条件写在 WHERE 里会改变 Join 语义,要写在 ON 子句中才能保持 Left Join 的效果。

Join 的执行流程概览

Doris 执行一个 Join 查询,大致走这么几步。我画了张图,你一看就明白。

Doris Join 执行流程概览 1. SQL 解析与语义分析 2. Join 类型与条件识别 3. 执行策略选择(Hash Join / Shuffle Join) 4. 数据分发与 Shuffle 5. 实际 Join 计算 6. 结果聚合与输出 关键决策点 第3步决定性能上限 Doris 根据数据量、 分布和 Join 类型自动选择 常见执行策略 • Broadcast Join • Shuffle Join • Bucket Shuffle Join

流程看起来不复杂,对吧?但实际跑起来,每一步都可能成为瓶颈。我挑几个关键点说说:

  1. SQL 解析阶段:Doris 会检查表是否存在、字段是否合法、Join 条件是否有效。这个阶段一般很快,但如果 SQL 写得特别长(比如几百行子查询嵌套),解析也会慢。
  2. 执行策略选择:这是最核心的一步。Doris 会根据左右表的数据量、数据分布、Join 类型,决定用 Broadcast Join 还是 Shuffle Join。我个人的经验是——小表广播、大表 Shuffle,这个原则能解决 80% 的性能问题。
  3. 数据分发:Shuffle 阶段会把数据按照 Join key 重新分区。如果 Join key 的分布不均匀(比如某个用户 ID 占了 90% 的数据),就会产生数据倾斜。嗯,这个问题我们后面会专门用一章来讲。

我的小技巧:在 Doris 里,你可以用 EXPLAIN 命令查看执行计划。比如 EXPLAIN SELECT ... FROM A JOIN B ON ...,Doris 会告诉你它选择了哪种 Join 策略。我每次写复杂查询都会先看一眼执行计划,确认策略对不对再跑。

一个简单的例子

光说不练假把式。咱们看个实际例子:

-- 创建订单表
CREATE TABLE orders (
    order_id BIGINT,
    user_id BIGINT,
    amount DECIMAL(10,2),
    order_date DATE
) DISTRIBUTED BY HASH(user_id) BUCKETS 10;

-- 创建用户表
CREATE TABLE users (
    user_id BIGINT,
    user_name VARCHAR(50),
    city VARCHAR(20)
) DISTRIBUTED BY HASH(user_id) BUCKETS 5;

-- 查询每个用户的订单总额
SELECT u.user_name, SUM(o.amount) AS total_amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_name;

这个查询里,users 是左表,orders 是右表。Doris 会先判断哪个表小——如果 users 只有几千行,而 orders 有几千万行,它大概率会选择 Broadcast Join,把小表广播到所有 BE 节点上,避免大表 Shuffle。

但如果你把条件反过来,或者 Join key 的分布有问题,性能可能就天差地别了。这些坑,咱们后面的章节一个一个填。