为什么GROUP BY比DISTINCT快3倍?90%的程序员都踩过这个坑!

云游道人 2025-05-14 8 阅读 0评论

引言:去重操作,你真的用对了吗?

“为什么同样的去重需求,同事的代码运行如飞,你的却慢如蜗牛?”

一位开发者在处理百万级数据时,发现SELECT DISTINCT耗时高达10秒,而改用GROUP BY后仅需3秒——性能差距超过3倍

90%的程序员误以为两者功能完全相同,但背后的机制天差地别。本文将彻底揭秘它们的核心区别,并教你如何避免性能陷阱!


一、功能解析:GROUP BY和DISTINCT的职责边界

1. DISTINCT:简单粗暴的去重工具
  • 核心功能:对查询结果的所有列进行全局去重。
  • 典型场景
    -- 查询所有不重复的城市  
    SELECT DISTINCT city FROM users;  
    -- 查询不重复的城市和性别组合  
    SELECT DISTINCT city, gender FROM users;  
  • 本质:将结果集加载到临时表,排序后去除重复行。
2. GROUP BY:分组聚合的多面手
  • 核心功能:按指定字段分组,通常结合聚合函数(如COUNTSUM)使用。
  • 典型场景
    -- 统计每个城市的用户数  
    SELECT city, COUNT(*) FROM users GROUP BY city;  
    -- 计算每个部门的平均薪资  
    SELECT department, AVG(salary) FROM employees GROUP BY department;  
  • 隐藏技能:当不搭配聚合函数时,可实现类似DISTINCT的去重效果。

二、本质区别:为何GROUP BY能吊打DISTINCT?

1. 执行计划对比:临时表与索引的生死博弈
  • DISTINCT的执行流程(无索引时):

    -- EXPLAIN结果:Using temporary; Using filesort  
    EXPLAIN SELECT DISTINCT city FROM users;  
    1. 全表扫描:读取所有数据到内存或磁盘临时表。
    2. 排序去重:对临时表按所有字段排序,遍历去除重复行。
    3. 资源消耗:若数据量超过tmp_table_size,临时表写入磁盘,触发I/O风暴。
  • GROUP BY的执行流程(有索引时):

    -- 添加索引  
    ALTER TABLE users ADD INDEX idx_city (city);  

    -- EXPLAIN结果:Using index(无临时表,无排序)  
    EXPLAIN SELECT city FROM users GROUP BY city;  
    1. 索引扫描:直接遍历city字段的B+树索引(有序)。
    2. 跳跃去重:利用索引有序性,跳过重复值,无需全量排序。
    3. 零回表:若仅查询分组字段,直接从索引取数据,无需访问原表。
2. 性能差距的三大核心原因
  • 原因1:索引利用率
    • GROUP BY可利用索引跳过排序和临时表,而DISTINCT即便有索引也可能触发全表扫描。
  • 原因2:内存与磁盘的较量
    • DISTINCT的临时表默认使用磁盘存储(MyISAM引擎),GROUP BY优先使用内存(MEMORY引擎)。
  • 原因3:隐式排序的代价
    SELECT city FROM users GROUP BY city ORDER BY NULL-- 性能再提升20%  
    • DISTINCT必须全排序,而GROUP BY可通过ORDER BY NULL禁用排序:
3. 实测数据:千万级表性能对比


查询语句
无索引耗时
有索引耗时
SELECT DISTINCT city FROM users
12.3s
9.8s
SELECT city FROM users GROUP BY city
11.7s
2.1s


  • 结论
    • 无索引时:两者性能接近,但GROUP BY略优(省去显式排序)。
    • 有索引时GROUP BY性能提升超过3倍!

三、避坑指南:这些场景GROUP BY反而更危险!

1. 分组字段无索引时
  • 问题GROUP BY同样需要全表扫描+临时表,性能与DISTINCT无异。
  • 优化方案
    -- 为分组字段添加索引  
    ALTER TABLE orders ADD INDEX idx_product (product_id);  
2. 查询包含非分组字段
  • 错误示例
    -- 查询结果不确定(MySQL可能随机返回user_id)  
    SELECT city, user_id FROM users GROUP BY city;  
  • 正确写法
    SELECT city, MAX(user_id) FROM users GROUP BY city; -- 明确聚合规则  
3. 分组的字段含复杂计算
  • 错误示例
    -- 索引失效,触发全表扫描  
    SELECT YEAR(create_time) FROM orders GROUP BY YEAR(create_time);  
  • 优化方案
    -- 新增冗余字段并添加索引  
    ALTER TABLE orders ADD COLUMN create_year INTADD INDEX idx_year (create_year);  
    UPDATE orders SET create_year = YEAR(create_time);  

四、终极实战:亿级数据去重优化方案

1. 索引设计黄金法则
  • 覆盖索引:确保查询字段全部包含在索引中。
    -- 为city和gender创建联合索引  
    ALTER TABLE users ADD INDEX idx_city_gender (city, gender);  
    -- 以下查询直接走索引  
    SELECT city, gender FROM users GROUP BY city, gender;  
2. 参数调优:强制临时表驻留内存
-- 调整会话级内存参数(单位:字节)  
SET tmp_table_size = 256 * 1024 * 1024;  -- 256MB  
SET max_heap_table_size = 256 * 1024 * 1024;  
3. 分布式方案:Elasticsearch预聚合
  • 适用场景:实时统计海量数据的唯一值。
  • 操作步骤
    GET /users/_search  
    {  
      "size"0,  
      "aggs": {  
        "unique_cities": {  
          "terms": { "field""city.keyword""size"1000 }  
        }  
      }  
    }  
    1. 将去重字段(如city)同步到Elasticsearch。
    2. 使用terms aggregation实现毫秒级去重:

五、灵魂拷问:你真的需要去重吗?

  • 场景1:仅统计不重复数量 → SELECT COUNT(DISTINCT city)
  • 场景2:需要列出所有不重复值 → 优先用GROUP BY+索引
  • 场景3:去重后需复杂计算 → 使用窗口函数(如ROW_NUMBER())。

结尾互动

你在使用GROUP BY和DISTINCT时踩过哪些坑?欢迎留言分享!

发表评论

快捷回复: 表情:
Addoil Applause Badlaugh Bomb Coffee Fabulous Facepalm Feces Frown Heyha Insidious KeepFighting NoProb PigHead Shocked Sinistersmile Slap Social Sweat Tolaugh Watermelon Witty Wow Yeah Yellowdog
提交
评论列表 (有 0 条评论, 8人围观)

最近发表

热门文章

最新留言

热门推荐

标签列表