【MySQL】DISTINCT 详解

在 MySQL 开发中,数据去重 是高频核心需求,无论是报表统计、多表关联查询、还是接口数据返回,都离不开去重操作。DISTINCT 作为 MySQL 官方提供的原生去重关键字,看似简单,实则隐藏着大量易踩坑知识点。

本文将全方位拆解 MySQL DISTINCT,让你一站式吃透这个核心关键字,彻底告别开发中的去重问题。

🌟【青柠代码录】--- 青柠来相伴,代码更简单 🌟

🔥【全栈】博客合集:https://www.yuque.com/u12587869/zplytb/ur5ohwqxd2axtiny 🔥

🎯【Java】面试题:https://www.yuque.com/u12587869/zplytb/eh7yqzitiab693og 🎯


一、DISTINCT 基础认知

1.1 核心定义

DISTINCT 是 MySQL 中的查询去重关键字 ,用于对查询结果集中的重复记录 进行过滤,仅保留唯一值,必须紧跟在 SELECT 关键字之后使用。

1.2 基础语法

复制代码
-- 单字段去重
SELECT DISTINCT 字段名 FROM 表名 [WHERE 条件];
​
-- 多字段去重,DISTINCT 其实是对后面所有列名的组合进行去重
SELECT DISTINCT 字段1, 字段2, ... FROM 表名 [WHERE 条件];
​
-- 配合聚合函数/统计使用
SELECT COUNT(DISTINCT 字段名) FROM 表名;

1.3 核心特性

  1. 仅作用于查询结果集,不会修改表中原始数据;

  2. NULL 值有效:多个 NULL 会被视为重复值,仅保留一个;

  3. 区分大小写(MySQL 默认不区分,可通过排序规则调整);

  4. 必须写在 SELECT 后、查询字段前,不能写在字段中间。DISTINCT 需要放到所有列名的前面,如果写成SELECT salary, DISTINCT department_id FROM employees会报错。


二、单字段 DISTINCT

2.1 测试表准备

创建常用的用户信息表,模拟真实业务数据:

复制代码
-- 创建用户表
CREATE TABLE `user_info` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
  `username` VARCHAR(50) NOT NULL COMMENT '用户名',
  `city` VARCHAR(30) COMMENT '所在城市',
  `age` TINYINT COMMENT '年龄',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
​
-- 插入测试数据(包含重复值)
INSERT INTO `user_info` (`username`, `city`, `age`) VALUES
('张三', '北京', 22),
('李四', '上海', 25),
('张三', '北京', 22),
('王五', '深圳', 28),
('赵六', NULL, 30),
('钱七', NULL, 26),
('李四', '上海', 25);

2.2 单字段去重实操

需求:查询所有不重复的用户所在城市

复制代码
-- 未去重:查询所有城市(包含重复+NULL)
SELECT city FROM user_info;
-- 结果:北京、上海、北京、深圳、NULL、NULL、上海
​
-- 去重后:仅保留唯一值
SELECT DISTINCT city FROM user_info;
-- 结果:北京、上海、深圳、NULL

关键结论

  1. 重复的城市名被完全过滤;

  2. 两个 NULL 仅保留一个,符合 MySQL 对 NULL 的去重规则;

  3. 单字段去重:字段值完全相同才会被判定为重复。


三、多字段 DISTINCT(90% 开发者踩坑点)

多字段去重是开发中最容易出错的场景。

核心规则:只有多个字段的组合值完全相同时,才会被判定为重复记录

3.1 多字段去重实操

需求:查询不重复的「用户名 + 城市」组合

复制代码
-- 未去重
SELECT username, city FROM user_info;
-- 结果:
-- 张三  北京
-- 李四  上海
-- 张三  北京
-- 王五  深圳
-- 赵六  NULL
-- 钱七  NULL
-- 李四  上海
​
-- DISTINCT 多字段去重
SELECT DISTINCT username, city FROM user_info;
-- 最终结果(仅保留组合唯一值):
-- 张三  北京
-- 李四  上海
-- 王五  深圳
-- 赵六  NULL
-- 钱七  NULL

3.2 易踩坑误区

❌ 错误认知:多字段去重会分别对每个字段去重

✅ 正确认知:多字段是「组合去重」,只要任意一个字段值不同,就视为唯一记录。

例:(张三,北京)(张三,上海) 是两条不同记录,不会被去重。


四、DISTINCT 与 NULL 值的处理规则

MySQL 中 NULL 代表未知值 ,但 DISTINCTNULL 有固定处理逻辑:

  1. 所有 NULL 值被视为相同值

  2. 去重后结果集中仅保留一个 NULL

  3. 不影响非 NULL 字段的去重逻辑。

测试示例:

复制代码
-- 查询去重后的年龄(包含NULL)
SELECT DISTINCT age FROM user_info;
-- 结果:22、25、28、30、26

五、DISTINCT + 聚合函数

在报表统计、数据计算、多维度业务分析场景中,DISTINCT 常与 COUNT/SUM/AVG/MAX/MIN 等聚合函数配合使用。

核心作用:对指定字段先去重,再进行聚合计算,精准统计唯一值相关指标

5.1 COUNT (DISTINCT) 统计唯一值数量

基础需求:统计系统中不重复的城市数量

复制代码
-- 统计去重后的城市总数(NULL 不计入统计)
SELECT COUNT(DISTINCT city) AS city_count FROM user_info;
-- 结果:3(北京、上海、深圳)

核心特性COUNT(DISTINCT 字段)自动忽略 NULL 值,这是企业统计的标准用法。


5.2 基础聚合函数配合 DISTINCT

复制代码
-- 统计不重复年龄的平均值(先去重年龄,再计算均值)
SELECT AVG(DISTINCT age) AS avg_age FROM user_info;
​
-- 统计不重复年龄的总和(先去重年龄,再计算总和)
SELECT SUM(DISTINCT age) AS sum_age FROM user_info;
​
-- 统计不重复年龄的最大值
SELECT MAX(DISTINCT age) AS max_age FROM user_info;
​
-- 统计不重复年龄的最小值
SELECT MIN(DISTINCT age) AS min_age FROM user_info;

5.3 复杂聚合场景

在真实开发中,DISTINCT + 聚合函数 绝不会单独使用,通常结合 WHERE 条件过滤、GROUP BY 分组、多表关联、HAVING 筛选 组合使用,覆盖电商、金融、后台管理系统核心统计场景。

前置说明

沿用前文 user_info 用户表,新增订单表 **order_info**,模拟真实业务关联场景:

复制代码
-- 创建订单表(企业级标准表结构)
CREATE TABLE `order_info` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID',
  `user_id` BIGINT NOT NULL COMMENT '用户ID',
  `order_amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额',
  `order_status` TINYINT NOT NULL COMMENT '订单状态:1-待支付 2-已支付 3-已取消',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单信息表';
​
-- 插入测试数据
INSERT INTO `order_info` (`user_id`, `order_amount`, `order_status`) VALUES
(1, 99.00, 2),
(1, 199.00, 2),
(2, 299.00, 2),
(3, 99.00, 2),
(4, 599.00, 3),
(5, 399.00, 2);

场景 1:DISTINCT + 聚合函数 + WHERE 条件过滤

业务需求 :统计已支付订单 中,消费过的唯一用户数量

复制代码
-- 过滤已支付订单,统计去重用户数
SELECT COUNT(DISTINCT user_id) AS pay_user_count
FROM order_info
WHERE order_status = 2; -- 订单状态=已支付

场景 2:DISTINCT + 聚合函数 + GROUP BY 分组

业务需求 :按城市分组 ,统计每个城市的唯一用户数、总消费金额、平均单笔消费

复制代码
SELECT
  u.city AS '用户城市',
  -- 统计当前城市唯一用户数
  COUNT(DISTINCT u.id) AS unique_user_count,
  -- 统计当前城市用户总消费额(去重订单,避免重复统计)
  SUM(DISTINCT o.order_amount) AS total_amount,
  -- 统计当前城市用户平均消费(去重订单金额)
  AVG(DISTINCT o.order_amount) AS avg_amount
FROM user_info u
LEFT JOIN order_info o ON u.id = o.user_id
WHERE o.order_status = 2 -- 仅统计已支付订单
GROUP BY u.city -- 按城市分组
HAVING u.city IS NOT NULL; -- 过滤城市为NULL的数据

场景 3:多字段 DISTINCT + 嵌套聚合

业务需求 :统计每个城市 下,同一天注册且产生支付订单唯一用户数(多维度去重统计)

复制代码
SELECT
  city AS '城市',
  DATE(create_time) AS '注册日期',
  -- 核心:同时对 用户ID+注册日期 去重,精准统计唯一用户
  COUNT(DISTINCT id, DATE(create_time)) AS daily_unique_user
FROM user_info
WHERE id IN (
  -- 子查询:筛选出已支付订单的用户ID
  SELECT DISTINCT user_id FROM order_info WHERE order_status = 2
)
GROUP BY city, DATE(create_time)
HAVING city IS NOT NULL
ORDER BY daily_unique_user DESC;

场景 4:DISTINCT 配合多聚合函数 + 多表联查

业务需求 :生成用户数据统计报表,包含:城市、总用户数、唯一付费用户数、总支付金额、平均支付金额

复制代码
SELECT
  u.city,
  -- 基础统计:当前城市总用户
  COUNT(u.id) AS total_user,
  -- 核心:去重统计付费用户
  COUNT(DISTINCT o.user_id) AS pay_unique_user,
  -- 去重统计总支付金额
  SUM(DISTINCT o.order_amount) AS total_pay_amount,
  -- 去重统计平均支付金额
  ROUND(AVG(DISTINCT o.order_amount), 2) AS avg_pay_amount
FROM user_info u
LEFT JOIN order_info o 
  ON u.id = o.user_id 
  AND o.order_status = 2 -- 关联时直接过滤无效订单,优化性能
GROUP BY u.city
HAVING u.city IS NOT NULL
ORDER BY total_pay_amount DESC;

六、DISTINCT 执行原理

要优化 DISTINCT 性能,必须理解其底层执行逻辑,MySQL 执行 DISTINCT 分两种场景:

6.1 无索引场景

  1. MySQL 加载全表数据到内存;

  2. 对结果集进行排序(排序是去重的前提);

  3. 遍历排序后的结果,过滤重复记录;

  4. 返回最终唯一结果。

缺点:大数据量下排序耗时极长,性能极差。

6.2 有索引场景(最优)

  1. 直接通过索引树获取有序数据;

  2. 无需全表扫描和内存排序;

  3. 直接遍历索引过滤重复值。

结论索引是 DISTINCT 性能优化的核心


七、DISTINCT 与 GROUP BY 区别

Java 面试中高频问题:DISTINCTGROUP BY 都能去重,二者有什么区别?开发如何选择?

对比维度 DISTINCT GROUP BY
核心用途 单纯数据去重 数据分组 + 聚合统计
执行效率 简单去重更快 分组逻辑更重,单纯去重效率更低
使用场景 仅需要唯一结果集,无聚合计算 需要分组统计(COUNT/SUM 等)
多字段规则 组合值去重 按字段分组,可搭配聚合函数
底层逻辑 排序 + 去重 分组 + 排序 + 聚合

7.1 选择标准

  1. 仅去重 :优先用 DISTINCT,语法更简洁、效率更高;

  2. 分组 + 统计 :必须用 GROUP BY

  3. 大数据量下:二者都需要依赖索引优化。


八、DISTINCT 性能优化

在千万级大数据量表中使用 DISTINCT,如果不优化,会直接导致接口超时、数据库宕机,以下是标准优化方案:

8.1 核心优化:为去重字段建立索引

原则 :对频繁使用 DISTINCT 的字段,建立普通索引 / 联合索引

示例:针对 city 字段去重优化

复制代码
-- 建立索引
CREATE INDEX idx_city ON user_info(city);
​
-- 优化后查询,直接走索引,无需全表扫描
SELECT DISTINCT city FROM user_info;

多字段去重优化:建立联合索引

复制代码
-- 对 username+city 建立联合索引
CREATE INDEX idx_username_city ON user_info(username, city);
​
-- 多字段去重走联合索引,性能提升100倍+
SELECT DISTINCT username, city FROM user_info;

8.2 禁止使用场景

  1. 禁止在无索引的大字段(TEXT/BLOB)上使用 DISTINCT;

  2. 禁止在全表查询(无 WHERE 条件)的千万级表中使用;

  3. 禁止嵌套多层子查询中滥用 DISTINCT。

8.3 替代优化方案

大数据量去重:优先用覆盖索引 + 分页,避免一次性加载全部数据。

相关推荐
数据知道2 小时前
MongoDB查询执行计划解读:executionStats详细分析与性能诊断
数据库·mongodb
筵陌2 小时前
MySQL Connector/C API的使用
数据库·mysql
霖霖总总2 小时前
[Redis小技巧15]Redis AOF 重写与混合持久化深度解析:从原理到生产实践
数据库·redis
moxiaoran57532 小时前
MySQL分库分表的实现(一)
数据库·mysql
Y001112362 小时前
Day6-MySQL-函数
数据库·sql·mysql
召田最帅boy2 小时前
使用自定义图片作为Emoji表情的技术实现
数据库·html
2401_853576502 小时前
使用PyTorch构建你的第一个神经网络
jvm·数据库·python
Nandeska3 小时前
6、认识和使用Redis Stack
java·数据库·redis
V1ncent Chen3 小时前
SQL大师之路 09 模式匹配(正则表达式)
数据库·sql·mysql·正则表达式·数据分析