DISTINCT 的“惯性陷阱“:当去重操作沦为性能累赘

写 DISTINCT 时觉得理所当然,执行计划却告诉你------它在笨拙地扫描整张表,哪怕 WHERE 已经把答案锁死了。


引子:一个"本该瞬间完成"的慢查询

生产环境告警,运维团队定位到这条 SQL:

sql 复制代码
SELECT DISTINCT status, category
FROM t_orders
WHERE status = 'ACTIVE' AND category = 'ELECTRONICS';

WHERE 条件已经把结果框死在了 (ACTIVE, ELECTRONICS) 这一个组合上。但执行计划毫不留情:全表扫描、排序或哈希去重,流程一个不落。30ms 的耗时,在高并发下成了明显的瓶颈。

为什么?因为传统优化器看到 DISTINCT 就条件反射------走"扫描 → 去重"的标准套路,完全不管 WHERE 条件已经把目标列的值钉死了。

金仓数据库 V9R4C19 对 DISTINCT 做了两层深度优化,把这套"机械套路"升级成了"智能判断"。本文拆解这两层刀法怎么出招、效果如何。


一、核心原理:两层刀法,由浅入深

第一刀:DISTINCT 换皮 GROUP BY

SELECT DISTINCT a, b FROM t 语义上等价于 SELECT a, b FROM t GROUP BY a, b。表面是换种写法,实质是借道更成熟的优化体系

金仓把 DISTINCT 改写为 GROUP BY 后,直接继承 GROUP BY 的优化家底:

  • 键值消除:目标列上有唯一索引或主键?优化器直接拿索引信息做裁剪,不用碰全表
  • 并行加速:GROUP BY 天然支持并行计算,改写后能蹭上并行去重的红利
  • 规则复用:GROUP BY 的优化规则在数据库里打磨多年,比 DISTINCT 单打独斗成熟得多
sql 复制代码
-- 原始 SQL
SELECT DISTINCT a, b FROM s1;

-- 优化器内部自动改写
SELECT a, b FROM s1 GROUP BY a, b;

第二刀:LIMIT 1 直接掀桌

这是更激进的一刀。当 WHERE 条件用常量把目标列彻底焊死时,DISTINCT 的去重本身就是多余的------结果要么有一行,要么空集。

看场景:

sql 复制代码
SELECT DISTINCT a, b FROM s1 WHERE a = 1 AND b = 1;

WHERE 已经把 ab 锁成 (1, 1)。哪怕扫到 100 条匹配记录,DISTINCT 完也只有一行 (1, 1)。所以:

sql 复制代码
-- 等价改写
SELECT a, b FROM s1 WHERE a = 1 AND b = 1 LIMIT 1;

威力在于:找到第一条匹配记录就能收工。数据分布均匀的话,扫描量从"全表"暴跌到"碰第一个匹配项"。

改写策略 适用场景 核心收益
DISTINCT → GROUP BY 通用场景 蹭 GROUP BY 的键值消除和并行能力
DISTINCT → LIMIT 1 目标列被 WHERE 常量完全固定 找到第一条即停,极致加速

二、实战验证:从毫秒到微秒

场景一:DISTINCT 转 GROUP BY

sql 复制代码
-- 测试表
CREATE TABLE s1 (
    id INT PRIMARY KEY,
    a INT,
    b VARCHAR(20),
    c DATE
);

-- 查某时间段内不重复的 (a, b) 组合
SELECT DISTINCT a, b
FROM s1
WHERE c >= '2026-01-01' AND c < '2026-04-01';

优化器内部改写为:

sql 复制代码
SELECT a, b
FROM s1
WHERE c >= '2026-01-01' AND c < '2026-04-01'
GROUP BY a, b;

改写后,若 ab 上有索引,直接走索引扫描,跳过全表扫描和哈希去重。

实测:464ms → 249ms,耗时砍半。

场景二:DISTINCT 转 LIMIT 1

sql 复制代码
-- 查特定用户状态(结果唯一)
SELECT DISTINCT user_status, vip_level
FROM t_user
WHERE user_id = 'U10086' AND user_status = 'ACTIVE';

user_status 被 WHERE 钉死为 'ACTIVE'user_id 是主键,结果集最多一行。优化器直接改写为:

sql 复制代码
SELECT user_status, vip_level
FROM t_user
WHERE user_id = 'U10086' AND user_status = 'ACTIVE'
LIMIT 1;

实测 :30ms → 0.03ms,提速 1000 倍

场景三:复杂场景组合拳

sql 复制代码
-- 多条件 + 子查询
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 被 WHERE 固定为 'SHIPPED',DISTINCT 等价于 LIMIT 1:

sql 复制代码
-- 改写后
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;

实测:12ms → 0.08ms,提速 150 倍。


三、验证优化是否生效

EXPLAIN 看执行计划,对比前后差异:

sql 复制代码
EXPLAIN (ANALYZE, BUFFERS)
SELECT DISTINCT a, b FROM s1 WHERE a = 1 AND b = 1;

优化生效的标志:执行计划中出现 Limit 节点,且 Actual Rows 只有一行。


四、最佳实践:写 SQL 时换个脑子

旧思维 新思维
DISTINCT 就是去重,写了就完事 DISTINCT 可能藏性能雷,想想有没有更高效的招
有 WHERE 过滤,DISTINCT 应该快 WHERE 把列值钉死时,DISTINCT 纯属多余
全靠数据库自动优化 摸清优化边界,复杂场景手动改写更稳

场景速查表

你的场景 推荐写法 原因
结果确定唯一(如主键查询) 去掉 DISTINCT,或加 LIMIT 1 去重操作多余
WHERE 常量固定了所有 SELECT 列 LIMIT 1 替代 DISTINCT 找到第一个就够
需要去重但不确定结果唯一性 保持 DISTINCT 或改 GROUP BY GROUP BY 并行能力更强
大表 + 索引列去重 GROUP BY + 索引 避开哈希去重的内存开销

LIMIT 1 替代的红线

以下情况不适用:

  1. 目标列没被 WHERE 常量完全固定 ------比如 WHERE a > 1 这种范围条件
  2. 涉及聚合或窗口函数------这些会改变结果基数
  3. 包含与 LIMIT 语义冲突的 ORDER BY------排序后再 LIMIT 会扭曲结果

五、实测数据与竞品对标

优化策略 原始耗时 优化后耗时 提升倍数
DISTINCT → GROUP BY 464ms 249ms 1.86×
DISTINCT → LIMIT 1 30ms 0.03ms 1000×
复杂场景组合 12ms 0.08ms 150×
特性 KingbaseES DM v8
DISTINCT 转 GROUP BY 支持 支持
DISTINCT 转 LIMIT 1 支持 不支持

结语

金仓数据库 V9R4C19 对 DISTINCT 的两层优化,本质是把"无脑去重"升级为"结果预判":

  • 第一刀(DISTINCT → GROUP BY):借道 GROUP BY 的成熟优化体系,通用场景耗时减半
  • 第二刀(DISTINCT → LIMIT 1):WHERE 把结果焊死时,用 LIMIT 1 直接掀掉去重流程,极端场景提速 1000 倍

对开发者和 DBA 的意义:第一,写 DISTINCT 不用再担心"明明 WHERE 已经锁死了为什么还要全表去重";第二,摸清这些优化的边界,在关键查询里主动选择更高效的写法。

毕竟,最理想的优化是------你写什么,数据库都能读懂你的真实意图

相关推荐
雪宫街道2 小时前
SpringBoot 向 IOC 容器注册组件的两种姿势:@Configuration 与 @Import
java·spring boot·后端·spring
techdashen2 小时前
Cargo 1.94 开发周期全解析
开发语言·后端·rust
枕星而眠2 小时前
Linux守护进程完全指南:从原理到实战
linux·运维·服务器·c++·后端
金融支付架构实战指南3 小时前
Milvus 向量检索服务 + SpringBoot 实战:电商商品语义检索与相似商品推荐
spring boot·后端·milvus·向量检索
齐 飞3 小时前
JDK21虚拟线程
java·后端
fox_lht3 小时前
15.4.循环和迭代器的性能比较
开发语言·后端·学习·rust
摇滚侠3 小时前
SpringMVC 入门到实战 HttpMessageConverter 65-74
java·后端·spring·intellij-idea
Csvn4 小时前
用户与权限管理 — 从创建到精细化管控
后端
金銀銅鐵4 小时前
用 Tkinter 实现简单的论语第一章阅读器
后端·python