订阅更新

3张表JOIN在一起,执行顺序到底是什么?

SQL手册 第 13 题 约 9 分钟读完

本文目录
  1. 总结
  2. 面试原题
  3. SQL写的顺序 ≠ 实际执行顺序
  4. 查看执行计划
  5. 多表JOIN的优化原则
  6. 实战场景:我的广告日报SQL
  7. 面试加分:JOIN的数据膨胀问题

总结

优化原则具体做法
------------------
先过滤再JOIN子查询/谓词下推,减少参与JOIN的数据量
小表广播让小维度表走Broadcast Join
减少JOIN次数小表先合并,再和大表JOIN
避免笛卡尔积检查每个ON条件
注意数据膨胀一对多JOIN后做聚合要小心
看执行计划EXPLAIN确认优化器的实际决策

面试答题思路:先写出正确的SQL,再从执行计划、表大小、过滤条件三个角度分析优化空间,最后提数据膨胀的风险。

明天 DAY 14 聊分区表的设计与查询优化,分区是大数据SQL性能优化的基石,分区设计得好,查询性能可以提升几十倍。

你在工作中写过最多几张表JOIN在一起的SQL?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

当你的SQL里出现3张、4张甚至5张表JOIN的时候,面试官很可能追问一个问题:"这些JOIN是按什么顺序执行的?你有没有想过把哪张表放前面?"今天聊聊多表JOIN的执行顺序、优化器的决策逻辑,以及怎么手动调优。

面试原题

写一条SQL关联用户表、订单表、商品表,查出用户姓名、订单金额、商品名称。请说说这条SQL的执行顺序,以及你会怎么优化。

SELECT
  u.user_name,
  o.order_amount,
  p.product_name
FROM user_order o
JOIN dim_user u ON o.user_id = u.user_id
JOIN dim_product p ON o.product_id = p.product_id
WHERE o.dt = '2025-01-01'

SQL写的顺序 ≠ 实际执行顺序

你在SQL里先写的JOIN dim_user再写JOIN dim_product,但实际执行时,优化器会自己决定先JOIN哪张表。

在Spark/Hive中,优化器的决策逻辑大致是:

1. 先看表的大小: 小表优先被处理。如果dim_user有100万行、dim_product有10万行、user_order过滤后有5000万行,优化器可能先拿user_order JOIN dim_product(因为dim_product更小,可能走Broadcast Join),再和dim_user JOIN。

2. 先看过滤条件: WHERE条件会被"下推"(Predicate Pushdown)到尽可能早的阶段执行。o.dt = '2025-01-01'会在扫描user_order表的时候就过滤掉,而不是JOIN完再过滤。

3. 看JOIN类型: INNER JOIN的顺序可以任意调整(结果不变),但LEFT JOIN的顺序不能随便换(左右表有区别)。

查看执行计划

面试的时候说"我会看执行计划来确认",绝对加分。

-- Spark SQL
EXPLAIN EXTENDED
SELECT u.user_name, o.order_amount, p.product_name
FROM user_order o
JOIN dim_user u ON o.user_id = u.user_id
JOIN dim_product p ON o.product_id = p.product_id
WHERE o.dt = '2025-01-01';
 
-- Hive
EXPLAIN
SELECT ...

执行计划会告诉你:哪张表做了Broadcast、哪些JOIN走的Shuffle、过滤条件有没有被下推。看懂执行计划是区分"会写SQL"和"会优化SQL"的分水岭。

多表JOIN的优化原则

原则一:大表放前面,小表放后面(Hive特有)

Hive的老优化器有一个规则:最后一个JOIN的表不会被加载到内存,所以要把最大的表放在最后。但现在Hive的CBO(基于代价的优化器)会自动调整顺序,这个规则的重要性降低了。

不过在写SQL的时候养成"大表驱动"的习惯没有坏处:

-- 推荐:大表在前,小表在后
SELECT ...
FROM big_fact_table f          -- 5000万行
JOIN dim_user u ON f.user_id = u.user_id         -- 100万行
JOIN dim_product p ON f.product_id = p.product_id -- 10万行
JOIN dim_region r ON f.region_id = r.region_id    -- 1000行

原则二:先过滤再JOIN

这个DAY 8讲过,但多表场景下更重要。能在JOIN之前过滤的条件,一定要提前过滤:

-- ❌ 先JOIN再过滤
SELECT ...
FROM user_order o
JOIN dim_user u ON o.user_id = u.user_id
JOIN dim_product p ON o.product_id = p.product_id
WHERE o.dt = '2025-01-01'
  AND u.user_level = 'VIP'
  AND p.category = '电子产品'
 
-- ✅ 子查询提前过滤
SELECT ...
FROM (SELECT * FROM user_order WHERE dt = '2025-01-01') o
JOIN (SELECT * FROM dim_user WHERE user_level = 'VIP') u
  ON o.user_id = u.user_id
JOIN (SELECT * FROM dim_product WHERE category = '电子产品') p
  ON o.product_id = p.product_id

虽然现代优化器(Spark CBO)可能会自动做谓词下推,但手动写清楚有两个好处:一是保险,不依赖优化器;二是代码可读性更好,别人一看就知道每张表用了哪些过滤条件。

原则三:减少JOIN的次数

如果多个维度表的JOIN Key相同,可以先把维度表合并,再和事实表JOIN:

-- ❌ 事实表和3张维度表分别JOIN
SELECT ...
FROM fact_table f
JOIN dim_a a ON f.key = a.key
JOIN dim_b b ON f.key = b.key
JOIN dim_c c ON f.key = c.key
 
-- ✅ 如果dim_a/b/c可以先合并
WITH dim_merged AS (
  SELECT a.key, a.col1, b.col2, c.col3
  FROM dim_a a
  JOIN dim_b b ON a.key = b.key
  JOIN dim_c c ON a.key = c.key
)
SELECT ...
FROM fact_table f
JOIN dim_merged d ON f.key = d.key

小表之间的JOIN很快(数据量小,可能直接在Driver端完成),合并后只和大表做一次Shuffle,比做三次Shuffle快得多。

原则四:避免笛卡尔积

这是新手最容易犯的错误——漏写JOIN条件或者ON条件没写对,导致两张表做笛卡尔积:

-- ❌ 漏写了ON条件
SELECT * FROM table_a, table_b
-- 如果a有10万行,b有10万行,结果就是100亿行
 
-- ❌ ON条件写错,实际变成了笛卡尔积
SELECT *
FROM user_order o
JOIN dim_user u ON 1 = 1  -- 永真条件 = 笛卡尔积

Hive默认不允许没有条件的JOIN(需要设置hive.strict.checks.cartesian.product=false才能跑),但Spark不会报错,直接给你跑出一个巨大的中间结果,然后OOM。

我见过一个同事因为JOIN条件复制粘贴的时候漏改了一个字段名,两张千万级的表做了笛卡尔积,直接把整个集群的YARN队列跑满了,影响了其他人的任务。从那以后我们组定了一条规则:提交SQL之前必须检查每个JOIN的ON条件。

实战场景:我的广告日报SQL

我每天跑的广告日报涉及5张表的关联:

SELECT
  d.dt,
  d.channel,
  d.campaign_id,
  c.campaign_name,
  d.cost,
  d.impressions,
  d.clicks,
  COALESCE(conv.conversions, 0) AS conversions,
  r.region_name
FROM (
  -- 先过滤当天的消耗数据
  SELECT * FROM ad_daily_cost WHERE dt = '${bizdate}'
) d
LEFT JOIN dim_campaign c ON d.campaign_id = c.campaign_id
LEFT JOIN dim_region r ON d.region_id = r.region_id
LEFT JOIN (
  -- 转化数据单独聚合后再JOIN
  SELECT campaign_id, COUNT(1) AS conversions
  FROM conversion_log
  WHERE dt = '${bizdate}'
  GROUP BY campaign_id
) conv ON d.campaign_id = conv.campaign_id

几个优化点:消耗表先按日期过滤;转化日志先聚合再JOIN(避免多对多的JOIN膨胀);维度表直接LEFT JOIN(都是小表,会自动走Broadcast)。

这条SQL从最初跑20分钟优化到2分钟,主要就是靠这三个手段。

面试加分:JOIN的数据膨胀问题

多表JOIN还有一个隐藏问题——行数膨胀。

-- 一个用户有3个订单,一个订单有2个商品
-- user: 1行
-- order: 3行
-- order_item: 6行
SELECT *
FROM dim_user u
JOIN user_order o ON u.user_id = o.user_id
JOIN order_item i ON o.order_id = i.order_id
WHERE u.user_id = 'u001'
-- 结果:6行,用户信息被重复了6次

如果你在这个基础上做聚合(比如SUM(order_amount)),就会因为行数膨胀导致金额被重复累加。

解法:要么分步聚合(先在子查询里聚合好再JOIN),要么用DISTINCT去重后再SUM。这个意识在面试中主动提出来,说明你写多表JOIN的时候会考虑数据完整性。

刷题进度保存在当前浏览器