跳过正文
  1. 博客/
  2. 后端/
  3. 数据库/

MySQL Group By 还可以用来去重

5 分钟· ·
后端 数据库 MySQL
作者
Allen
一个强大、轻量级的 Hugo 主题。
目录

最近在接手老项目的时候,看到一个SQL: select * from xx group by id
当时一看到这句就感觉,这个group by是不是多余的,既然select 全部了,那去掉其实也无所谓,
然后询问上一个接手的同事才知道这个是用来去重了,好家伙,以前一直用来分组统计的语句竟然可以用来
去重,涨知识了

Group By 到底可不可以去重呢
#

首先我们新建一个测试表,并插入一些测试数据

CREATE TABLE test.student (
  
  `sno` varchar(20) NOT NULL,
  
  `sname` varchar(20) DEFAULT NULL,
  
  `ssex` varchar(20) DEFAULT NULL,
  
  `sage` int(11) DEFAULT NULL,
  
  `sdept` varchar(20) DEFAULT NULL,
  
  PRIMARY KEY (`sno`)
  
);
  
INSERT INTO test.student (sno, sname, ssex, sage, sdept) VALUES ('201215121', '李勇', '男', 20, 'CS');
  
INSERT INTO test.student (sno, sname, ssex, sage, sdept) VALUES ('201215122', '刘晨', '女', 19, 'CS');
  
INSERT INTO test.student (sno, sname, ssex, sage, sdept) VALUES ('201215123', '王敏', '女', 18, 'MA');
  
INSERT INTO test.student (sno, sname, ssex, sage, sdept) VALUES ('201215124', '张立', '男', 19, 'IS');
  
INSERT INTO test.student (sno, sname, ssex, sage, sdept) VALUES ('201215125', '张立', '男', 19, 'IS');
  

然后我们测试一些是否可以去重

mysql> SELECT * from test.student GROUP BY ssex ;
  
ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'test.student.sno' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
  

哎呀出错了,意思是不支持,因为我们sql_mode 设置了only_full_group_by这个,我们来看看我们sql_mode里面有啥

mysql> SELECT @@SESSION.sql_mode \G;
  
*************************** 1. row ***************************
  
@@SESSION.sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
  
1 row in set (0.00 sec)
  

的确我们sql_mode是设置了ONLY_FULL_GROUP_BY这个值,这个sql_mode是啥呢,首先我们知道
MySQL只是SQL一种实现软件,他为了和其他SQL兼容,就设置了这个变量,你可以通过
修改这个值来修改他的语法支持

其实group by本来的意图就是一个聚合函数,正常来说,我们一般是使用下面的语法

select a, count(*) from xxx group by a
  

但是MySQL非常开放,它允许你使用非group by 字段,在5.7之前都是默认允许的,但是5.7之后,它默认关闭了
但是你可以修改关闭

接下来我们临时关闭掉这个,怎么关闭呢,非常简单,把ONLY_FULL_GROUP_BYsql_mode 删除就好了
接下面执行下面语句

set sql_mode =(SELECT replace(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY,', ''));
  

然后我们再执行上面sql

mysql> SELECT * from test.student GROUP BY ssex ;
  
+-----------+--------+------+------+-------+
  
| sno       | sname  | ssex | sage | sdept |
  
+-----------+--------+------+------+-------+
  
| 201215122 | 刘晨   | 女   |   19 | CS    |
  
| 201215121 | 李勇   | 男   |   20 | CS    |
  
+-----------+--------+------+------+-------+
  
2 rows in set (0.00 sec)
  

o了,我们再看看我们目前的sql_mode

mysql> SELECT @@SESSION.sql_mode;
  
+------------------------------------------------------------------------------------------------------------------------+
  
| @@SESSION.sql_mode                                                                                                     |
  
+------------------------------------------------------------------------------------------------------------------------+
  
| STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION |
  
+------------------------------------------------------------------------------------------------------------------------+
  

的确目前看到ONLY_FULL_GROUP_BY这个了,而且我们发现的确去重了

Distinct和Group By
#

通过上面的实验我们知道了,Group By的确可以去重,但是我们去重一般使用Distinct来,
Distinct原理很简单

我们执行下面语句

mysql> explain SELECT distinct ssex from test.student ;
  
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------+
  
| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra           |
  
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------+
  
|  1 | SIMPLE      | student | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    5 |   100.00 | Using temporary |
  
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------+
  

我们可以看到Using temporary告诉我们,就是创建一个临时表,然后把
Distinct的字段从表里取出来,然后返回给客户端就好了

我们看看Group By的原理

mysql> explain SELECT * from test.student GROUP BY ssex ;
  
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+---------------------------------+
  
| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                           |
  
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+---------------------------------+
  
|  1 | SIMPLE      | student | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    5 |   100.00 | Using temporary; Using filesort |
  
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+---------------------------------+
  

我们也看到了Using temporary;,其实Group By底层也是用到临时表,把你Group By的值当
做主键,而且我们看到默认它会取第一个出现的值作为最终的值
这个是为什么呢

Group By 原理解析
#

我们前面知道,Group By 一开始只是用来做聚合函数的,所以底层上最开始设计的时候就是

  • 首先建一张临时表,把Group By字段作为唯一主键(下面称作key)
  • 然后把原始表中所有数据按照key进行分组
  • 对分组内数据一个一个进行聚合,比如执行count, avg 等等函数,最后每组跟随主键key产生最终的一列值
  • 最后对临时表进行排序返回

所以其实当时为了支持,不在主键内的值,MySQL只是简单的把组内第一个出现值赋值进去

那为什么不把最后组内最后一个值赋值进去呢,首先是如果实现这个功能,想当于组内数据要全部执行一遍,
效率太低了,那有没有办法能实现去重的时候取组内最后一个呢

首先说目前是不支持的,不过能通过下面两种方式来实现

方法一:

首先把你想要的数据进行排序

create temporary table test.tt as SELECT * from test.student order by sno desc;
  

然后我们看正常来按照年龄排序

mysql> SELECT * from test.student GROUP BY sage;
  
+-----------+--------+------+------+-------+
  
| sno       | sname  | ssex | sage | sdept |
  
+-----------+--------+------+------+-------+
  
| 201215123 | 王敏   | 女   |   18 | MA    |
  
| 201215122 | 刘晨   | 女   |   19 | CS    |
  
| 201215121 | 李勇   | 男   |   20 | CS    |
  
+-----------+--------+------+------+-------+
  
3 rows in set (0.00 sec)
  

接下来我们再看看我们去重取最后一个出现的,我们看到19张立取代了第一个出现的刘晨

mysql> SELECT * from test.tt GROUP BY sage;
  
+-----------+--------+------+------+-------+
  
| sno       | sname  | ssex | sage | sdept |
  
+-----------+--------+------+------+-------+
  
| 201215123 | 王敏   | 女   |   18 | MA    |
  
| 201215125 | 张立   | 男   |   19 | IS    |
  
| 201215121 | 李勇   | 男   |   20 | CS    |
  
+-----------+--------+------+------+-------+
  
3 rows in set (0.00 sec)
  

方法二:

第二种方法其实把上面两个步骤合成一个,首先我们把原始数据进行排序,然后取出组内第一个

    select sno, sname, sage, sdept
  
    from (SELECT @i := if(@sage <> sage, 0, @i + 1) as i, @sage := sage, a.*
  
    from (select * from test.student order by sage, sno desc) a) a
  
    where i = 0
  
    order by sage;
  

我们用上面这行函数模仿了,Group By去重,我们可以看到我们在末尾加了一个order by sage
语句,这是因为默认Group By会按照Group By的键值给我们排序,
所以如果你想优化Group By性能,当你对组排序顺序无所谓的时候,你可以在
Group By 末尾加上order by null关闭排序功能

总结
#

Group By去重功能在5.7之后就默认关闭了,所以其实大家用来做去重还是慎用,不过通过这个功能去探索
底层原理对你数据库性能优化还是非常有帮助的

资料
#

相关文章

PostgreSQL的自增键
3 分钟
后端 数据库 PostgreSQL
Dubbo浅探
3 分钟
后端 框架 Java Dubbo
Spring Cloud Alibaba浅探
2 分钟
后端 框架 Java SpringBoot
SpringCloud浅析
5 分钟
后端 框架 Java SpringBoot
浅析Spring
3 分钟
后端 框架 Java SpringBoot
浅析微服务
5 分钟
后端 框架 Java