在 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 核心特性
-
仅作用于查询结果集,不会修改表中原始数据;
-
对
NULL值有效:多个NULL会被视为重复值,仅保留一个; -
区分大小写(MySQL 默认不区分,可通过排序规则调整);
-
必须写在
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
关键结论:
-
重复的城市名被完全过滤;
-
两个
NULL仅保留一个,符合 MySQL 对NULL的去重规则; -
单字段去重:字段值完全相同才会被判定为重复。
三、多字段 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 代表未知值 ,但 DISTINCT 对 NULL 有固定处理逻辑:
-
所有
NULL值被视为相同值; -
去重后结果集中仅保留一个 NULL;
-
不影响非 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 无索引场景
-
MySQL 加载全表数据到内存;
-
对结果集进行排序(排序是去重的前提);
-
遍历排序后的结果,过滤重复记录;
-
返回最终唯一结果。
缺点:大数据量下排序耗时极长,性能极差。
6.2 有索引场景(最优)
-
直接通过索引树获取有序数据;
-
无需全表扫描和内存排序;
-
直接遍历索引过滤重复值。
结论 :索引是 DISTINCT 性能优化的核心。
七、DISTINCT 与 GROUP BY 区别
Java 面试中高频问题:DISTINCT 和 GROUP BY 都能去重,二者有什么区别?开发如何选择?
| 对比维度 | DISTINCT | GROUP BY |
|---|---|---|
| 核心用途 | 单纯数据去重 | 数据分组 + 聚合统计 |
| 执行效率 | 简单去重更快 | 分组逻辑更重,单纯去重效率更低 |
| 使用场景 | 仅需要唯一结果集,无聚合计算 | 需要分组统计(COUNT/SUM 等) |
| 多字段规则 | 组合值去重 | 按字段分组,可搭配聚合函数 |
| 底层逻辑 | 排序 + 去重 | 分组 + 排序 + 聚合 |
7.1 选择标准
-
仅去重 :优先用
DISTINCT,语法更简洁、效率更高; -
分组 + 统计 :必须用
GROUP BY; -
大数据量下:二者都需要依赖索引优化。
八、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 禁止使用场景
-
禁止在无索引的大字段(TEXT/BLOB)上使用 DISTINCT;
-
禁止在全表查询(无 WHERE 条件)的千万级表中使用;
-
禁止嵌套多层子查询中滥用 DISTINCT。
8.3 替代优化方案
大数据量去重:优先用覆盖索引 + 分页,避免一次性加载全部数据。