如何根据多个排序列选择每组的第一行?

Posted

技术标签:

【中文标题】如何根据多个排序列选择每组的第一行?【英文标题】:How to SELECT the top row per group based on multiple ordering columns? 【发布时间】:2016-12-16 22:07:57 【问题描述】:

我的查询如下所示:

SELECT time_start, some_count
    FROM foo
    WHERE user_id = 1
    AND DATE(time_start) = '2016-07-27'
    ORDER BY some_count DESC, time_start DESC LIMIT 1;

这样做是返回一行,其中 some_count 是 user_id = 1 的最高计数。它还为我提供了该some_count 的最新时间戳,因为some_count 对于多个time_start 值可能相同,而我想要最新的一个。

现在我要做的是运行一个查询,该查询将为在特定日期至少发生一次的每个user_id 计算出这一点,在本例中为2016-07-27。最终它可能需要一个 GROUP BY,因为我正在寻找每个 user_id 的组最大值

编写这种性质的查询的最佳方式是什么?

【问题讨论】:

SELECT DISTINCT(user_id), ... 将在没有 GROUP BY 的情况下为每个用户获取一个条目。您想要哪一列的 MAX() 值? 我想要some_count 的 MAX() 值,但我还需要知道 MAX() time_stop 与特定 some_count 匹配的位置,因为@987654332 可能有多行@ 与 user_idtime_stop 相同 什么是主键? @PaulSpiegel id 这是一个自动增量列。 能否请您显示初始表格和最终所需表格,以便我们更好地帮助您 【参考方案1】:

你可以使用NOT EXISTS()

SELECT * FROM foo t
WHERE (DATE(time_start) = '2016-07-27'
   OR DATE(time_stop) = '2016-07-27') 
  AND NOT EXISTS(SELECT 1 FROM foo s
                 WHERE t.user_id = s.user_id
                 AND (s.some_count > t.some_count
                  OR (s.some_count = t.some_count
                      AND s.time_stop > t.time_stop)))

NOT EXISTS() 将仅选择具有更大计数的另一条记录或具有相同计数但不存在较新的time_stop 的另一条记录。

【讨论】:

【参考方案2】:

您可以将原始查询用作 WHERE 子句中的相关子查询。

SELECT user_id, time_stop, some_count
FROM foo f
WHERE f.id = (
    SELECT f1.id
    FROM foo f1
    WHERE f1.user_id = f.user_id -- correlate
    AND DATE(f1.time_start) = '2016-07-27'
    ORDER BY f1.some_count DESC, f1.time_stop DESC LIMIT 1
)

mysql 应该能够为每个不同的user_id 缓存子查询的结果。

另一种方法是使用嵌套的 GROUP BY 查询:

select f.user_id, f.some_count, max(f.time_stop) as time_stop
from (
    select f.user_id, max(f.some_count) as some_count
    from foo f
    where date(f.time_start) = '2016-07-27'
    group by f.user_id
) sub
join foo f using(user_id, some_count)
where date(f.time_start) = '2016-07-27'
group by f.user_id, f.some_count

【讨论】:

【参考方案3】:
SELECT user_id,
       some_count,
       max(time_start) AS time_start
FROM
  (SELECT a.*
   FROM foo AS a
   INNER JOIN
     (SELECT user_id,
             max(some_count) AS some_count
      FROM foo
      WHERE DATE(time_start) = '2016-07-27'
      GROUP BY user_id) AS b ON a.user_id = b.user_id
   AND a.some_count = b.some_count) AS c
GROUP BY user_id,
         some_count;

从内到外解释:最内层表 (b) 将为您提供每个用户的最大 some_count。这还不够,因为您想要两列的最大值 - 所以我将它与完整表 (a) 连接以获取具有这些最大值 (c) 的记录,并从中获取最大 time_start每个 user/some_count 组合。

【讨论】:

我不得不编辑我的 OP。我需要DESC time_start。您的查询现在运行的方式,我得到匹配 time_start 的行不匹配:WHERE DATE(time_start) = '2016-07-27' @randombits - 我编辑了我的查询,更改是使用time_start 而不是time_stop。不确定我是否遵循您在评论中的意思,您将从今天开始收到条目,但每天最多。你是什​​么意思匹配time_start【参考方案4】:

我正在分享我的两种方法。

方法 #1(可扩展):

使用MySQL user_defined variables

SELECT
    t.user_id,
    t.time_start,
    t.time_stop,
    t.some_count
FROM 
(
    SELECT
        user_id,
        time_start,
        time_stop,
        some_count,
        IF(@sameUser = user_id, @rn := @rn + 1,
             IF(@sameUser := user_id, @rn := 1, @rn := 1)
        ) AS row_number

    FROM    foo
    CROSS JOIN (
        SELECT
            @sameUser := - 1,
            @rn := 1
    ) var
    WHERE   DATE(time_start) = '2016-07-27'
    ORDER BY    user_id,    some_count DESC,    time_stop DESC
) AS t
WHERE t.row_number <= 1
ORDER BY t.user_id;

可扩展,因为如果您想为每个用户提供最新的 n 行,那么您只需要更改此行:

... WHERE t.row_number &lt;= n...

如果查询提供预期结果,我可以稍后添加说明


方法 #2:(不可扩展)

使用INNER JOIN and GROUP BY

SELECT 
 F.user_id,
 F.some_count,
 F.time_start,
 MAX(F.time_stop) AS max_time_stop
FROM foo F
INNER JOIN 
(
    SELECT 
        user_id,
        MAX(some_count) AS max_some_count
    FROM foo
    WHERE DATE(time_start) = '2016-07-27'
    GROUP BY user_id
) AS t
ON F.user_id = t.user_id AND F.some_count = t.max_some_count
WHERE DATE(time_start) = '2016-07-27'
GROUP BY F.user_id

【讨论】:

【参考方案5】:

我相信,您不需要为查询做任何花哨的事情。 只需按 user_id 升序和 some_counttime_start 降序对表格进行排序,然后从有序表格 GROUP BY 中选择预期字段>user_id。这很简单。尝试让我知道是否有效。

SELECT user_id, some_count, time_start
FROM (SELECT * FROM foo ORDER BY user_id ASC, some_count DESC, time_start DESC)sorted_foo
WHERE DATE( time_start ) = '2016-07-27'
GROUP BY user_id

【讨论】:

上一个答案有错误。对不起,不必要的错误。我解决了这些问题并进行了检查。现在看来它工作正常:)【参考方案6】:

策略

一般来说,找到最大值比对记录组进行排序更有效。在这种情况下,排序是一个整数 (some_count),后跟一个日期/时间 (time_start) - 所以要找到一个最大的行,我们需要以某种方式组合它们。

执行此操作的一种简单方法是将两者组合成一个字符串,但字符串比较通常会遇到问题,例如,"4" 的值高于"12"。这很容易通过使用LPAD 添加前导零来克服,因此4 变为"0000000004",在字符串比较中低于"0000000012"。假设 time_start 是一个 DATETIME 字段,它可以简单地附加到此字段以进行二级排序,因为它的字符串转换会产生可排序的格式 (yyyy-mm-dd hh:MM:ss)。

SQL

使用这个策略,我们可以通过一个简单的子选择来限制:

SELECT time_start, some_count
FROM foo f1
WHERE DATE(time_start) = '2016-07-27'
  AND CONCAT(LPAD(some_count, 10, '0'), time_start) = 
      (SELECT MAX(CONCAT(LPAD(some_count, 10, '0'), time_start))
       FROM foo f2
       WHERE DATE(f2.time_start) = '2016-07-27'
         AND f2.user_id = f1.user_id);

演示

Rextester 演示在这里:http://rextester.com/HCGY1362

【讨论】:

【参考方案7】:

你的问题可以用一个叫做窗口函数的东西来解决,但是MySQL不支持这种特性。

我有两个解决方案给你。一种是模拟窗口函数,另一种是在 MySQL 中编写一些查询来解决这些情况的常用方法。

这是第一个,我回复this question:

-- simulates the window function
-- first_value(<col>) over(partition by user_id order by some_count DESC, time_start DESC)
SELECT
  user_id,
  substring_index(group_concat(time_start ORDER BY some_count DESC, time_start DESC), ',', 1) time_start,
  substring_index(group_concat(some_count ORDER BY some_count DESC, time_start DESC), ',', 1) some_count
FROM foo
WHERE DATE(time_start) = '2016-07-27'
GROUP BY user_id
;

基本上,您按user_id 对数据进行分组,并使用, 分隔符连接指定列中的所有值,按您想要的列对每个组进行排序,然后仅对第一个排序值进行子字符串化。这不是最佳方法...

这是第二个,我回答了this question:

SELECT 
  user_id,
  some_count,
  MAX(time_start) time_start
FROM foo outq
WHERE 1=1
  AND DATE(time_start) = '2016-07-27'
  AND NOT EXISTS
  (
    SELECT 1
    FROM foo 
    WHERE 1=1
      AND user_id    = outq.user_id
      AND some_count > outq.some_count
      AND DATE(time_start) = DATE(outq.time_start)
  )
GROUP BY
  user_id,
  some_count
;

基本上,子查询会检查每个user_id,如果有任何some_count 高于在该日期检查的当前@,因为主查询期望它是NOT EXISTS。您将在某个日期留下所有最高的some_count user_id,但是对于来自用户的相同最高值,该日期可能存在多个不同的time_start。现在事情很简单。您可以安全地GROUP BY 用户和计数,因为它们已经是您想要的数据,并从组中获得最大的time_start

这种子查询是MySQL中解决此类问题的常用方法。我建议您尝试两种解决方案,但选择第二种解决方案并记住子查询 sintax 以解决任何未来的问题。

此外,在 MySQL 中,隐式 ORDER BY &lt;columns&gt; 应用于所有具有 GROUP BY &lt;columns&gt; 的查询。如果您不关心结果顺序,可以通过声明 ORDER BY NULL 来节省一些处理,这将禁用查询中的隐式排序功能。

【讨论】:

【参考方案8】:
SELECT  c1.user_id, c1.some_count, MAX(c1.time_start) AS time_start
    FROM  foo AS c1
    JOIN
      ( SELECT  user_id, MAX(some_count) AS some_count
            FROM  foo
            WHERE time_start >= '2016-07-27'
              AND time_start  < '2016-07-27' + INTERVAL 1 DAY
            GROUP BY  user_id
      ) AS c2 USING (user_id, some_count)
    GROUP BY c1.user_id, c1.some_count

并且,添加这些以获得更好的性能:

INDEX(user_id, some_count, time_start)
INDEX(time_start)

time_start 范围的测试已更改,以便可以使用第二个索引。

这大致来自groupwise max 上的博客。

【讨论】:

以上是关于如何根据多个排序列选择每组的第一行?的主要内容,如果未能解决你的问题,请参考以下文章

如何选择每组的第一行?

如何获得每组的第一行?

MySQL按顺序查找每组最近/最大的记录

如何将熊猫数据框值除以每组的第一行?

DolphinDB:如何获取每个滑动组的最大值的第一行?

从每组的第一行和最后一行获取值