【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 核心特性仅作用于查询结果集不会修改表中原始数据对NULL值有效多个NULL会被视为重复值仅保留一个区分大小写MySQL 默认不区分可通过排序规则调整必须写在SELECT后、查询字段前不能写在字段中间。DISTINCT 需要放到所有列名的前面如果写成SELECT salary, DISTINCT department_id FROM employees会报错。二、单字段 DISTINCT2.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 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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的去重规则单字段去重字段值完全相同才会被判定为重复。三、多字段 DISTINCT90% 开发者踩坑点多字段去重是开发中最容易出错的场景。核心规则只有多个字段的组合值完全相同时才会被判定为重复记录。3.1 多字段去重实操需求查询不重复的「用户名 城市」组合-- 未去重 SELECT username, city FROM user_info; -- 结果 -- 张三 北京 -- 李四 上海 -- 张三 北京 -- 王五 深圳 -- 赵六 NULL -- 钱七 NULL -- 李四 上海 -- DISTINCT 多字段去重 SELECT DISTINCT username, city FROM user_info; -- 最终结果仅保留组合唯一值 -- 张三 北京 -- 李四 上海 -- 王五 深圳 -- 赵六 NULL -- 钱七 NULL3.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 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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);场景 1DISTINCT 聚合函数 WHERE 条件过滤业务需求统计已支付订单中消费过的唯一用户数量-- 过滤已支付订单统计去重用户数 SELECT COUNT(DISTINCT user_id) AS pay_user_count FROM order_info WHERE order_status 2; -- 订单状态已支付场景 2DISTINCT 聚合函数 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;场景 4DISTINCT 配合多聚合函数 多表联查业务需求生成用户数据统计报表包含城市、总用户数、唯一付费用户数、总支付金额、平均支付金额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都能去重二者有什么区别开发如何选择对比维度DISTINCTGROUP 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;多字段去重优化建立联合索引-- 对 usernamecity 建立联合索引 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 替代优化方案大数据量去重优先用覆盖索引 分页避免一次性加载全部数据。