首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >SQL 里最危险的"正确"写法:"加个 DISTINCT 保险一点"——这句话有多危险

SQL 里最危险的"正确"写法:"加个 DISTINCT 保险一点"——这句话有多危险

作者头像
PawSQL
发布2026-06-17 20:45:10
发布2026-06-17 20:45:10
790
举报

在 SQL 开发中,DISTINCT 是使用频率极高的关键字,但它也是最容易被误用的特性之一。本文从数据建模的本质出发,深入探讨对多列(尤其含小数类型)使用DISTINCT 去重的根本错误、潜在危害,以及正确的替代方案。

目录

  1. DISTINCT去重去的是什么?大多数人从一开始就想错了
  2. 浮点数的精度陷阱:相同的金额,数据库认为它们不同
  3. 加一个 DISTINCT,埋三个隐患
  4. "加个 DISTINCT 保险一点"——这句话有多危险
  5. 停止用 DISTINCT 去重,试试这几种正确写法

一、去重去的是什么?大多数人从一开始就想错了

去重的对象应该是业务实体,而不是度量指标。这个看似简单的原则,正是大量 DISTINCT 误用的根源。

在关系数据库的理论基础中,每张表都在描述某种业务实体(Entity)——订单表描述订单,用户表描述用户,支付记录表描述一次支付行为。每个实体都有其唯一标识符(主键),而金额、税率、折扣这类字段是该实体的度量属性(Measure),它们描述实体但不定义实体。

两条记录是否"相同",应由业务主体的唯一标识决定,而非由其属性值碰巧相等来判断。用 DISTINCT 去重本质上是在说"属性值完全相同 = 同一个实体"——这是一个在逻辑上就站不住脚的假设。

考虑这个典型场景:

❌ 语义错误的写法

代码语言:javascript
复制
SELECT DISTINCT
 user_id, amount, status 
FROM
 orders;

这条 SQL 隐含了一个危险假设:如果两条订单记录的 user_id、amount、status 完全相同,它们就是同一条订单。但现实中存在两种截然不同的情况:

场景

数据状态

正确操作

DISTINCT 的处理

同一用户支付两笔相同金额的独立订单

正常数据

保留两条

错误合并

ETL 管道重复写入同一条订单

数据冗余

去重保留一条

结果正确(但原因是巧合)

DISTINCT 无法区分这两种场景,它只是机械地比较列值,将"碰巧所有属性相同"的行合并——有时结果是对的,有时结果是错的,而且不会有任何报错提示。

二、浮点数的精度陷阱

当 DISTINCT 的列中包含 FLOAT、DOUBLE 等浮点类型时,问题会从"语义错误"升级为"结果不可预测"。

1. 二进制表示的根本局限

IEEE 754 标准下的浮点数使用二进制来存储十进制小数,而大多数十进制小数(如 0.1、0.3、99.99)在二进制中是无限循环小数,无法被精确存储,只能近似表示。

代码语言:javascript
复制
-- 在 MySQL 中执行
 
SELECT 0.1 + 0.2 ;
-- 输出:0.30000000000000004 ← 不是 0.3!
SELECT 0.1 + 0.2 = 0.3;
-- 输出:0(false)

2. DISTINCT 的判等机制与浮点数的冲突

DISTINCT 通过精确相等比较(即 = 操作符)来判断两行是否重复。将其用于浮点列时,结果完全不可控:

⚠业务上完全相同的两个金额值(如都表示 99.99 元),因浮点存储误差的微小差异,在 DISTINCT 的视角中可能是两个不同的值,导致去重失败——而这一切不会产生任何错误或警告。

更隐蔽的是,当多列中存在多个浮点字段时,每个字段都是潜在的精度陷阱,组合爆炸式地增加了"假不重复"的概率,结果集行数偏多,但偏差极小,人工审查几乎无法察觉。

三、加一个 DISTINCT,埋三个隐患

危害 1:掩盖 JOIN 逻辑错误

在实际开发中,DISTINCT 被用来"修复"查询结果中莫名出现的重复行,但重复行往往是 JOIN 条件不完整导致的笛卡尔积膨胀:

❌ 用 DISTINCT 压制笛卡尔积

代码语言:javascript
复制
SELECT DISTINCT
 o.user_id, o.amount, p.payment_method 
FROM
 orders o 
JOIN
 payments p 
ON
 o.user_id = p.user_id; 
-- ⚠ 缺少 order_id 关联,一个用户有 N 笔支付则结果膨胀 N 倍
-- DISTINCT 让结果"看起来正常",但数据已经被错误地交叉组合过

这是最危险的一类误用:数据在 JOIN 阶段就已经被错误地扩散,DISTINCT 只是在最终结果上打了掩护,真正的 bug 隐藏在 SQL 逻辑中,在数据量增大或业务变化时随时可能爆发。

危害 2:掩盖上游数据管道问题

❌ 用 DISTINCT 绕过 ETL 重复写入

代码语言:javascript
复制
-- 数仓的 ETL 任务缺少幂等性保障,导致数据重复落库
-- 报表查询加上 DISTINCT 后结果"正常了"
SELECT DISTINCT
 user_id, order_date, amount 
FROM
 dw.fact_orders; 
-- 源头 bug 没有修复,下游 SUM(amount) 仍然翻倍

查询层的 DISTINCT 只能修复这一条 SQL 的结果,但其他查询、其他报表、下游的聚合计算仍然会受到重复数据的影响。修复症状而不修复病根,技术债务持续积累。

危害 3:掩盖表设计缺陷

如果一张表的查询必须依赖 DISTINCT 才能得到正确结果,这往往意味着该表缺乏明确的唯一性约束,主键设计存在问题。用 DISTINCT 绕过这个问题,让表设计的缺陷永远不会被修复。

四、为什么开发者会这样写

理解误用的根源,是建立正确规范的前提。以下是 DISTINCT 被滥用的几个典型心理模式:

五、停止用 DISTINCT 去重,试试这几种正确写法

方案 1:追溯重复的根源,从源头修复

出现重复行时,首先要回答:为什么会有重复? 是 JOIN 逻辑问题、ETL 管道问题还是表设计问题?从根源修复,而不是在查询层打补丁。

方案 2:使用精确数值类型存储金额

❌ 浮点类型(不精确)

代码语言:javascript
复制
amount DOUBLE,
discount FLOAT,
tax_rate REAL

✅ 精确类型(推荐)

代码语言:javascript
复制
amount DECIMAL(18, 4) 
discount DECIMAL(10, 4) 
tax_rate DECIMAL(6, 4)

DECIMAL 是精确数值类型,基于十进制存储,不存在二进制近似误差,是金融场景金额字段的标准选择。

方案 3:基于业务主键明确去重

❌ 依赖属性值相同来去重

代码语言:javascript
复制
SELECT DISTINCT
 user_id, amount, status 
FROM
 orders;

✅ 明确基于业务主键去重

代码语言:javascript
复制
SELECT
  user_id,
  amount,
  STATUS
FROM
  orders
WHERE
  order_id IN (
    -- 每个业务主键只保留一条
    SELECT
      MIN(order_id)
    FROM
      orders
    GROUP BY
      business_order_no
  );

方案 4:用 ROW_NUMBER() 精确控制去重规则

这是最推荐的去重模式。它强制开发者明确两个核心问题:什么是"同一实体"(PARTITION BY),以及出现重复时保留哪一条(ORDER BY)。

✅ 语义清晰的去重写法

代码语言:javascript
复制
SELECT
  order_id,
  user_id,
  amount,
  STATUS
FROM
  (
    SELECT
      *,
      ROW_NUMBER() OVER (
        PARTITION BY
          business_order_no
          -- 定义"同一实体"
        ORDER BY
          updated_at DESC
          -- 保留最新一条
      ) AS rn
    FROM
      orders
  ) t
WHERE
  rn = 1;

方案 5:修复 JOIN 逻辑,而非用 DISTINCT 压制结果

❌ JOIN 条件不完整 + DISTINCT 掩盖

代码语言:javascript
复制
SELECT DISTINCT
 o.user_id, o.amount, p.payment_method 
FROM orders o 
JOIN payments p 
ON o.user_id = p.user_id;

✅ 补全 JOIN 关联条件

代码语言:javascript
复制
SELECT
 o.user_id, o.amount, p.payment_method 
FROM
 orders o 
JOIN
 payments p 
ON o.order_id = p.order_id;
-- 通过正确的关联键,从根本上消除重复,无需 DISTINCT

总结

DISTINCT 本身并非错误的语法,但它被广泛地误用于两个不应该承担的职责:掩盖数据管道的重复写入问题,以及替代基于业务主键的精确去重逻辑。

当这种误用遇上 FLOAT/DOUBLE 等浮点类型时,问题进一步升级为不可预测的精度错误——正确的结果只是一种巧合,而非设计的保证。

每次使用 DISTINCT 前,先问自己:我知道这些数据重复的根本原因吗?我是在基于业务实体的唯一标识去重,还是在依赖属性值碰巧相同来去重?如果无法清晰回答第一个问题,DISTINCT 就不应该出现在这里。

本文由 PawSQL 规则引擎团队整理,相关 SQL 审查规则已集成至 PawSQL审核引擎。

🌐关于PawSQL

PawSQL专注于数据库性能优化自动化和智能化,提供的解决方案覆盖SQL开发、测试、运维的整个流程,广泛支持多种主流商用、国产和开源数据库,为开发者和企业提供一站式的创新SQL优化解决方案。

图片
图片
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PawSQL 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、去重去的是什么?大多数人从一开始就想错了
  • 二、浮点数的精度陷阱
    • 1. 二进制表示的根本局限
    • 2. DISTINCT 的判等机制与浮点数的冲突
  • 三、加一个 DISTINCT,埋三个隐患
    • 危害 1:掩盖 JOIN 逻辑错误
    • 危害 2:掩盖上游数据管道问题
    • 危害 3:掩盖表设计缺陷
  • 四、为什么开发者会这样写
  • 五、停止用 DISTINCT 去重,试试这几种正确写法
    • 方案 1:追溯重复的根源,从源头修复
    • 方案 2:使用精确数值类型存储金额
    • 方案 3:基于业务主键明确去重
    • 方案 4:用 ROW_NUMBER() 精确控制去重规则
    • 方案 5:修复 JOIN 逻辑,而非用 DISTINCT 压制结果
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档