最近在接手老项目的时候,看到一个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_BY
在sql_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
之后就默认关闭了,所以其实大家用来做去重还是慎用,不过通过这个功能去探索
底层原理对你数据库性能优化还是非常有帮助的