问题
在工作中常会遇到将数据分组排序的问题,如在考试成绩中,找出每个班级的前五名等。
在orcale等数据库中可以使用partition 语句来解决,但在MySQL中就比较麻烦了。这次翻译的文章就是专门解决这个问题的
原文地址: How to select the first/least/max
row per group in SQL
翻译
在使用SQL的过程中,我们经常遇到这样一类问题:如何找出每个程序最近的日志条目?如何找出每个用户的最高分?在每个分类中最受欢迎的商品是什么?通常这类“找出每个分组中最高分的条目”的问题可以使用相同的技术来解决。在这篇文章里我将介绍如何解决这类问题,而且会介绍如何找出最高的前几名而不仅仅是第一名。
这篇文章会用到行数(row number),我在原来的文章 MySQL-specific 和 generic
techniques 中已经提到过如何为每个分组设置行数了。在这里我会使用与原来的文章中相同的表格,但会加入新的price 字段
01 |
+--------+------------+-------+ |
02 |
|
type | variety | price | |
03 |
+--------+------------+-------+ |
04 |
|
apple | gala | 2.79 | |
05 |
|
apple | fuji | 0.24 | |
06 |
|
apple | limbertwig | 2.87 | |
07 |
|
orange | valencia | 3.59 | |
08 |
|
orange | navel | 9.36 | |
09 |
|
pear | bradford | 6.05 | |
10 |
|
pear | bartlett | 2.14 | |
11 |
|
cherry | bing | 2.55 | |
12 |
|
cherry | chelan | 6.33 | |
13 |
+--------+------------+-------+ |
选择每个分组中的最高分
这里我们要说的是如何找出每个程序最新的日志记录或审核表中最近的更新或其他类似的排序问题。这类问题在IRC频道和邮件列表中出现的越来越频繁。我使用水果问题来作为示例,在示例中我们要选出每类水果中最便宜的一个,我们期望的结果如下
1 |
+--------+----------+-------+ |
2 |
|
type | variety | price | |
3 |
+--------+----------+-------+ |
4 |
|
apple | fuji | 0.24 | |
5 |
|
orange | valencia | 3.59 | |
6 |
|
pear | bartlett | 2.14 | |
7 |
|
cherry | bing | 2.55 | |
8 |
+--------+----------+-------+ |
这个问题有几种解法,但基本上就是这两步:找出最低的价格,然后找出和这个价格同一行的其他数据
其中一个常用的方法是使用自连接(self-join),第一步根据type(apple, cherry etc)进行分组,并找出每组中price的最小值
01 |
select
type, min(price) as minprice |
第二步是将刚刚结果与原来的表进行连接。既然刚刚给结果已经被分组了,我们将刚刚的查询语句作为子查询以便于连接没有被分组的原始表格。
01 |
select
f.type, f.variety, f.price |
03 |
select
type, min(price) as minprice |
04 |
from
fruits group by type |
05 |
)
as x inner join fruits as f on f.type = x.type and f.price = x.minprice; |
07 |
+--------+----------+-------+ |
08 |
|
type | variety | price | |
09 |
+--------+----------+-------+ |
10 |
|
apple | fuji | 0.24 | |
11 |
|
cherry | bing | 2.55 | |
12 |
|
orange | valencia | 3.59 | |
13 |
|
pear | bartlett | 2.14 | |
14 |
+--------+----------+-------+ |
还可以使用相关子查询(correlated subquery)的方式来解决。这种方法在不同的mysql优化系统下,可能性能会有一点点下降,但这种方法会更直观一些。
01 |
select
type, variety, price |
03 |
where
price = (select min(price) from fruits as f where f.type = fruits.type); |
04 |
+--------+----------+-------+ |
05 |
|
type | variety | price | |
06 |
+--------+----------+-------+ |
07 |
|
apple | fuji | 0.24 | |
08 |
|
orange | valencia | 3.59 | |
09 |
|
pear | bartlett | 2.14 | |
10 |
|
cherry | bing | 2.55 | |
11 |
+--------+----------+-------+ |
这两种查询在逻辑上是一样的,他们性能也基本相同
找出每组中前N个值
这个问题会稍微复杂一些。我们可以使用聚集函数(MIN(), MAX()等等)来找一行,但是找前几行不能直接使用这些函数,因为它们都只返回一个值。但这个问题还是可以解决的。
这次我们找出每个类型(type)中最便宜的前两种水果,首先我们尝试
01 |
select
type, variety, price |
03 |
where
price = (select min(price) from fruits as f where f.type = fruits.type) |
04 |
or
price = (select min(price) from fruits as f where f.type = fruits.type |
05 |
and
price > (select min(price) from fruits as f2 where f2.type = fruits.type)); |
06 |
+--------+----------+-------+ |
07 |
|
type | variety | price | |
08 |
+--------+----------+-------+ |
09 |
|
apple | gala | 2.79 | |
10 |
|
apple | fuji | 0.24 | |
11 |
|
orange | valencia | 3.59 | |
12 |
|
orange | navel | 9.36 | |
13 |
|
pear | bradford | 6.05 | |
14 |
|
pear | bartlett | 2.14 | |
15 |
|
cherry | bing | 2.55 | |
16 |
|
cherry | chelan | 6.33 | |
17 |
+--------+----------+-------+ |
是的,我们可以写成自连接(self-join)的形式,但是仍不够好(我将这个练习留给读者)。这种方式在N变大(前三名,前4名)的时候性能会越来越差。我们可以使用其他的表现形式编写这个查询,但是它们都不够好,它们都相当的笨重和效率低下。(译者注:这种方式获取的结果时,如果第N个排名是重复的时候最后选择的结果会超过N,比如上面例子还有一个apple价格也是0.24,那最后的结果就会有3个apple)
我们有一种稍好的方式,在每个种类中选择不超过该种类第二便宜的水果
1 |
select
type, variety, price |
4 |
select
count(*) from fruits as f |
5 |
where
f.type = fruits.type and f.price <= fruits.price |
这次的代码要优雅很多,而且在N增加时不需要重新代码(非常棒!)。但是这个查询在功能上和原来的是一样。他们的时间复杂度均为分组中条目数的二次方。而且,很多优化器都不能优化这种查询,使得它的耗时最好为全表行数的二次方(尤其在没有设置正确的索引时),而且数据量大时,可能将服务器会停止响应。那么还有更好的方法吗?有没有办法可以仅仅扫描一次数据,而不是通过子查询进行多次扫描。(译者注:这种方法有一个问题,就是如果排名并列第一的数字超过N后,这个分组会选不出数据,比如price为2.79的apple有3个,那么结果中就没有apple了)
使用 UNION
如果已经为type, price设置了索引,而且在每个分组中去除的数据要多于包含的数据,一种非常高效的单次扫描的方法是将查询拆分成多个独立的查询(尤其对mysql,对其他的RDBMSs也有效),再使用UNION将结果拼到一起。mysql的写法如下:
1 |
(select
* from fruits where type = 'apple' order
by price limit 2 ) |
3 |
(select
* from fruits where type = 'orange' order
by price limit 2 ) |
5 |
(select
* from fruits where type = 'pear' order
by price limit 2 ) |
7 |
(select
* from fruits where type = 'cherry' order
by price limit 2 ) |
Peter Zaistev写了相关的文章, 我在这里就不赘述了。如果这个方案满足你的要求,那它就是一个非常好的选择.
注意:这里要使用UNION ALL,而不是UNION。后者会在合并的时候会将重复的条目清除掉。在我们的这个示例中没有去除重复的需求,所以我们告诉服务器不要清除重复,清除重复在这个问题中是无用的,而且会造成性能的大幅下降。
使用用户自定义变量
但结果是数据表中很小一部分条目并且有索引用来排序的时候,使用UNION的方式是一个很好的选择。而当你要获取数据表中大部分条目时也有一种能达到线性时间的方法,那就是使用用户定义变量。这里我将介绍的仅仅是mysql中的用法。在我原来的博客在mysql中,如何为条目编号(How
to number rows in MySQL)里介绍了它是怎么工作的:
1 |
set @num := 0 , @type := '' ; |
2 |
select
type, variety, price |
4 |
select
type, variety, price, |
5 |
@num := if ( @type =
type, @num + 1 , 1 )
as row_number, |
9 |
)
as x where x.row_number <= 2 ; |
这个方法并不仅仅做单次扫描,子查询在后台创建临时表,然后通过一次扫描将数据填充进去,然后在临时表中选择数据用于主查询的WHERE语句。但即使是两次扫描,它的时间复杂度仍是O(n),这里n是表示数据表的行数。它远比上面的相关子查询的结果O(n
^ 2)要好许多, 这里的n表示的是分组中平均条目数 - 即使是中等规模的数据也会造成极差的性能。(假设每种水果中有5
varitey,那么就需要25次扫描)
在MySQL中一次扫描的方法
如果你无法放弃你头脑中优化查询的想法,你可以试试这个方法,它不使用临时表,并且只做一次扫描
1 |
set @num := 0 , @type := '' ; |
3 |
select
type, variety, price, |
4 |
@num := if ( @type =
type, @num + 1 , 1 )
as row_number, |
7 |
group
by type, price, variety |
8 |
having
row_number <= 2 ; |
只要MySQL的GROUP BY语句符合标准,这个方式在理论上就是是可行。那么实际上可行吗?下面是我在MySQL 5.0.7的Windows 版上的结果
01 |
+--------+----------+-------+------------+--------+ |
02 |
|
type | variety | price | row_number | dummy | |
03 |
+--------+----------+-------+------------+--------+ |
04 |
|
apple | gala | 2.79 | 1 |
apple | |
05 |
|
apple | fuji | 0.24 | 3 |
apple | |
06 |
|
orange | valencia | 3.59 | 1 |
orange | |
07 |
|
orange | navel | 9.36 | 3 |
orange | |
08 |
|
pear | bradford | 6.05 | 1 |
pear | |
09 |
|
pear | bartlett | 2.14 | 3 |
pear | |
10 |
|
cherry | bing | 2.55 | 1 |
cherry | |
11 |
|
cherry | chelan | 6.33 | 3 |
cherry | |
12 |
+--------+----------+-------+------------+--------+ |
可以看到,这已经和结果很接近了。他返回了每个分组的第一行和第三行,结果并没有按照price的升序进行排列。当时HAVING 语句要求row_number不应当大于2。接下来是5.0.24a 在ubuntu上的结果:
01 |
+--------+------------+-------+------------+--------+ |
02 |
|
type | variety | price | row_number | dummy | |
03 |
+--------+------------+-------+------------+--------+ |
04 |
|
apple | fuji | 0.24 | 1 |
apple | |
05 |
|
apple | gala | 2.79 | 1 |
apple | |
06 |
|
apple | limbertwig | 2.87 | 1 |
apple | |
07 |
|
cherry | bing | 2.55 | 1 |
cherry | |
08 |
|
cherry | chelan | 6.33 | 1 |
cherry | |
09 |
|
orange | valencia | 3.59 | 1 |
orange | |
10 |
|
orange | navel | 9.36 | 1 |
orange | |
11 |
|
pear | bartlett | 2.14 | 1 |
pear | |
12 |
|
pear | bradford | 6.05 | 1 |
pear | |
13 |
+--------+------------+-------+------------+--------+ |
这次,所有的row_number都是1,而且好像所有行都返回了。可以参考MySQL手册用户自定义变量。
使用这种技术的结果很难确定,主要是因为这里涉及的技术是你和我都不能直接接触的,例如MySQL在Group的时候使用哪个索引。如果你仍需要使用它 -
我知道很多人已经用了,因为我告诉了他们 -
你还是可以用的。我们正在进入SQL的真正领域,但是上面的结果是在没有设置索引的情况下得到的。我们现在看看了设置了索引之后group的结果是什么。
1 |
alter
table fruits add key(type, price); |
执行之后会发现没有什么变化,之后使用EXPLAIN查看查询过程,会发现此查询没有使用任何索引。这是为什么呢?因为Group使用了3个字段,但是索引只有两个字段。实际上,查询仍使用了临时表,所有我们并没完成一次扫描的目标。我们可以强制使用索引:
1 |
set @num := 0 , @type := '' ; |
3 |
select
type, variety, price, |
4 |
@num := if ( @type =
type, @num + 1 , 1 )
as row_number, |
6 |
from
fruits force index(type) |
7 |
group
by type, price, variety |
8 |
having
row_number <= 2 ; |
我们看一下是否起作用了。
01 |
+--------+----------+-------+------------+--------+ |
02 |
|
type | variety | price | row_number | dummy | |
03 |
+--------+----------+-------+------------+--------+ |
04 |
|
apple | fuji | 0.24 | 1 |
apple | |
05 |
|
apple | gala | 2.79 | 2 |
apple | |
06 |
|
cherry | bing | 2.55 | 1 |
cherry | |
07 |
|
cherry | chelan | 6.33 | 2 |
cherry | |
08 |
|
orange | valencia | 3.59 | 1 |
orange | |
09 |
|
orange | navel | 9.36 | 2 |
orange | |
10 |
|
pear | bartlett | 2.14 | 1 |
pear | |
11 |
|
pear | bradford | 6.05 | 2 |
pear | |
12 |
+--------+----------+-------+------------+--------+ |
现在我们得到了我们想要的结果了,而且没有文件排序(filesort)和临时表。还有一种方法就是将variety提出到GROUP BY之外,这样它就可以使用自己的索引。因为这个查询是一个从分组中查询非分组字段的查询,它只能在
ONLY_FULL_GROUP_BY 模式关闭(链接)的情况下才能起作用。但是在没有特殊原因的情况下,我不建议你这么做。
其他方法
可以在评论中看到其他的方法,里面有的确有一些非常梦幻的方法。我一直在你们的评论获取知识,感谢你们。
总结
我们这里介绍了集中方法去解决“每个分组中最大的条目”这类问题已经进一步扩展到查询每组中前N个条目的方法。之后我们深入探讨了一些MySQL特定的技术,这些技术看起来有一些傻和笨。但是如果你需要榨干服务器的最后一点性能,你就需要知道什么时候去打破规则。对于那些认为这是MySQL本身的问题的人,我要说这不是,我曾经看到过使用其他平台的人也在做着同样的事情,如SQL
Server。在每个平台上都会有很多特殊的小技巧和花招,使用他们的人必须去适应它。
原文出处: http://my.oschina.net/u/1032146/blog/149300
- 如何在MySQL中查询每个分组的前几名【转】
问题 在工作中常会遇到将数据分组排序的问题,如在考试成绩中,找出每个班级的前五名等. 在orcale等数据库中可以使用partition语句来解决,但在mysql中就比较麻烦了.这次翻译的文章就是专门 ...
- mysql 查询每个分组的前几名
按分组排序,并查出每个分组的前3名 单表 SELECT * FROM ( SELECT ZONEID, uid, NAME, fight, IF ( , ) AS rank, ( @zone := z ...
- 如何在MySQL中查询当前数据上一条和下一条的记录
如果ID是主键或者有索引,可以直接查找: 方法一: 查询上一条记录的SQL语句(如果有其他的查询条件记得加上other_conditions以免出现不必要的错误): select * from tab ...
- 如何查找MySQL中查询慢的SQL语句
如何查找MySQL中查询慢的SQL语句 更多 如何在mysql查找效率慢的SQL语句呢?这可能是困然很多人的一个问题,MySQL通过慢查询日志定位那些执行效率较低的SQL 语句,用--log-slow ...
- 如何在MySQL中获得更好的全文搜索结果
如何在MySQL中获得更好的全文搜索结果 很多互联网应用程序都提供了全文搜索功能,用户可以使用一个词或者词语片断作为查询项目来定位匹配的记录.在后台,这些程序使用在一个SELECT 查询中的LIKE语 ...
- 如何查找MySQL中查询慢的SQL语句(转载)
转载自https://www.cnblogs.com/qmfsun/p/4844472.html 如何在mysql查找效率慢的SQL语句呢?这可能是困然很多人的一个问题,MySQL通过慢查询日志定位那 ...
- [django/mysql] 使用distinct在mysql中查询多条不重复记录值的解决办法
前言:不废话.,直接进入正文 正文: 如何使用distinct在mysql中查询多条不重复记录值? 首先,我们必须知道在django中模型执行查询有两种方法: 第一种,使用django给出的api,例 ...
- mysql中查询一个字段属于哪一个数据库中的哪一个表的方式
mysql中查询一个字段具体是属于哪一个数据库的那一张表:用这条语句就能查询出来,其中 table_schema 是所在库, table_name 是所在表 --mysql中查询某一个字段名属于哪一个 ...
- 如何在mysql中存储音乐和图片文件
如何在mysql中存储音乐和图片文件? 果你想把二进制的数据,比如说图片文件和HTML文件,直接保存在你的MySQL数据库,那么这篇文章就是为你而写的! 我将告诉你怎样通过HTML表单来储存这些文件, ...
随机推荐
- java线程安全问题之静态变量、实例变量、局部变量
java多线程编程中,存在很多线程安全问题,至于什么是线程安全呢,给出一个通俗易懂的概念还是蛮难的,如同<java并发编程实践>中所说: 写道 给线程安全下定义比较困难.存在很多种定义,如 ...
- java链接mysql 中文乱码
{转!} 背景: 由于最近在开发一个APP的后台程序,需要Java连接远程的MySQL数据库进行数据的更新和查询操作,并且插入的数据里有中文,在插入到数据库后发现中文都是乱码.网上查了很多教程,最后都 ...
- 给zTree添加onSelect callback
由于jquery ui没有集成tree控件,所以在网上找到了zTree v3.zTree无疑是功能非常强大的一款jquery的插件,而且是国人作品,zTree的文档写得也非常好,效果很炫.但是我在使用 ...
- Hibernate的七种映射关系之七种关联映射(一)
关联映射就是将关联关系映射到数据库里,在对象模型中就是一个或多个引用. 一.Hibernate多对一关联映射:就是在“多”的一端加外键,指向“一”的一端. 比如多个学生对应一个班级,多个用户对应一个级 ...
- 什么是REST设计风格
https://zh.wikipedia.org/wiki/REST http://www.infoq.com/cn/articles/rest-introduction
- zookeeper安装和dubbo-admin使用
简介 ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件.它是一个为分布式应用提供一致性服务的软件,提 ...
- pycahrm使用docstrings来指定变量类型、返回值类型、函数参数类型
py里面不需要显示声明类型,这和java c这些静态语言不同,虽然python这样做少了一些代码和写代码的困难度,但还是非常多的弊端的,运行速度 代码安全, 这些都是语言本身带来的本的弊端,这些没办法 ...
- selenium之 chromedriver与chrome版本映射表(更新至v2.31)
转自:http://blog.csdn.net/huilan_same/article/details/51896672 chromedriver版本 支持的Chrome版本 v2.31 v58-60 ...
- RF-获取上个月份
验证1: case ${NowDate} set variable 20170103 ${year} set variable ${NowDate[0:4]} ${month} set variabl ...
- C#调用DLL报“试图加载格式不正确的程序”
项目右键属性->项目设计器->生成->平台->把'默认设置(任何 CPU)'改为x86或者x64