LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

为什么阿里等大厂禁止使用三表以上的JOIN?3种替代方案

admin
2026年4月17日 14:59 本文热度 58

你开发时有没有过把三四张表一股脑 JOIN 在一起的经历,反正我是有过的,本地开发测试时跑得飞快,一上线就慢查询告警刷屏,甚至可能直接把业务干崩了。一查发现是自己写的多表JOIN惹的祸,觉得冤不冤?

我们原来公司要求每个人都要学习阿里的java开发规范,还要求每人都要考过阿里的考试,我记得当时还要19.9一次。阿里的 Java 开发规范有一条 "禁止超过三张表进行 JOIN 操作" 。估计不少人都当 "教条" 看过,但其实这条规范背后,是阿里在亿级流量下踩过无数坑才总结出的经验。今天咱们就从 MySQL 执行引擎的底层逻辑聊起,把这条规范的底裤都扒出来,顺便看看遇到多表关联场景该咋解决。

一个案例看性能灾难现场

先看个电商场景的血泪案例:某平台的早年的一个程序员写的订单查询接口,为了一次性返回订单、用户、商品、仓库的全部信息,写了这么一段 SQL:

SELECT o.*, u.name, u.phone, p.product_name, w.warehouse_name
FROM
 orders o
JOIN
 users u ON o.user_id = u.user_id
JOIN
 products p ON o.product_id = p.product_id
JOIN
 warehouses w ON o.warehouse_id = w.id
WHERE
 o.status = 1;

看起来好像没啥问题是吧?但上线后直接炸了:

  • • 单次查询耗时 800ms+,远超业务要求的 200ms 阈值
  • • 高峰期数据库 CPU 直接飙到 90%多,慢查询告警刷不停
  • • 最后排查发现,MySQL 优化器在处理四表 JOIN 时,错误选了 warehouses 表当驱动表,导致本该走索引的查询直接退化成全表扫描,一下子扫了上百万行数据

为啥会这样?咱们得从 MySQL 的 JOIN 底层逻辑说起。

MySQL 多表 JOIN 的底层坑

2.1 JOIN 算法的先天不足

你可能不知道,MySQL 在 8.0.18 版本之前,只支持嵌套循环类 JOIN 算法,没有 Hash Join 和 Sort-Merge Join 这些更高效的算法。这三种嵌套循环算法的问题在哪呢?

  • • Simple Nested-Loop Join:暴力双循环,驱动表每一行都要去匹配被驱动表的所有行,时间复杂度 O (m * n),基本没用 。流程大致如图:
  • • Block Nested-Loop Join:把驱动表批量加载到 JOIN Buffer 里再匹配(流程如下图),但还是 O (m * n) 的复杂度,没索引的大表关联时慢到离谱。
  • • Index Nested-Loop Join:这是相对最优的算法,利用被驱动表的索引减少扫描,复杂度 O (m * log n),但前提是关联字段必须有索引。我们看看算法流程:
  • 可关键问题是当表的数量超过 3 张时,哪怕用的是最优的 Index Nested-Loop Join,性能也会呈指数级下降。

2.2 优化器的决策困境

从 MySQL 的源码来看,优化器在处理多表 JOIN 时,需要先确定表的连接顺序,而可能的连接顺序是阶乘级增长的:

  • • 3 张表:3! = 6 种可能
  • • 4 张表:4! = 24 种可能
  • • 5 张表:5! = 120 种可能

MySQL 的优化器并没有足够的时间去穷举所有可能,它用的是贪心算法—— 每次选择当前看起来最优的连接顺序,而不是全局最优。当表的数量超过 3 张后,这种局部最优的选择很容易掉进坑里,就像刚才那个案例一样,选错了驱动表直接导致全表扫描。

而且优化器的决策严重依赖表的统计信息,如果统计信息过时或者不准,那结果就更不可控了。我经常会碰到,表数据变动了(增删了),但统计信息没更新,优化器还是按原来的数据量选驱动表,结果查询直接崩了。

2.3 内存消耗与临时表的噩梦

多表 JOIN 还有个更隐蔽的问题:中间结果集的内存消耗

每一次 JOIN 都会生成一个中间结果集,MySQL 会先尝试把这个结果集放在内存里的 JOIN Buffer 里,但如果结果集太大,就会生成临时表写到磁盘上。你想想,3 张表 JOIN 的中间结果集可能就有几十万行,再 JOIN 第四张表,数据量直接指数级膨胀,临时表能占几个 G 的磁盘空间,IO 直接拉满,整个数据库都会变慢。

分布式架构下,多表 JOIN难题

像阿里这类业务基本都是分库分表的分布式架构,这时候多表 JOIN 的问题就更严重了:

3.1 跨节点数据传输爆炸

在分布式数据库里,要避免网络间大量数据传输,而JOIN 需要跨节点、跨分片传输数据。比如订单表在 A 节点,用户表在 B 节点,仓库表在 C 节点,那多表 JOIN 时,数据要在 A、B、C 之间来回传输,网络直接成了瓶颈。

我们曾经内部的实测数据显示,单分片查询只需要 25ms,跨分片四表 JOIN 直接涨到 1200ms(好在不是线上,否则直接要成盒了),网络流量更是从 5KB 涨到了 120MB,差了 24000 倍!

3.2 执行计划完全失控

分库分表中间件比如 DRDS,很难把复杂的多表 JOIN 下推到存储节点,只能在中间层拉取所有数据再做 JOIN,这不仅慢,而且很容易因为数据量太大直接 OOM。

还有更要命的问题--分布式事务问题。多表 JOIN 如果涉及到跨库修改,那分布式事务的一致性问题会让你头大,性能也会掉一半以上。

常见的三种替代解决方案

既然多表 JOIN 这么坑,那遇到需要多表关联的场景该咋办?通常有三个实用的解决方案。

4.1 分步查询 + 应用层组装(最常用)

思路简单直接粗暴,既然JOIN走不通,那就把复杂的多表 JOIN 拆成多次单表查询,然后在应用层把数据组装起来。比如刚才那个订单查询的场景,可以这么改:

// 1. 先查订单主表数据
List<Order> orders = orderDao.query("SELECT * FROM orders WHERE status=1 LIMIT 100");

// 2. 提取关联的用户ID、商品ID,去重

Set<Long> userIds = orders.stream().map(Order::getUserId).collect(Collectors.toSet());
Set<Long> productIds = orders.stream().map(Order::getProductId).collect(Collectors.toSet());

// 3. 批量查询用户和商品数据

Map<Long, User> userMap = userDao.queryByIds(userIds).stream()
        .collect(Collectors.toMap(User::getId, Function.identity()));
Map<Long, Product> productMap = productDao.queryByIds(productIds).stream()
        .collect(Collectors.toMap(Product::getId, Function.identity()));

// 4. 应用层组装数据

orders.forEach(order -> {
    order.setUserName(userMap.get(order.getUserId()).getName());
    order.setProductName(productMap.get(order.getProductId()).getName());
});

这种做法有什么好处呢?

  • • 每个查询都是单表查询,充分利用索引,性能最稳定;
  • • 可以利用缓存,比如用户信息可以存在 Redis 里,不用每次都查数据库;
  • • 分步查询可以并行执行,减少整体响应时间;
    这种方式也是有缺点的,就是增加了应用层的代码量,复杂的逻辑从数据库层上提到了应用层,但相比数据库性能爆炸,也算是一种比较好的解法,也是最常见的一种,在我们过去中应用时最多的一种方式。相对来说复杂一点,代码量多一点根本不算啥,毕竟技术是为业务服务,不管黑猫白猫,抓到老鼠(解决问题)就是好猫。

4.2 冗余字段,一种空间换时间方式

如果是高频查询的场景,比如订单列表要显示商品名称、用户昵称这种不常变的字段,那可以直接把这些字段冗余到订单表里,彻底避免 JOIN。这也是我们很常用的一种方式,但是要适度,相对来说更克制一些,因为如果处理不好很容易带来数据不一致的。

比如修改订单表结构:

CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  user_id BIGINT,
  product_id BIGINT,
  product_name VARCHAR(100),  -- 冗余商品名称
  user_name VARCHAR(100),     -- 冗余用户昵称
  order_time DATETIME,
  status TINYINT,
  INDEX idx_user_id (user_id)
);

但冗余字段要注意三个原则:

  • • 只冗余高频查询、低频变更的字段,比如商品名称可以,但商品库存就不行,因为库存变太频繁
  • • 要保证数据一致性,比如可以用Binlog变更监听(比如Canal)方式更新表字段,异步更新订单表的冗余字段
  • • 写 QPS 高的业务要慎用,因为每次修改都要同步更新冗余字段,会增加写的开销

4.3 物化视图或者汇总表

这种通常是报表场景。如果是实时性要求不高的报表、分析场景,那可以用物化视图或者汇总表,提前把多表关联的结果计算好,查询的时候直接查预计算的表就行。这种其实还是很常见的,只要实时性要求没那么高,很多都是通过大数据跑批的方式形成大宽表供业务使用。

比如创建一个订单详情的物化视图:

CREATE MATERIALIZED VIEW order_detail_view 
AS

SELECT
 o.id, o.order_time, u.name, p.product_name, o.amount
FROM
 orders o 
JOIN
 users u ON o.user_id = u.user_id
JOIN
 products p ON o.product_id = p.product_id
WHERE
 o.status = 1
WITH
 DATA;

然后定时刷新这个物化视图,比如每小时刷新一次,查询的时候直接查这个视图就行,响应时间能从几百 ms 降到几 ms。

这三个方案也不是互斥的,是对应不同场景的替代。可以都用,也可以选择性的用。

什么时候可以打破规则?

当然,这条规范也不是绝对的,毕竟规矩是死的,人是活的,只要知道原理,还是可以适当利用的。其实这三种场景可以适当放宽的:

  1. 1. 小表关联小表:比如都是小于 1000 行的配置表,JOIN 个四五张也没事,因为数据量太小,性能损耗可以忽略。
  2. 2. NewSQL 数据库:比如 TiDB、PolarDB-X 这些分布式数据库,支持分布式 Hash Join,多表 JOIN 的性能比 MySQL 好很多。这些NewSQL 数据库从底层架构就跟MySQL这种关系型数据库完全不同,并且目标场景也不一样,所以不完全适用。
  3. 3. OLAP 分析场景:比如 ClickHouse、Hive 这些数仓,本身就是为多表关联设计的,星型模型、雪花模型里十几张表 JOIN 都是常规操作。这个本身就是场景不一样了、数据库也不一样了。

规范不是死守禁令

不过规范也不是什么金科玉律。其实禁止三表以上 JOIN,本质上是架构思维的转变

  • • 从 "让数据库做所有事情",到 "数据库只负责存储和简单查询,复杂逻辑交给应用层"
  • • 从 "追求实时一致性",到 "接受最终一致性,用空间换时间"
  • • 从 "单体数据库思维",到 "分布式架构思维"

其实哪怕是允许的双表 JOIN,也要记住三个原则:

  • • 小表驱动大表
  • • 关联字段必须有索引
  • • 不要用 SELECT * ,只查需要的字段

大厂的规范从来不是拍脑袋想出来的,都是无数坑踩过后才总结出来的经验。以后看到这种规范时,多问几个为什么,多想想背后的原因,自然就有答案了,知其然知其所以然。

 


阅读原文:https://mp.weixin.qq.com/s/S6Mo-H3MoSgiaAUzlOtQNg


该文章在 2026/4/17 16:19:49 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-1  粤公网安备44030602007207号