如何在 MYSQL 中选择具有 MAX(列值)的行,按另一列进行分区?
Posted
技术标签:
【中文标题】如何在 MYSQL 中选择具有 MAX(列值)的行,按另一列进行分区?【英文标题】:How can I SELECT rows with MAX(Column value), PARTITION by another column in MYSQL? 【发布时间】:2009-03-04 20:14:26 【问题描述】:我的桌子是:
id | home | datetime | player | resource |
---|---|---|---|---|
1 | 10 | 04/03/2009 | john | 399 |
2 | 11 | 04/03/2009 | juliet | 244 |
5 | 12 | 04/03/2009 | borat | 555 |
3 | 10 | 03/03/2009 | john | 300 |
4 | 11 | 03/03/2009 | juliet | 200 |
6 | 12 | 03/03/2009 | borat | 500 |
7 | 13 | 24/12/2008 | borat | 600 |
8 | 13 | 01/01/2009 | borat | 700 |
我需要选择每个不同的home
保持datetime
的最大值。
结果是:
id | home | datetime | player | resource |
---|---|---|---|---|
1 | 10 | 04/03/2009 | john | 399 |
2 | 11 | 04/03/2009 | juliet | 244 |
5 | 12 | 04/03/2009 | borat | 555 |
8 | 13 | 01/01/2009 | borat | 700 |
我试过了:
-- 1 ..by the mysql manual:
SELECT DISTINCT
home,
id,
datetime AS dt,
player,
resource
FROM topten t1
WHERE datetime = (SELECT
MAX(t2.datetime)
FROM topten t2
GROUP BY home)
GROUP BY datetime
ORDER BY datetime DESC
不起作用。结果集有 130 行,尽管数据库有 187 行。
结果包括home
的一些重复项。
-- 2 ..join
SELECT
s1.id,
s1.home,
s1.datetime,
s1.player,
s1.resource
FROM topten s1
JOIN (SELECT
id,
MAX(datetime) AS dt
FROM topten
GROUP BY id) AS s2
ON s1.id = s2.id
ORDER BY datetime
不。提供所有记录。
-- 3 ..something exotic:
各种结果。
【问题讨论】:
【参考方案1】:你离得太近了!您需要做的就是选择住宅及其最大日期时间,然后在 BOTH 字段中加入 topten
表:
SELECT tt.*
FROM topten tt
INNER JOIN
(SELECT home, MAX(datetime) AS MaxDateTime
FROM topten
GROUP BY home) groupedtt
ON tt.home = groupedtt.home
AND tt.datetime = groupedtt.MaxDateTime
【讨论】:
测试它是否不同,如果两个相等的最大日期时间在同一个家中(与不同的玩家) 我认为执行此操作的经典方法是使用自然连接:“SELECT tt.* FROM topten tt NATURAL JOIN (SELECT home, MAX(datetime) AS datetime FROM topten GROUP BY home) mostrecent; "完全相同的查询,但可以说更具可读性 如果有两行具有相同的 'home' 和 'datetime' 字段值怎么办? @Young 您的查询的问题是它可能会返回给定家庭的非最大行的id
、player
和resource
,即对于 home = 10,您可能会得到:@ 987654326@ 换句话说,它不能保证结果集中一行的所有列都属于给定主页的 max(datetime)。
关于@KemalDuran 上面的评论,如果有两行具有相同的 home 和 datetime 字段,您需要做的是采用 Michael La Voie 的解决方案并将 MAX(id) AS MaxID
添加到内部SELECT
语句,然后在末尾添加另一行 AND tt.id = groupedtt.MaxID
。【参考方案2】:
最快的MySQL
解决方案,无需内部查询,无需GROUP BY
:
SELECT m.* -- get the row that contains the max value
FROM topten m -- "m" from "max"
LEFT JOIN topten b -- "b" from "bigger"
ON m.home = b.home -- match "max" row with "bigger" row by `home`
AND m.datetime < b.datetime -- want "bigger" than "max"
WHERE b.datetime IS NULL -- keep only if there is no bigger than max
解释:
使用home
列将表与自身连接起来。使用LEFT JOIN
可确保表m
中的所有行都出现在结果集中。那些在表b
中没有匹配项的将有NULL
s 用于b
的列。
JOIN
上的另一个条件要求仅匹配来自b
的行,这些行在datetime
列上的值大于来自m
的行。
使用问题中发布的数据,LEFT JOIN
将生成以下对:
+------------------------------------------+--------------------------------+
| the row from `m` | the matching row from `b` |
|------------------------------------------|--------------------------------|
| id home datetime player resource | id home datetime ... |
|----|-----|------------|--------|---------|------|------|------------|-----|
| 1 | 10 | 04/03/2009 | john | 399 | NULL | NULL | NULL | ... | *
| 2 | 11 | 04/03/2009 | juliet | 244 | NULL | NULL | NULL | ... | *
| 5 | 12 | 04/03/2009 | borat | 555 | NULL | NULL | NULL | ... | *
| 3 | 10 | 03/03/2009 | john | 300 | 1 | 10 | 04/03/2009 | ... |
| 4 | 11 | 03/03/2009 | juliet | 200 | 2 | 11 | 04/03/2009 | ... |
| 6 | 12 | 03/03/2009 | borat | 500 | 5 | 12 | 04/03/2009 | ... |
| 7 | 13 | 24/12/2008 | borat | 600 | 8 | 13 | 01/01/2009 | ... |
| 8 | 13 | 01/01/2009 | borat | 700 | NULL | NULL | NULL | ... | *
+------------------------------------------+--------------------------------+
最后,WHERE
子句只保留b
列中具有NULL
s 的对(在上表中用*
标记);这意味着,由于JOIN
子句的第二个条件,从m
中选择的行在datetime
列中具有最大值。
阅读SQL Antipatterns: Avoiding the Pitfalls of Database Programming 书以了解其他 SQL 技巧。
【讨论】:
使用SQLite
,当匹配列(即“home”)上没有索引时,第一个比 La Voie 的版本慢得多。 (用 24k 行测试得到 13k 行)
这是最好的答案,如果你显示执行计划,你会看到这个查询少一步
如果两行有相同的home
和datetime
并且datetime
是该特定home
的最大值会发生什么?
@AjaxLeung 列上的索引 home
和 datetime
。作为一般规则,如果索引包含在ON
、WHERE
或ORDER BY
子句中使用的列,则索引会有所帮助。但是,这取决于列的使用方式。如果在表达式中使用列,则索引是无用的。将EXPLAIN
放在find out what indexes are used (and how) 的查询前面。
这个想法对我有用。它有助于简化我的子查询【参考方案3】:
这里是 T-SQL 版本:
-- Test data
DECLARE @TestTable TABLE (id INT, home INT, date DATETIME,
player VARCHAR(20), resource INT)
INSERT INTO @TestTable
SELECT 1, 10, '2009-03-04', 'john', 399 UNION
SELECT 2, 11, '2009-03-04', 'juliet', 244 UNION
SELECT 5, 12, '2009-03-04', 'borat', 555 UNION
SELECT 3, 10, '2009-03-03', 'john', 300 UNION
SELECT 4, 11, '2009-03-03', 'juliet', 200 UNION
SELECT 6, 12, '2009-03-03', 'borat', 500 UNION
SELECT 7, 13, '2008-12-24', 'borat', 600 UNION
SELECT 8, 13, '2009-01-01', 'borat', 700
-- Answer
SELECT id, home, date, player, resource
FROM (SELECT id, home, date, player, resource,
RANK() OVER (PARTITION BY home ORDER BY date DESC) N
FROM @TestTable
)M WHERE N = 1
-- and if you really want only home with max date
SELECT T.id, T.home, T.date, T.player, T.resource
FROM @TestTable T
INNER JOIN
( SELECT TI.id, TI.home, TI.date,
RANK() OVER (PARTITION BY TI.home ORDER BY TI.date) N
FROM @TestTable TI
WHERE TI.date IN (SELECT MAX(TM.date) FROM @TestTable TM)
)TJ ON TJ.N = 1 AND T.id = TJ.id
编辑 不幸的是,MySQL 中没有 RANK() OVER 函数。 但可以模拟,见Emulating Analytic (AKA Ranking) Functions with MySQL。 所以这是 MySQL 版本:
SELECT id, home, date, player, resource
FROM TestTable AS t1
WHERE
(SELECT COUNT(*)
FROM TestTable AS t2
WHERE t2.home = t1.home AND t2.date > t1.date
) = 0
【讨论】:
@MaxGontar,你的 mysql 解决方案很不错,谢谢。如果在 @_TestTable 中删除 row#1>: SELECT 1, 10, '2009-03-04', 'john', 399 会怎样,也就是说,如果给定的房屋价值只有一行怎么办?谢谢。 BUG:将“RANK()”替换为“ROW_NUMBER()”。如果您有平局(由重复的日期值引起),您将有两条记录,其中 N 为“1”。【参考方案4】:即使每个home
有两行或多行且DATETIME
的行数相等,这也可以:
SELECT id, home, datetime, player, resource
FROM (
SELECT (
SELECT id
FROM topten ti
WHERE ti.home = t1.home
ORDER BY
ti.datetime DESC
LIMIT 1
) lid
FROM (
SELECT DISTINCT home
FROM topten
) t1
) ro, topten t2
WHERE t2.id = ro.lid
【讨论】:
在表格中添加了盖子字段,不好 这个没有在 phpMyAdmin 上执行。页面刷新但没有结果也没有错误..?WHERE ti.home = t1.home
- 你能解释一下语法吗?
@IstiaqueAhmed:你在这里不明白的到底是什么?是关联查询,你提到的表达式是关联条件。
@Quassnoi,具有 WHERE ti.home = t1.home
行的 select
查询不需要定义 t1
的 FROM
子句。那么它是如何使用的呢?【参考方案5】:
我认为这会给你想要的结果:
SELECT home, MAX(datetime)
FROM my_table
GROUP BY home
但是如果您还需要其他列,只需与原始表进行连接(检查Michael La Voie
答案)
最好的问候。
【讨论】:
他还需要其他栏目。 id、家庭、日期时间、播放器、资源【参考方案6】:由于人们似乎不断地跑到这个帖子上(评论日期范围从 1.5 年开始),这并没有这么简单:
SELECT * FROM (SELECT * FROM topten ORDER BY datetime DESC) tmp GROUP BY home
不需要聚合函数...
干杯。
【讨论】:
这似乎不起作用。错误消息:选择列表中的“x”列无效,因为它既不包含在聚合函数中,也不包含在 GROUP BY 子句中。 这在 SQL Server 或 Oracle 中肯定行不通,尽管它看起来可能在 MySQL 中可行。 这真的很漂亮!这是如何运作的?通过使用 DESC 和默认组返回列?因此,如果我将其更改为 datetime ASC,它会返回每个家庭的最早行吗? 这太棒了! 如果您有非聚合列(在 MySQL 中),这种方法将不起作用。【参考方案7】:你也可以试试这个,对于大表的查询性能会更好。当每个家庭的记录不超过两个并且它们的日期不同时,它就会起作用。更好的通用 MySQL 查询来自上面的 Michael La Voie。
SELECT t1.id, t1.home, t1.date, t1.player, t1.resource
FROM t_scores_1 t1
INNER JOIN t_scores_1 t2
ON t1.home = t2.home
WHERE t1.date > t2.date
或者如果是 Postgres 或那些提供分析功能的 dbs 试试
SELECT t.* FROM
(SELECT t1.id, t1.home, t1.date, t1.player, t1.resource
, row_number() over (partition by t1.home order by t1.date desc) rw
FROM topten t1
INNER JOIN topten t2
ON t1.home = t2.home
WHERE t1.date > t2.date
) t
WHERE t.rw = 1
【讨论】:
这个答案正确吗?我尝试使用它,但它似乎不会为“家”选择日期最新的记录,而只会删除日期最旧的记录。这是一个示例:SQLfiddle @kidOfDeath - 用上下文和 Postgres 查询更新了我的回复 使用SQLite
,当匹配列(即“home”)上没有索引时,第一个比 La Voie 的版本慢得多。【参考方案8】:
SELECT tt.*
FROM TestTable tt
INNER JOIN
(
SELECT coord, MAX(datetime) AS MaxDateTime
FROM rapsa
GROUP BY
krd
) groupedtt
ON tt.coord = groupedtt.coord
AND tt.datetime = groupedtt.MaxDateTime
【讨论】:
【参考方案9】:这适用于 Oracle:
with table_max as(
select id
, home
, datetime
, player
, resource
, max(home) over (partition by home) maxhome
from table
)
select id
, home
, datetime
, player
, resource
from table_max
where home = maxhome
【讨论】:
如何选择最大日期时间?他要求按家分组,并选择最大日期时间。我不明白这是怎么做到的。【参考方案10】:在 SQL Server 上试试这个:
WITH cte AS (
SELECT home, MAX(year) AS year FROM Table1 GROUP BY home
)
SELECT * FROM Table1 a INNER JOIN cte ON a.home = cte.home AND a.year = cte.year
【讨论】:
【参考方案11】:SELECT c1, c2, c3, c4, c5 FROM table1 WHERE c3 = (select max(c3) from table)
SELECT * FROM table1 WHERE c3 = (select max(c3) from table1)
【讨论】:
【参考方案12】:这是 MySQL 版本,它只打印一个条目,其中一组中有重复的 MAX(datetime)。
你可以在这里测试http://www.sqlfiddle.com/#!2/0a4ae/1
样本数据
mysql> SELECT * from topten;
+------+------+---------------------+--------+----------+
| id | home | datetime | player | resource |
+------+------+---------------------+--------+----------+
| 1 | 10 | 2009-04-03 00:00:00 | john | 399 |
| 2 | 11 | 2009-04-03 00:00:00 | juliet | 244 |
| 3 | 10 | 2009-03-03 00:00:00 | john | 300 |
| 4 | 11 | 2009-03-03 00:00:00 | juliet | 200 |
| 5 | 12 | 2009-04-03 00:00:00 | borat | 555 |
| 6 | 12 | 2009-03-03 00:00:00 | borat | 500 |
| 7 | 13 | 2008-12-24 00:00:00 | borat | 600 |
| 8 | 13 | 2009-01-01 00:00:00 | borat | 700 |
| 9 | 10 | 2009-04-03 00:00:00 | borat | 700 |
| 10 | 11 | 2009-04-03 00:00:00 | borat | 700 |
| 12 | 12 | 2009-04-03 00:00:00 | borat | 700 |
+------+------+---------------------+--------+----------+
带有用户变量的 MySQL 版本
SELECT *
FROM (
SELECT ord.*,
IF (@prev_home = ord.home, 0, 1) AS is_first_appear,
@prev_home := ord.home
FROM (
SELECT t1.id, t1.home, t1.player, t1.resource
FROM topten t1
INNER JOIN (
SELECT home, MAX(datetime) AS mx_dt
FROM topten
GROUP BY home
) x ON t1.home = x.home AND t1.datetime = x.mx_dt
ORDER BY home
) ord, (SELECT @prev_home := 0, @seq := 0) init
) y
WHERE is_first_appear = 1;
+------+------+--------+----------+-----------------+------------------------+
| id | home | player | resource | is_first_appear | @prev_home := ord.home |
+------+------+--------+----------+-----------------+------------------------+
| 9 | 10 | borat | 700 | 1 | 10 |
| 10 | 11 | borat | 700 | 1 | 11 |
| 12 | 12 | borat | 700 | 1 | 12 |
| 8 | 13 | borat | 700 | 1 | 13 |
+------+------+--------+----------+-----------------+------------------------+
4 rows in set (0.00 sec)
接受的答案输出
SELECT tt.*
FROM topten tt
INNER JOIN
(
SELECT home, MAX(datetime) AS MaxDateTime
FROM topten
GROUP BY home
) groupedtt ON tt.home = groupedtt.home AND tt.datetime = groupedtt.MaxDateTime
+------+------+---------------------+--------+----------+
| id | home | datetime | player | resource |
+------+------+---------------------+--------+----------+
| 1 | 10 | 2009-04-03 00:00:00 | john | 399 |
| 2 | 11 | 2009-04-03 00:00:00 | juliet | 244 |
| 5 | 12 | 2009-04-03 00:00:00 | borat | 555 |
| 8 | 13 | 2009-01-01 00:00:00 | borat | 700 |
| 9 | 10 | 2009-04-03 00:00:00 | borat | 700 |
| 10 | 11 | 2009-04-03 00:00:00 | borat | 700 |
| 12 | 12 | 2009-04-03 00:00:00 | borat | 700 |
+------+------+---------------------+--------+----------+
7 rows in set (0.00 sec)
【讨论】:
尽管我喜欢这个答案,因为这对我帮助很大,我必须指出一个主要缺陷,它依赖于使用的 mysql 系统。基本上,这个解决方案依赖于子选择中的 ORDER BY 子句。这可能或可能不适用于各种 mysql 环境。我没有在纯 MySQL 上尝试过,但可以肯定的是,这在 MariaDB 10.1 上不能可靠地工作,正如这里***.com/questions/26372511/… 所解释的那样,但相同的代码在 Percona Server 上确实可以正常工作。准确地说,您可能会或可能不会得到相同的结果,具体取决于 t1 列的数量。 该语句的示例是,在 MariaDB 10.1 上,当我使用 t1 表中的 5 列时,它可以工作。一旦我添加了第六列,显然会弄乱原始表中的“自然”数据排序,它就停止了工作。原因是,子选择中的数据变得无序,因此我多次遇到“is_first_appear = 1”条件。相同的代码,相同的数据,在 Percona 上运行正常。【参考方案13】:另一种使用子查询对每组最近的行进行 gt 的方法,该子查询基本上计算每组每行的排名,然后过滤掉最近的行,如 rank = 1
select a.*
from topten a
where (
select count(*)
from topten b
where a.home = b.home
and a.`datetime` < b.`datetime`
) +1 = 1
DEMO
这里是visual demo 每行的排名号,以便更好地理解
通过读取一些 cmets 如果有两行具有相同的 'home' 和 'datetime' 字段值怎么办?
上述查询将失败,并会针对上述情况返回多于 1 行。为了掩盖这种情况,将需要另一个标准/参数/列来决定在上述情况下应该采用哪一行。通过查看示例数据集,我假设有一个主键列 id
应该设置为自动递增。所以我们可以通过CASE
这样的语句调整相同的查询来使用这个列来选择最近的行
select a.*
from topten a
where (
select count(*)
from topten b
where a.home = b.home
and case
when a.`datetime` = b.`datetime`
then a.id < b.id
else a.`datetime` < b.`datetime`
end
) + 1 = 1
DEMO
以上查询将在相同的datetime
值中选择具有最高id 的行
visual demo 表示每一行的排名号
【讨论】:
【参考方案14】:为什么不使用: SELECT home, MAX(datetime) AS MaxDateTime,player,resource FROM topten GROUP BY home 我错过了什么吗?
【讨论】:
这仅对 MySQL 有效,并且仅对 5.7 之前的版本(?)或 5.7 之后的版本有效,并且禁用了 ONLY_FULL_GROUP_BY,因为它正在选择尚未聚合/分组的列(播放器,资源),这意味着MySQL 将为这两个结果字段提供随机选择的值。玩家列不会有问题,因为它与 home 列相关,但资源列与 home 或 datetime 列不相关,您无法保证您会收到哪个资源值。 +1 用于解释,但 w.r.t 提出的问题,此查询不会返回 MySQL 5.6 版和before
中的 expected
输出,我非常怀疑它在 MySQL 5.7 版中的行为是否不同和after
。
@simpleuser, `这不会是播放器列的问题,因为它与主列相关` - 你能解释更多吗?
@IstiaqueAhmed 我再看一遍,那句话是不正确的。我原以为每个玩家总是有相同的房屋价值,但我现在发现他们没有,因此该列也会出现相同的随机选择问题【参考方案15】:
@Michae 接受的答案在大多数情况下都可以正常工作,但在以下情况下失败了。
如果有 2 行的 HomeID 和 Datetime 相同,则查询将返回两行,而不是根据需要返回不同的 HomeID,以便在查询中添加 Distinct,如下所示。
SELECT DISTINCT tt.home , tt.MaxDateTime
FROM topten tt
INNER JOIN
(SELECT home, MAX(datetime) AS MaxDateTime
FROM topten
GROUP BY home) groupedtt
ON tt.home = groupedtt.home
AND tt.datetime = groupedtt.MaxDateTime
【讨论】:
结果显示 - “#1054 - '字段列表'中的未知列'tt.MaxDateTime'” @IstiaqueAhmed 你有 MaxDatetime 归档,即任何类似的列名..? 不,OP中的表没有这样的列。 这个错误也说同样的请......你到底想做什么?您可以发送表结构和您的查询吗?【参考方案16】:在 MySQL 8.0 中,这可以通过使用带有公共表表达式的 row_number() 窗口函数来有效地实现。
(这里的 row_number() 基本上为每个玩家从资源的降序顺序为 1 开始为每一行生成唯一的序列。因此,对于每个序列号为 1 的玩家行将具有最高的资源值。现在我们需要做的就是正在为每个玩家选择序列号为 1 的行。可以通过围绕该查询编写外部查询来完成。但我们使用通用表表达式代替,因为它更具可读性。)
架构:
create TABLE TestTable(id INT, home INT, date DATETIME,
player VARCHAR(20), resource INT);
INSERT INTO TestTable
SELECT 1, 10, '2009-03-04', 'john', 399 UNION
SELECT 2, 11, '2009-03-04', 'juliet', 244 UNION
SELECT 5, 12, '2009-03-04', 'borat', 555 UNION
SELECT 3, 10, '2009-03-03', 'john', 300 UNION
SELECT 4, 11, '2009-03-03', 'juliet', 200 UNION
SELECT 6, 12, '2009-03-03', 'borat', 500 UNION
SELECT 7, 13, '2008-12-24', 'borat', 600 UNION
SELECT 8, 13, '2009-01-01', 'borat', 700
查询:
with cte as
(
select id, home, date , player, resource,
Row_Number()Over(Partition by home order by date desc) rownumber from TestTable
)
select id, home, date , player, resource from cte where rownumber=1
输出:
id | home | date | player | resource |
---|---|---|---|---|
1 | 10 | 2009-03-04 00:00:00 | john | 399 |
2 | 11 | 2009-03-04 00:00:00 | juliet | 244 |
5 | 12 | 2009-03-04 00:00:00 | borat | 555 |
8 | 13 | 2009-01-01 00:00:00 | borat | 700 |
db小提琴here
【讨论】:
【参考方案17】:试试这个
select * from mytable a join
(select home, max(datetime) datetime
from mytable
group by home) b
on a.home = b.home and a.datetime = b.datetime
问候 克
【讨论】:
测试它是否不同,如果两个相等的最大日期时间在同一个家中(与不同的玩家)max(datetime)
的别名是 datetime
。不会有问题吗?
最高的datetime
怎么选的?【参考方案18】:
这是您需要的查询:
SELECT b.id, a.home,b.[datetime],b.player,a.resource FROM
(SELECT home,MAX(resource) AS resource FROM tbl_1 GROUP BY home) AS a
LEFT JOIN
(SELECT id,home,[datetime],player,resource FROM tbl_1) AS b
ON a.resource = b.resource WHERE a.home =b.home;
【讨论】:
你能解释一下你的答案吗?【参考方案19】:希望下面的查询将给出所需的输出:
Select id, home,datetime,player,resource, row_number() over (Partition by home ORDER by datetime desc) as rownum from tablename where rownum=1
【讨论】:
【参考方案20】:(注意:Michael 的答案非常适合目标列 datetime
不能为每个不同的 home
包含重复值的情况。)
如果您的表home
xdatetime
有重复的行,并且您只需为每个不同的home
列选择一行,这是我的解决方案:
您的表格需要一个唯一的列(如id
)。如果没有,请创建一个视图并向其中添加一个随机列。
使用此查询为每个唯一的 home
值选择一行。在datetime
重复的情况下选择最低的id
。
SELECT tt.*
FROM topten tt
INNER JOIN
(
SELECT min(id) as min_id, home from topten tt2
INNER JOIN
(
SELECT home, MAX(datetime) AS MaxDateTime
FROM topten
GROUP BY home) groupedtt2
ON tt2.home = groupedtt2.home
) as groupedtt
ON tt.id = groupedtt.id
【讨论】:
【参考方案21】:如果有 2 条记录具有相同的日期和家庭,则接受的答案对我不起作用。加入后将返回 2 条记录。虽然我需要选择其中的任何一个(随机)。此查询用作连接子查询,因此无法仅限制 1。 这是我达到预期结果的方式。但是不知道性能。
select SUBSTRING_INDEX(GROUP_CONCAT(id order by datetime desc separator ','),',',1) as id, home, MAX(datetime) as 'datetime'
from topten
group by (home)
【讨论】:
以上是关于如何在 MYSQL 中选择具有 MAX(列值)的行,按另一列进行分区?的主要内容,如果未能解决你的问题,请参考以下文章