SQL语句为什么要避免使用join
文章目录
- 前言
- 一、为什么要减少使用join
- 1.带来的性能消耗更高
- 1.1.驱动表无索引的复杂度分析
- 1.2.被驱动表无索引的复杂度分析
- 1.3.总时间复杂度对比
- 2.多表 JOIN 加剧数据库稳定性风险
- 2.1.锁竞争加剧,影响并发性能
- 2.2.优化器决策难度增加,易出 “烂执行计划”
- 2.3.故障排查难度高
- 二、如何在减少 JOIN 的同时满足业务需求
- 1.数据冗余
- 2.宽表设计
- 3.分步骤查询
前言
在SQL优化中,减少“join”是一个优化方向。本文会给大家介绍为什么要减少join及优化措施。
一、为什么要减少使用join
1.带来的性能消耗更高
数据库执行 JOIN 时,会将多个表的记录进行 “关联匹配”,这个过程比单表查询复杂得多。可以把进行关联匹配的两张表看作驱动表和被驱动表,执行过程中被驱动表是否使用索引性能差别极大。
在分析前,先明确以下前提(基于数据库最常用的嵌套循环 JOIN算法,适用于中小表关联,也是优化器默认选择):
- 驱动表为 T1,总数据量为 N,经 WHERE 筛选后的数据量为 M(M ≤ N);
- 被驱动表为 T2,总数据量为 K;
- 索引查询的时间复杂度为 O(log K)(B + 树索引,查询单条数据的平均复杂度);
- 全表扫描的时间复杂度为 O(K)(需遍历表中所有记录)。
- 嵌套循环 JOIN 的核心流程:1 次驱动表扫描 + M 次被驱动表匹配(每 1 条驱动表记录对应 1 次被驱动表查询)。
1.1.驱动表无索引的复杂度分析
驱动表的核心作用是 “生成外层循环的查询条件”,其扫描过程仅执行 1 次,无索引的影响仅体现在 “筛选阶段的全表扫描”,不随后续流程放大。
驱动表的复杂度分为 “筛选阶段” 和 “后续无影响阶段”:
- 筛选阶段(获取 M 行数据):
有索引:若筛选条件(如 T1.region = ‘北京’)有索引,复杂度为 O(log N + M)(索引定位到符合条件的范围,再读取 M 行数据);
无索引:需全表扫描 T1 筛选出 M 行,复杂度为 O(N)(遍历所有 N 行,判断是否符合条件)。 - 后续阶段(驱动被驱动表):驱动表筛选出 M 行后,后续仅需传递 M 个关联字段值(如 user_id),无额外计算,复杂度为 O(1)(固定成本)。
- 总时间复杂度:驱动表无索引时,仅筛选阶段复杂度从 O(log N + M) 上升到 O(N),但 O(N) 是固定值(仅执行 1 次),不随 M(驱动表筛选后行数)或 K(被驱动表数据量)增长。
1.2.被驱动表无索引的复杂度分析
被驱动表的核心作用是 “响应 M 次匹配请求”,无索引的影响会被 M 倍放大 —— 每 1 次匹配都需全表扫描,复杂度随 M 和 K 同步增长,是 JOIN 性能恶化的核心原因。
被驱动表的复杂度集中在 “M 次匹配阶段”,直接决定总复杂度:
有索引:每次匹配通过索引查找,单次复杂度为 O(log K),M 次总复杂度为 O(M × log K);
无索引:每次匹配需全表扫描,单次复杂度为 O(K),M 次总复杂度为 O(M × K)。
1.3.总时间复杂度对比
| 场景 | 被驱动表匹配阶段复杂度 | JOIN 总复杂度(含驱动表 | 增长趋势 |
|---|---|---|---|
| 被驱动表有索引 | O(M × log K) | O(N) + O(M × log K) | 随 M 线性增长,随 K 对数增长 |
| 被驱动表无索引 | O(M × K) | O(N) + O(M × K) | 随 M 和 K 均线性增长(指数级恶化) |
M和K都取决于数据和业务,不是我们可以控制的,但是我们可以给被驱动表要扫描的列加上索引来优化性能。
2.多表 JOIN 加剧数据库稳定性风险
2.1.锁竞争加剧,影响并发性能
JOIN 查询通常需要扫描多个表的大量数据,过程中会持有表锁或行锁的时间更长(尤其是写操作并发时),期间被扫描的行 / 表会被锁定,其他写请求(如UPDATE/DELETE)需排队等待,导致并发吞吐量下降,甚至引发死锁。
2.2.优化器决策难度增加,易出 “烂执行计划”
数据库优化器(如 MySQL 的 Cost-Based Optimizer)会根据表数据量、索引、统计信息生成 “最优执行计划”,但表数量越多(JOIN 越多),优化器的决策复杂度越高.
多表 JOIN 时,优化器需评估所有可能的 “表关联顺序”(n 表 JOIN 时有 n! 种顺序),若统计信息不准确(如数据更新后未及时ANALYZE TABLE),易选择低效的关联顺序;
极端情况下,多表 JOIN 可能导致优化器 “超时”,直接采用默认的低效计划,进一步恶化性能。
2.3.故障排查难度高
当一个包含 5 张表 JOIN 的查询执行缓慢时,排查定位问题的难度远高于单表查询。需要逐一验证 “关联字段是否有索引”“驱动表选择是否合理”“中间结果集是否过大”“是否有隐式转换” 等,而单表查询只需重点排查索引和 SQL 写法即可。
二、如何在减少 JOIN 的同时满足业务需求
1.数据冗余
可以把将高频关联字段冗余到主表。
例如:订单表(orders)高频需要 “用户名”(来自用户表user),可在orders表中新增user_name字段,在用户注册 / 改名时同步更新该字段。
优点:彻底避免 “订单 - 用户” 的 JOIN,查询速度极快;
注意:需保证冗余字段的一致性(比如说更改用户表user的“用户名”时需要同步更改订单表中冗余的user_name字段,可通过触发器、消息队列同步),适合读多写少场景。
2.宽表设计
可以提前预聚合多表数据。对于报表、统计类查询(如 “按地区统计订单量”),可通过 ETL 工具(如 Flink、DataX)将 “订单表 + 用户表 + 地区表” 的关联数据预聚合为一张宽表(如order_stat_by_region),查询时直接查宽表,无需 JOIN。
优点:查询性能提升 10 倍以上,适合非实时统计场景。
3.分步骤查询
可以用应用层关联替代SQL语句的“join”。对于数据量极大的多表关联(如 3 张表均超 1000 万行),可将查询拆分为 2 步:
- 先查主表(如订单表),获取关键 ID(如user_id);
- 用IN语句批量查询关联表(如SELECT * FROM user WHERE id IN (1,2,3…));
- 在应用层(如 Java 代码)用 HashMap 将结果关联。
优点:避免数据库处理大表 JOIN 的高消耗,且批量查询效率更高。
