高并发 SQL 瓶颈元凶:被你忽略的 DISTINCT 冗余开销
DISTINCT 明明有 WHERE 条件,为什么还要全表扫描?金仓去重优化的两层刀法
引言:一个看似多余的 DISTINCT,藏着性能陷阱
几乎每个写过 SQL 的人都用过 DISTINCT。它的语义很简单——去掉重复行。但"简单"不等于"快"。
在一个客户的生产环境中,运维团队发现这样一条 SQL:
SELECT DISTINCT status, category
FROM t_orders
WHERE status = 'ACTIVE' AND category = 'ELECTRONICS';
过滤条件已经把结果锁定到了唯一的值组合 (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。这看起来只是换了一种写法,但实际意义在于:
GROUP BY 有更成熟的优化路径。
金仓数据库将 DISTINCT 改写为 GROUP BY 后,可以复用 GROUP BY 已有的优化能力:
- 键值消除:如果目标列上存在唯一索引或主键,优化器可以直接利用索引信息进行键值裁剪,无需扫描全部数据
- 并行执行:GROUP BY 天然支持并行计算,改写后可以享受并行去重的性能红利
- 优化器规则复用:GROUP BY 的优化规则在数据库中积累多年,比 DISTINCT 单独优化的成熟度更高
-- 原始 SQL
SELECT DISTINCT a, b FROM s1;
-- 优化器内部改写(对用户透明)
SELECT a, b FROM s1 GROUP BY a, b;
第二层刀法:LIMIT 1 替代 DISTINCT / GROUP BY
这是更激进也更高效的一层优化。当目标列被常值条件完全固定时,DISTINCT 的去重操作本身就是多余的——结果要么有值(一行),要么没值(零行)。
考虑以下场景:
SELECT DISTINCT a, b FROM s1 WHERE a = 1 AND b = 1;
WHERE 条件已经把 a 和 b 锁死为常量 (1, 1)。即使扫描到 100 条匹配的记录,DISTINCT 之后的结果也只有一行 (1, 1)。所以:
-- 等价改写
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
-- 创建测试表
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;
改写后,优化器可以利用 GROUP BY 已有的键值消除规则,如果 a 或 b 上有索引,直接走索引扫描,避免全表扫描和哈希去重。
实测效果:464ms → 249ms,耗时减少近一半。
场景二:DISTINCT 转 LIMIT 1
-- 场景:查询特定用户的状态(结果唯一)
SELECT DISTINCT user_status, vip_level
FROM t_user
WHERE user_id = 'U10086' AND user_status = 'ACTIVE';
由于 user_status 在 WHERE 中已被固定为 'ACTIVE',vip_level 虽然未被固定,但 user_id 是主键,整个结果集最多只有一行。优化器将其改写为:
SELECT user_status, vip_level
FROM t_user
WHERE user_id = 'U10086' AND user_status = 'ACTIVE'
LIMIT 1;
实测效果:30ms → 0.03ms,提速 1000 倍。
场景三:复杂场景组合优化
-- 复杂场景:多条件 + 子查询
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:
-- 优化器改写后
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 查看执行计划,对比优化前后的差异:
-- 查看原始执行计划
EXPLAIN (ANALYZE, BUFFERS)
SELECT DISTINCT a, b FROM s1 WHERE a = 1 AND b = 1;
-- 优化后应看到 LIMIT 节点,且扫描行数显著减少
如果执行计划中出现了 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 替代策略只在以下条件下生效:
- 目标列被 WHERE 条件中的常值完全固定——比如
WHERE a = 1 AND b = 2 - 不涉及聚合函数或窗口函数——这些会改变结果的基数
- 不包含 ORDER BY 与 LIMIT 语义冲突的子句
实测效果汇总
| 优化策略 | 原始耗时 | 优化后耗时 | 性能提升 |
|---|---|---|---|
| DISTINCT → GROUP BY | 464ms | 249ms | 1.86x |
| DISTINCT → LIMIT 1 | 30ms | 0.03ms | 1000x |
| 复杂场景组合 | 12ms | 0.08ms | 150x |
与同类产品的对比:
| 特性 | 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 为什么还要全表去重";第二,了解这些优化的边界,在关键查询中主动采用更高效的写法。毕竟,最好的优化是你写什么,数据库都能理解你的意图。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)