问题

在工作中常会遇到将数据分组排序的问题,如在考试成绩中,找出每个班级的前五名等。 在orcale等数据库中可以使用partition语句来解决,但在mysql中就比较麻烦了。这次翻译的文章就是专门解决这个问题的

原文地址: How to select the first/least/max row per group in SQL

翻译

在使用SQL的过程中,我们经常遇到这样一类问题:如何找出每个程序最近的日志条目?如何找出每个用户的最高分?在每个分类中最受欢迎的商品是什么?通常这类“找出每个分组中最高分的条目”的问题可以使用相同的技术来解决。在这篇文章里我将介绍如何解决这类问题,而且会介绍如何找出最高的前几名而不仅仅是第一名。

这篇文章会用到行数(row number),我在原来的文章 MySQL-specificgeneric techniques 中已经提到过如何为每个分组设置行数了。在这里我会使用与原来的文章中相同的表格,但会加入新的price 字段

+--------+------------+-------+
| type | variety | price |
+--------+------------+-------+
| apple | gala | 2.79 |
| apple | fuji | 0.24 |
| apple | limbertwig | 2.87 |
| orange | valencia | 3.59 |
| orange | navel | 9.36 |
| pear | bradford | 6.05 |
| pear | bartlett | 2.14 |
| cherry | bing | 2.55 |
| cherry | chelan | 6.33 |
+--------+------------+-------+

选择每个分组中的最高分

这里我们要说的是如何找出每个程序最新的日志记录或审核表中最近的更新或其他类似的排序问题。这类问题在IRC频道和邮件列表中出现的越来越频繁。我使用水果问题来作为示例,在示例中我们要选出每类水果中最便宜的一个,我们期望的结果如下

+--------+----------+-------+
| type | variety | price |
+--------+----------+-------+
| apple | fuji | 0.24 |
| orange | valencia | 3.59 |
| pear | bartlett | 2.14 |
| cherry | bing | 2.55 |
+--------+----------+-------+

这个问题有几种解法,但基本上就是这两步:找出最低的价格,然后找出和这个价格同一行的其他数据

其中一个常用的方法是使用自连接(self-join),第一步根据type(apple, cherry etc)进行分组,并找出每组中price的最小值

select type, min(price) as minprice
from fruits
group by type;
+--------+----------+
| type | minprice |
+--------+----------+
| apple | 0.24 |
| cherry | 2.55 |
| orange | 3.59 |
| pear | 2.14 |
+--------+----------+

第二步是将刚刚结果与原来的表进行连接。既然刚刚给结果已经被分组了,我们将刚刚的查询语句作为子查询以便于连接没有被分组的原始表格。

select f.type, f.variety, f.price
from (
select type, min(price) as minprice
from fruits group by type
) as x inner join fruits as f on f.type = x.type and f.price = x.minprice; +--------+----------+-------+
| type | variety | price |
+--------+----------+-------+
| apple | fuji | 0.24 |
| cherry | bing | 2.55 |
| orange | valencia | 3.59 |
| pear | bartlett | 2.14 |
+--------+----------+-------+

还可以使用相关子查询(correlated subquery)的方式来解决。这种方法在不同的mysql优化系统下,可能性能会有一点点下降,但这种方法会更直观一些。

select type, variety, price
from fruits
where price = (select min(price) from fruits as f where f.type = fruits.type);
+--------+----------+-------+
| type | variety | price |
+--------+----------+-------+
| apple | fuji | 0.24 |
| orange | valencia | 3.59 |
| pear | bartlett | 2.14 |
| cherry | bing | 2.55 |
+--------+----------+-------+

这两种查询在逻辑上是一样的,他们性能也基本相同。

找出每组中前N个值

这个问题会稍微复杂一些。我们可以使用聚集函数(MIN(), MAX()等等)来找一行,但是找前几行不能直接使用这些函数,因为它们都只返回一个值。但这个问题还是可以解决的。

这次我们找出每个类型(type)中最便宜的前两种水果,首先我们尝试

select type, variety, price
from fruits
where price = (select min(price) from fruits as f where f.type = fruits.type)
or price = (select min(price) from fruits as f where f.type = fruits.type
and price > (select min(price) from fruits as f2 where f2.type = fruits.type));
+--------+----------+-------+
| type | variety | price |
+--------+----------+-------+
| apple | gala | 2.79 |
| apple | fuji | 0.24 |
| orange | valencia | 3.59 |
| orange | navel | 9.36 |
| pear | bradford | 6.05 |
| pear | bartlett | 2.14 |
| cherry | bing | 2.55 |
| cherry | chelan | 6.33 |
+--------+----------+-------+

是的,我们可以写成自连接(self-join)的形式,但是仍不够好(我将这个练习留给读者)。这种方式在N变大(前三名,前4名)的时候性能会越来越差。我们可以使用其他的表现形式编写这个查询,但是它们都不够好,它们都相当的笨重和效率低下。(译者注:这种方式获取的结果时,如果第N个排名是重复的时候最后选择的结果会超过N,比如上面例子还有一个apple价格也是0.24,那最后的结果就会有3个apple)

我们有一种稍好的方式,在每个种类中选择不超过该种类第二便宜的水果

select type, variety, price
from fruits
where (
select count(*) from fruits as f
where f.type = fruits.type and f.price <= fruits.price
) <= 2;

这次的代码要优雅很多,而且在N增加时不需要重新代码(非常棒!)。但是这个查询在功能上和原来的是一样。他们的时间复杂度均为分组中条目数的二次方。而且,很多优化器都不能优化这种查询,使得它的耗时最好为全表行数的二次方(尤其在没有设置正确的索引时),而且数据量大时,可能将服务器会停止响应。那么还有更好的方法吗?有没有办法可以仅仅扫描一次数据,而不是通过子查询进行多次扫描。(译者注:这种方法有一个问题,就是如果排名并列第一的数字超过N后,这个分组会选不出数据,比如price为2.79的apple有3个,那么结果中就没有apple了)

使用 UNION

如果已经为type, price设置了索引,而且在每个分组中去除的数据要多于包含的数据,一种非常高效的单次扫描的方法是将查询拆分成多个独立的查询(尤其对mysql,对其他的RDBMSs也有效),再使用UNION将结果拼到一起。mysql的写法如下:

(select * from fruits where type = 'apple' order by price limit 2)
union all
(select * from fruits where type = 'orange' order by price limit 2)
union all
(select * from fruits where type = 'pear' order by price limit 2)
union all
(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)里介绍了它是怎么工作的:

set @num := 0, @type := '';
select type, variety, price
from (
select type, variety, price,
@num := if(@type = type, @num + 1, 1) as row_number,
@type := type as dummy
from fruits
order by type, price
) as x where x.row_number <= 2;

这个方法并不仅仅做单次扫描,子查询在后台创建临时表,然后通过一次扫描将数据填充进去,然后在临时表中选择数据用于主查询的WHERE语句。但即使是两次扫描,它的时间复杂度仍是O(n),这里n是表示数据表的行数。它远比上面的相关子查询的结果O(n ^ 2)要好许多, 这里的n表示的是分组中平均条目数 - 即使是中等规模的数据也会造成极差的性能。(假设每种水果中有5 varitey,那么就需要25次扫描)

在MySQL中一次扫描的方法

如果你无法放弃你头脑中优化查询的想法,你可以试试这个方法,它不使用临时表,并且只做一次扫描

set @num := 0, @type := '';

select type, variety, price,
@num := if(@type = type, @num + 1, 1) as row_number,
@type := type as dummy
from fruits
group by type, price, variety
having row_number <= 2;

只要MySQL的GROUP BY语句符合标准,这个方式在理论上就是是可行。那么实际上可行吗?下面是我在MySQL 5.0.7的Windows 版上的结果

+--------+----------+-------+------------+--------+
| type | variety | price | row_number | dummy |
+--------+----------+-------+------------+--------+
| apple | gala | 2.79 | 1 | apple |
| apple | fuji | 0.24 | 3 | apple |
| orange | valencia | 3.59 | 1 | orange |
| orange | navel | 9.36 | 3 | orange |
| pear | bradford | 6.05 | 1 | pear |
| pear | bartlett | 2.14 | 3 | pear |
| cherry | bing | 2.55 | 1 | cherry |
| cherry | chelan | 6.33 | 3 | cherry |
+--------+----------+-------+------------+--------+

可以看到,这已经和结果很接近了。他返回了每个分组的第一行和第三行,结果并没有按照price的升序进行排列。当时HAVING 语句要求row_number不应当大于2。接下来是5.0.24a 在ubuntu上的结果:

+--------+------------+-------+------------+--------+
| type | variety | price | row_number | dummy |
+--------+------------+-------+------------+--------+
| apple | fuji | 0.24 | 1 | apple |
| apple | gala | 2.79 | 1 | apple |
| apple | limbertwig | 2.87 | 1 | apple |
| cherry | bing | 2.55 | 1 | cherry |
| cherry | chelan | 6.33 | 1 | cherry |
| orange | valencia | 3.59 | 1 | orange |
| orange | navel | 9.36 | 1 | orange |
| pear | bartlett | 2.14 | 1 | pear |
| pear | bradford | 6.05 | 1 | pear |
+--------+------------+-------+------------+--------+

这次,所有的row_number都是1,而且好像所有行都返回了。可以参考MySQL手册用户自定义变量

使用这种技术的结果很难确定,主要是因为这里涉及的技术是你和我都不能直接接触的,例如MySQL在Group的时候使用哪个索引。如果你仍需要使用它 - 我知道很多人已经用了,因为我告诉了他们 - 你还是可以用的。我们正在进入SQL的真正领域,但是上面的结果是在没有设置索引的情况下得到的。我们现在看看了设置了索引之后group的结果是什么。

alter table fruits add key(type, price);

执行之后会发现没有什么变化,之后使用EXPLAIN查看查询过程,会发现此查询没有使用任何索引。这是为什么呢?因为Group使用了3个字段,但是索引只有两个字段。实际上,查询仍使用了临时表,所有我们并没完成一次扫描的目标。我们可以强制使用索引:

set @num := 0, @type := '';

select type, variety, price,
@num := if(@type = type, @num + 1, 1) as row_number,
@type := type as dummy
from fruits force index(type)
group by type, price, variety
having row_number <= 2;

我们看一下是否起作用了。

+--------+----------+-------+------------+--------+
| type | variety | price | row_number | dummy |
+--------+----------+-------+------------+--------+
| apple | fuji | 0.24 | 1 | apple |
| apple | gala | 2.79 | 2 | apple |
| cherry | bing | 2.55 | 1 | cherry |
| cherry | chelan | 6.33 | 2 | cherry |
| orange | valencia | 3.59 | 1 | orange |
| orange | navel | 9.36 | 2 | orange |
| pear | bartlett | 2.14 | 1 | pear |
| pear | bradford | 6.05 | 2 | pear |
+--------+----------+-------+------------+--------+

现在我们得到了我们想要的结果了,而且没有文件排序(filesort)和临时表。还有一种方法就是将variety提出到GROUP BY之外,这样它就可以使用自己的索引。因为这个查询是一个从分组中查询非分组字段的查询,它只能在 ONLY_FULL_GROUP_BY 模式关闭(链接)的情况下才能起作用。但是在没有特殊原因的情况下,我不建议你这么做。

其他方法

可以在评论中看到其他的方法,里面有的确有一些非常梦幻的方法。我一直在你们的评论获取知识,感谢你们。

总结

我们这里介绍了集中方法去解决“每个分组中最大的条目”这类问题已经进一步扩展到查询每组中前N个条目的方法。之后我们深入探讨了一些MySQL特定的技术,这些技术看起来有一些傻和笨。但是如果你需要榨干服务器的最后一点性能,你就需要知道什么时候去打破规则。对于那些认为这是MySQL本身的问题的人,我要说这不是,我曾经看到过使用其他平台的人也在做着同样的事情,如SQL Server。在每个平台上都会有很多特殊的小技巧和花招,使用他们的人必须去适应它。

本文转自:[翻译]如何在mysql中查询每个分组的前几名

如何在MySQL中查询每个分组的前几名【转】的更多相关文章

  1. 如何在mysql中查询每个分组的前几名

    问题 在工作中常会遇到将数据分组排序的问题,如在考试成绩中,找出每个班级的前五名等.  在orcale等数据库中可以使用partition 语句来解决,但在MySQL中就比较麻烦了.这次翻译的文章就是 ...

  2. mysql 查询每个分组的前几名

    按分组排序,并查出每个分组的前3名 单表 SELECT * FROM ( SELECT ZONEID, uid, NAME, fight, IF ( , ) AS rank, ( @zone := z ...

  3. 如何在MySQL中查询当前数据上一条和下一条的记录

    如果ID是主键或者有索引,可以直接查找: 方法一: 查询上一条记录的SQL语句(如果有其他的查询条件记得加上other_conditions以免出现不必要的错误): select * from tab ...

  4. 如何查找MySQL中查询慢的SQL语句

    如何查找MySQL中查询慢的SQL语句 更多 如何在mysql查找效率慢的SQL语句呢?这可能是困然很多人的一个问题,MySQL通过慢查询日志定位那些执行效率较低的SQL 语句,用--log-slow ...

  5. 如何在MySQL中获得更好的全文搜索结果

    如何在MySQL中获得更好的全文搜索结果 很多互联网应用程序都提供了全文搜索功能,用户可以使用一个词或者词语片断作为查询项目来定位匹配的记录.在后台,这些程序使用在一个SELECT 查询中的LIKE语 ...

  6. 如何查找MySQL中查询慢的SQL语句(转载)

    转载自https://www.cnblogs.com/qmfsun/p/4844472.html 如何在mysql查找效率慢的SQL语句呢?这可能是困然很多人的一个问题,MySQL通过慢查询日志定位那 ...

  7. [django/mysql] 使用distinct在mysql中查询多条不重复记录值的解决办法

    前言:不废话.,直接进入正文 正文: 如何使用distinct在mysql中查询多条不重复记录值? 首先,我们必须知道在django中模型执行查询有两种方法: 第一种,使用django给出的api,例 ...

  8. mysql中查询一个字段属于哪一个数据库中的哪一个表的方式

    mysql中查询一个字段具体是属于哪一个数据库的那一张表:用这条语句就能查询出来,其中 table_schema 是所在库, table_name 是所在表 --mysql中查询某一个字段名属于哪一个 ...

  9. 如何在mysql中存储音乐和图片文件

    如何在mysql中存储音乐和图片文件? 果你想把二进制的数据,比如说图片文件和HTML文件,直接保存在你的MySQL数据库,那么这篇文章就是为你而写的! 我将告诉你怎样通过HTML表单来储存这些文件, ...

随机推荐

  1. 佛祖镇楼,BUG避易

    def FZZL(): print(" _ooOoo_ ") print(" o8888888o ") print(" 88 . 88 ") ...

  2. ElasticSearch本地调测环境构建

    ElasicSearch版本:6.0.0:https://github.com/elastic/elasticsearch.git 1:安装JVM(JVM1.8以上) 2:安装gradle(3.3以上 ...

  3. C Primer Plus 第6章 C控制语句:循环 编程练习

    记录下写的最后几题. 14. #include <stdio.h> int main() { double value[8]; double value2[8]; int index; f ...

  4. java多线程对CountDownLatch的使用实例

    介绍 CountDownLatch是一个同步辅助类,它允许一个或多个线程一直等待直到其他线程执行完毕才开始执行. 用给定的计数初始化CountDownLatch,其含义是要被等待执行完的线程个数. 每 ...

  5. PAT1056:Mice and Rice

    1056. Mice and Rice (25) 时间限制 100 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Yue Mice an ...

  6. shell 中test命令

    test可用于测试表达式,支持测试的范围包括:字符串比较,算术比较,文件存在性.属性.类型等判断.例如,判断文件是否为空.文件是否存在.是否是目录.变量是否大于5.字符串是否等于"longs ...

  7. CentOS6.7 mysql5.6.33修改数据文件位置

    问题:mysql存放的数据文件,分区容量较小,目前已经满,导致mysql连接不上, 解决方案: 1.删除分区里一个不需要用的数据,如:日志文件等(解决不了根本问题) 2.对某个磁盘扩容 3.修改数据存 ...

  8. SwaggerUI--SosoApi

    1.SwaggerUI是什么? Swagger UI是一款RESTFUL接口的文档在线自动生成+功能测试功能软件. Swagger-UI 的官方地址:http://swagger.io/ Github ...

  9. Node.js使用supervisor遭遇‘supervisor’不是内部或外部命令,如果解决?

    如果你有 PHP 开发经验,会习惯在修改 PHP 脚本后直接刷新浏览器以观察结果,而你 在开发 Node.js 实现的 HTTP 应用时会发现,无论你修改了代码的哪一部份,都必须终止 Node.js ...

  10. How nginx "location if" works

    Nginx's if directive does have some weirdness in practice. And people may misuse it when they do not ...