在MySQL查询中,DISTINCT关键字用于从结果集中消除完全重复的行。无论是做报表统计还是接口数据清洗,掌握它的运行机制都能避免写出拖慢数据库的SQL。很多初学者以为DISTINCT只能放在SELECT之后单独使用,实际上它可以配合多列、聚合函数以及子查询形成丰富的去重方案。

一、DISTINCT的基础语法与单列去重原理
最基础的用法是在SELECT列表的第一个位置写上DISTINCT,后面跟随需要去重的列。MySQL在执行时会先根据指定列的值构建哈希或排序结构,将值相同的行合并为一行输出。例如一张用户表里有多个相同的邮箱记录,我们只想拿到不重复的邮箱列表,就可以使用单列去重。这种操作在语法上非常直观,但底层会消耗临时表空间,尤其是当去重列没有索引时,MySQL往往要进行一次全表扫描再加排序去重。
下面是一段典型的单列去重查询代码,假设表名为user_info,字段为email:
SELECT DISTINCT email FROM user_info;
如果email列上已经建立了普通索引,MySQL有可能利用索引的有序性直接跳过重读相邻相同值,从而避免额外排序。这也是为什么在大数据表上做去重前,先确认索引结构能显著提升效率。相反,若去重列是长文本类型如TEXT,不仅无法建普通索引,还会让临时表落盘,查询延迟陡增。
单列去重还有一个容易忽略的点:NULL值被视为相等。也就是说多行该列为NULL时,DISTINCT结果里只保留一个NULL。这和GROUP BY的行为一致,在写统计逻辑时要特别注意,避免把空值当成有效维度计入。
二、多列联合去重与GROUP BY的对比
当业务要求按“城市+性别”这样的组合维度去重时,DISTINCT可以紧跟多个列名,此时只有这些列的值同时完全相同的行才会被合并。多列去重常用于拉取维度组合清单,比如电商系统要获取所有已开通的城市与仓库配对。此时MySQL会将多列拼接后做哈希判重,列数越多、区分度越低,临时表就越大。
示例代码如下,从订单表order_tbl取出不重复的下单城市和支付方式:
SELECT DISTINCT city, pay_type FROM order_tbl;
很多人会把DISTINCT多列和GROUP BY混用,认为二者等价。在仅查询去重列时,SELECT DISTINCT a,b FROM t 与 SELECT a,b FROM t GROUP BY a,b 结果一致,但执行计划常有差别。GROUP BY在老版本MySQL里默认走排序,新版本有了松散索引扫描优化;DISTINCT有时直接走哈希去重。如果去重的同时还要计数,GROUP BY配合COUNT更自然,而DISTINCT必须与COUNT组合成COUNT(DISTINCT col)才能统计非重复值数量,语义上更偏向聚合而非行过滤。
从可读性角度看,单纯获取唯一组合清单用DISTINCT语句更短;但若后续要LEFT JOIN其他表或加HAVING条件,GROUP BY结构更容易扩展。在联合去重场景,建议先在小样本上用EXPLAIN比对二者成本,再决定生产写法。
三、COUNT(DISTINCT)与大数据量去重优化
统计非重复值数量是分析类查询的高频需求,MySQL提供COUNT(DISTINCT col)语法。它在单线程下会维护一个去重集合,当列基数非常高(如用户ID)且数据上亿时,容易触发内存超限并写入磁盘临时表。此时查询可能从毫秒级退化到几十秒,直接影响前端体验。
看一段统计独立访客的SQL:
SELECT COUNT(DISTINCT user_id) FROM access_log WHERE visit_date = '2023-09-01';
面对上面这种重查询,常见优化思路有几种。其一是利用近似计算,MySQL没有内置HyperLogLog,但可以通过业务层按天分表,再汇总各分表去重结果,降低单语句压力。其二是建立覆盖索引,让扫描只走索引叶子节点,减少回表开销。其三是当允许一定误差时,用随机变量抽样,比如只统计user_id尾号等于某值的部分数据再放大估算。
另外要注意,COUNT(DISTINCT)在多列上并不支持COUNT(DISTINCT a,b)这种写法直接统计联合去重,必须借助子查询先DISTINCT出组合,外层再COUNT。例如:
SELECT COUNT(*) FROM ( SELECT DISTINCT a, b FROM large_table ) AS tmp;
这种嵌套写法会把内层结果物化成临时表,若数据量巨大依然吃力。因此在超大规模去重统计时,往往要把计算卸载到数据仓库或借助位图索引,MySQL侧只承担轻量查询。理解DISTINCT的能力边界,才能在设计阶段就规避慢查询。