开发过程中有些细节容易被忽略,今天挑几个重点聊一聊。
引言:有 WHERE 条件,为什么 DISTINCT 仍可能很重?
DISTINCT 的语义很简单:去掉重复行。但在执行层面,它可能意味着排序、哈希聚合或额外的去重步骤。
举个例子:
SELECT DISTINCT status, category
FROM t_orders
WHERE status = 'ACTIVE'
AND category = 'ELECTRONICS';
从语义上看,status 和 category 已经被常值条件固定,最终结果最多只有一种组合:(ACTIVE, ELECTRONICS)。如果执行计划仍然先扫描大量数据再做去重,就会产生不必要的开销。
这类查询优化的关键,不是简单地“加速 DISTINCT”,而是识别 DISTINCT 在当前语义下是否仍然必要。
一、优化思路:从去重到结果基数判断
1.1 DISTINCT 改写为 GROUP BY
SELECT DISTINCT a, b FROM t 在语义上等价于:
SELECT a, b
FROM t
GROUP BY a, b;
这个改写的意义在于,优化器可以复用 GROUP BY 的成熟路径,举个例子:
- 使用已有索引减少排序或哈希成本;
- 利用并行聚合能力;
- 结合唯一约束或主键判断结果基数。
-- 原始 SQL
SELECT DISTINCT a, b FROM s1;
-- 语义等价形式
SELECT a, b FROM s1 GROUP BY a, b;
1.2 DISTINCT 改写为 LIMIT 1
当目标列被常值条件完全固定时,去重操作本身可能是多余的。
SELECT DISTINCT a, b
FROM s1
WHERE a = 1 AND b = 1;
由于 a 和 b 都已固定,结果要么是 (1, 1),要么没有行。这样一来可以等价搞懂为:
SELECT a, b
FROM s1
WHERE a = 1 AND b = 1
LIMIT 1;
这种写法的优势是:找到第一条匹配记录后即可停止,不必继续扫描并去重。
改写策略适用条件主要收益DISTINCT -> GROUP BY通用去重场景复用聚合与索引优化路径DISTINCT -> LIMIT 1结果列被常值或唯一条件约束避免不必要的完整去重
二、代码示例
2.1 DISTINCT 转 GROUP BY
CREATE TABLE s1 (
id INT PRIMARY KEY,
a INT,
b VARCHAR(20),
c DATE
);
SELECT DISTINCT a, b
FROM s1
WHERE c >= '2026-01-01'
AND c < '2026-04-01';
可以等价改写为:
SELECT a, b
FROM s1
WHERE c >= '2026-01-01'
AND c < '2026-04-01'
GROUP BY a, b;
如果 a、b 或过滤列上有合适索引,执行计划可能减少排序、哈希或扫描成本。实际效果需要结合执行计划验证。
2.2 DISTINCT 转 LIMIT 1
SELECT DISTINCT user_status, vip_level
FROM t_user
WHERE user_id = 'U10086'
AND user_status = 'ACTIVE';
如果 user_id 是主键,查询结果最多只有一行,DISTINCT 没有实际意义。可以写成:
SELECT user_status, vip_level
FROM t_user
WHERE user_id = 'U10086'
AND user_status = 'ACTIVE'
LIMIT 1;
在这种情况下,去掉 DISTINCT 能让 SQL 意图更明确,也避免优化器误判。
2.3 子查询场景
SELECT DISTINCT t1.status
FROM t_order t1
WHERE t1.order_id IN (
SELECT order_id
FROM t_payment
WHERE pay_status = 'PAID'
)
AND t1.status = 'SHIPPED';
由于 t1.status 已被常值固定,结果列只有一种可能值。如果业务只需要判断是否存在记录,可以改为:
SELECT t1.status
FROM t_order t1
WHERE t1.order_id IN (
SELECT order_id
FROM t_payment
WHERE pay_status = 'PAID'
)
AND t1.status = 'SHIPPED'
LIMIT 1;
如果只是做存在性判断,更建议直接使用 EXISTS:
SELECT EXISTS (
SELECT 1
FROM t_order t1
WHERE t1.order_id IN (
SELECT order_id
FROM t_payment
WHERE pay_status = 'PAID'
)
AND t1.status = 'SHIPPED'
);
三、如何验证优化是否生效
使用执行计划观察是否还存在不必要的去重节点:
EXPLAIN (ANALYZE, BUFFERS)
SELECT DISTINCT a, b
FROM s1
WHERE a = 1 AND b = 1;
重点关注:
1. 是否出现 HashAggregate、Sort、Unique 等去重节点;
2. 是否出现 Limit 节点;
3. 实际扫描行数是否显著小于全表行数;
4. 是否走到了合适的索引。
四、最佳实践
场景建议写法原因主键或唯一键查询去掉 DISTINCT结果天然唯一SELECT 列全部被常值固定使用 LIMIT 1找到第一条即可返回只判断是否存在使用 EXISTS语义更准确需要真实去重保留 DISTINCT 或使用 GROUP BY避免错误改写大表去重结合索引、统计信息和执行计划验证去重成本可能很高
需要注意的限制
LIMIT 1 替代 DISTINCT 通常需要满足以下条件:
1. 目标列被常值条件、唯一键或主键约束限定;
2. 不涉及会改变结果基数的聚合或窗口函数;
3. 不存在与 ORDER BY、分页或业务排序相关的语义冲突;
4. 调用方确实只需要一条代表性结果。
五、性能数据的表达方式
这类优化在特定数据分布下可能十分明显,但不宜把单次测试数字写成通用承诺。建议用以下方式描述:
在目标列被常值固定、且匹配记录较早命中的场景中,LIMIT 1 可以显著减少扫描和去重成本。实际收益取决于数据分布、索引、统计信息和执行计划。
如果文章需要保留测试结果,应补充测试环境、数据量、索引结构和执行计划截图。
总结
DISTINCT 不只是一个语法关键字,它可能引入排序、哈希或唯一化操作。优化这类 SQL 的关键,是判断“是否真的需要去重”。
当结果天然唯一、目标列被常值固定,或业务只是判断是否存在时,可以考虑去掉 DISTINCT,改用 LIMIT 1 或 EXISTS。这样既能让 SQL 语义更清晰,也能减少不必要的执行成本。
这篇笔记就先到这里,后面用到新的思路或者发现有问题再补充。
评论 (0)
暂无评论