在mysql查询里,当我们需要根据多个列的组合来去除重复数据,同时又要保留每组的某一条完整记录时,很多写法都会让人踩坑。distinct关键字虽然能去重,但它在多列场景下的行为常常和直觉不符。理解它和group by协作的正确方式,是写出稳定sql的关键。

distinct多列的真实执行逻辑
不少人在写查询时会直接使用select distinct col1, col2 from table这样的语句,以为只会对col1和col2单独去重。实际上distinct作用于整行结果,它会对select后面出现的所有列的组合进行唯一性判断。也就是说,只有当col1和col2的值同时相同时,这一行才会被判定为重复并合并。如果表里还有col3,即便col1、col2相同但col3不同,distinct也不会去掉这些行。
这种机制带来一个典型问题:当我们既想按col1、col2去重,又想顺带拿出col3的最新值或者其他关联字段时,distinct就无能为力了。因为一旦把col3写进select列表,整行组合就变了,去重效果立刻失效。下面这段代码展示了常见的误用:
select distinct user_id, order_type, create_time from orders where status = 1;
上面的语句里,只要create_time有细微差别,即便user_id和order_type一样,行也不会合并。如果业务需求是“每个用户每种订单类型只取一条”,这种写法必然返回多余数据。要解决它,必须引入group by来显式定义分组维度。
用group by替代distinct完成组合去重
group by才是真正按指定列组合分组的手段。把需要去重的列放到group by后面,再用聚合函数处理其余列,就能精确控制结果。比如上面的场景,可以写成按user_id和order_type分组,然后取最大的create_time表示最近下单时间:
select user_id, order_type, max(create_time) as last_time from orders where status = 1 group by user_id, order_type;
这样写之后,mysql会把user_id和order_type相同的行归为一组,max(create_time)从中挑出时间最大的那条。结果集里每个user_id加order_type的组合只出现一次,达到了组合去重的目的。相比distinct,group by的优势在于你可以决定非分组列如何取舍,而不是被迫把它们全塞进去重维度。
需要注意的是,在开启了ONLY_FULL_GROUP_BY模式的mysql版本中,select列表里的列要么在group by中,要么被聚合函数包裹,否则会直接报错。这其实是一种保护,避免你拿到不确定的值。如果你的mysql是旧版且关闭了该模式,看似能跑的语句其实会随机返回组内某行数据,迁移到新环境就会出故障。因此写group by时要保证所有非聚合列都明确分组或聚合。
结合子查询处理取完整行的复杂情况
有时业务要求更苛刻:按多列去重后,要拿回整行所有字段,而不仅仅是聚合出来的值。例如想取每个用户每种订单类型下create_time最新那一整条记录。这时可以先通过group by找到每组的最大时间,再拿这个结果去原表关联,从而带回完整行:
select o.*
from orders o
inner join (
select user_id, order_type, max(create_time) as last_time
from orders
where status = 1
group by user_id, order_type
) t on o.user_id = t.user_id
and o.order_type = t.order_type
and o.create_time = t.last_time
where o.status = 1;
子查询t先算出每个组合的最新时间,外层再用这些键关联回原表,就能精确取出对应整行。这种方式比在group by里硬写一堆聚合函数清晰得多,也方便后期加索引优化。建议在user_id、order_type、create_time上建联合索引,子查询和关联都会明显变快。
反观直接用distinct试图解决这类问题,不仅语义不对,还会让优化器无法利用分组索引,全表扫描风险高。当数据量上涨,两者的性能差距会非常直观。掌握group by配合子查询或聚合函数的思路,基本可以覆盖九成以上的多列去重需求,也让sql在mysql各版本间更可移植。