在使用SQL处理多表关联查询的场景中,我们经常会遇到需要去重的需求,很多开发者习惯直接在查询语句中添加Distinct关键字来实现去重。但实际执行时往往会发现,带有Distinct的关联查询耗时远高于预期,这本质上和SQL的执行顺序以及去重逻辑的底层实现有关。

SQL关联查询与Distinct的基本执行逻辑
要理解Distinct在关联查询中耗时高的原因,首先需要明确SQL语句的默认执行顺序。在标准SQL的执行流程中,JOIN操作会先于SELECT阶段的DISTINCT执行,也就是说默认情况下是先完成多表关联生成临时结果集,再对这个临时结果集进行去重操作。
我们可以通过一个简单的示例来看这个流程,假设我们有两个表:
- 用户表
user,包含字段id、name - 订单表
order,包含字段id、user_id、order_no
如果我们要查询所有下过订单的用户名称,常见的写法如下:
SELECT DISTINCT u.name FROM user u JOIN `order` o ON u.id = o.user_id
这条语句的执行顺序实际是:先执行u JOIN o生成所有用户和订单关联的临时数据,再对这个临时数据的u.name字段去重。
先Join后Distinct的性能问题根源
先执行Join再执行Distinct的耗时问题,主要来自两个层面:临时数据量膨胀和去重计算开销增加。
临时数据量膨胀
多表Join的结果集大小通常是两个表数据量的乘积级别(如果不加过滤条件),如果用户表有1万条数据,订单表有10万条数据,那么Join后的临时结果集可能达到百万甚至千万级别,而实际我们需要去重的用户名称可能只有几千条。在如此大的临时结果集上执行去重,必然会带来大量的IO和内存开销。
去重计算的成本
Distinct的去重逻辑通常需要对目标字段进行排序或者使用哈希表去重,数据量越大,排序和哈希计算的成本就越高。当临时结果集的数据量远超实际需要去重的数据量时,这些计算大部分都是无效开销。
调整执行顺序的优化方案
既然先Join后Distinct的问题出在临时数据量过大,那么优化的核心思路就是先缩小数据范围再去关联,也就是先对需要去重的表做去重,再执行Join操作。
以上面的查询需求为例,我们可以先对订单表的user_id去重,得到所有下过订单的用户ID,再和用户表关联获取用户名称,写法如下:
SELECT u.name
FROM user u
JOIN (
SELECT DISTINCT user_id
FROM `order`
) o ON u.id = o.user_id
这种写法的执行逻辑是:先对订单表的user_id去重,得到只有几千条数据的临时表,再用这个小表和用户表关联,Join的结果集大小只有几千条,去重的计算量也大幅降低。
两种写法的性能对比
我们可以通过执行计划来对比两种写法的性能差异,以下是两种写法的核心指标对比:
| 对比项 | 先Join后Distinct | 先去重再Join |
|---|---|---|
| 临时结果集大小 | 百万级 | 千级 |
| 去重计算数据量 | 百万级 | 十万级(仅订单表去重) |
| 内存占用 | 高 | 低 |
| 执行耗时 | 数秒甚至数十秒 | 毫秒级 |
其他优化建议
除了调整Join和Distinct的顺序,还有几个优化点需要注意:
- 给Join的关联字段和Distinct的字段添加合适的索引,索引可以大幅降低Join和去重的扫描成本
- 如果去重的字段是主键或者唯一键,不需要额外添加Distinct,避免无效计算
- 尽量在Where条件中提前过滤不需要的数据,缩小参与Join的数据范围
在实际开发中,我们需要根据具体的业务场景和数据量,选择合适的查询写法,避免盲目使用Distinct导致关联查询性能下降。