DISTINCT 查询优化:从去重语义到 LIMIT 1 改写

开发过程中有些细节容易被忽略,今天挑几个重点聊一聊。

引言:有 WHERE 条件,为什么 DISTINCT 仍可能很重?

DISTINCT 的语义很简单:去掉重复行。但在执行层面,它可能意味着排序、哈希聚合或额外的去重步骤。

举个例子:

SELECT DISTINCT status, category
FROM t_orders
WHERE status = 'ACTIVE'
  AND category = 'ELECTRONICS';

从语义上看,statuscategory 已经被常值条件固定,最终结果最多只有一种组合:(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;

由于 ab 都已固定,结果要么是 (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;

如果 ab 或过滤列上有合适索引,执行计划可能减少排序、哈希或扫描成本。实际效果需要结合执行计划验证。

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. 是否出现 HashAggregateSortUnique 等去重节点;
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 1EXISTS。这样既能让 SQL 语义更清晰,也能减少不必要的执行成本。


这篇笔记就先到这里,后面用到新的思路或者发现有问题再补充。

评论 (0)

暂无评论