根据其他列的顺序从组中选择一个值

时间:2012-10-04 11:45:04

标签: mysql sql mariadb

问题

假设我有这张表tabfiddle可用)。

| g | a | b |     v |
---------------------
| 1 | 3 | 5 |   foo |
| 1 | 4 | 7 |   bar |
| 1 | 2 | 9 |   baz |
| 2 | 1 | 1 |   dog |
| 2 | 5 | 2 |   cat |
| 2 | 5 | 3 | horse |
| 2 | 3 | 8 |   pig |

我按g对行进行分组,对于每个组,我想要列v中的一个值。但是,我不希望任何值,但我希望来自行最大a的值,以及所有这些值中最大b的值。换句话说,我的结果应该是

| 1 |   bar |
| 2 | horse |

当前解决方案

我知道要实现此目的的查询:

SELECT grps.g,
(SELECT v FROM tab
 WHERE g = grps.g
 ORDER BY a DESC, b DESC
 LIMIT 1) AS r
FROM (SELECT DISTINCT g FROM tab) grps

问题

但我认为这个查询相当丑陋。主要是因为它使用依赖子查询,这感觉就像一个真正的性能杀手。所以我想知道是否有一个更容易解决这个问题。

预期答案

我期望这个问题的最可能的答案是MySQL(或MariaDB)的某种附加或补丁,它确实为此提供了一个功能。但我也欢迎其他有用的灵感。任何没有依赖子查询的东西都可以作为答案。

如果您的解决方案仅适用于单个排序列,即无法区分cathorse,请随意提出答案以及我希望它对大多数用例。例如,100*a+b可能是两列都对上述数据进行排序的可能方式,同时仍然只使用一个表达式。

我有一些非常讨厌的解决方案,可能会在一段时间后添加它们,但我会首先看一下,看看是否有一些不错的新解决方案首先注入。


基准测试结果

由于很难通过查看它们来比较各种答案,我已经对它们进行了一些基准测试。这是使用MySQL 5.1在我自己的桌面上运行的。这些数字不会与任何其他系统相比,只能相互比较。如果性能对您的应用程序至关重要,您可能应该使用现实数据进行自己的测试。当新答案出现时,我可能会将它们添加到我的脚本中,然后重新运行所有测试。

所以看起来到目前为止我自己的解决方案并不是那么糟糕,即使是依赖子查询。令人惊讶的是,acatt的解决方案也使用了一个依赖子查询,因此我考虑过这个解决方案,它的表现要差得多。可能是MySQL优化器无法应对的。 RichardTheKiwi提出的解决方案似乎也具有良好的整体表现。另外两种解决方案在很大程度上取决于数据的结构。由于许多小组小组,xdazz的方法优于其他所有小组,而Dems的解决方案对于少数大型小组表现最佳(尽管仍然不是特别好)。

4 个答案:

答案 0 :(得分:5)

这种方式不使用子查询。

SELECT t1.g, t1.v
FROM tab t1
LEFT JOIN tab t2 ON t1.g = t2.g AND (t1.a < t2.a OR (t1.a = t2.a AND t1.b < t2.b))
WHERE t2.g IS NULL

说明:

LEFT JOIN的工作原理是,当t1.a处于最大值时,没有s2.a具有更大的值,s2行值将为NULL。

答案 1 :(得分:5)

SELECT g, a, b, v
  FROM (
            SELECT *, 
                   @rn := IF(g = @g, @rn + 1, 1) rn, 
                   @g := g
              FROM (select @g := null, @rn := 0) x, 
                   tab
          ORDER BY g, a desc, b desc, v
       ) X
 WHERE rn = 1;

单通。所有其他解决方案对我来说都是O(n ^ 2)。

答案 2 :(得分:1)

许多RDBMS都有特别适合这个问题的结构。 MySQL 不是 其中之一。

这将引导您采用三种基本方法。

  • 使用EXISTS和EXISTS子句中的相关子查询,检查每条记录以查看它是否是您想要的记录。 (@ acatt的答案,但我知道MySQL并不总能很好地优化这一点。确保在(g,a,b)上有一个复合索引,然后再假设MySQL不能很好地做到这一点。)

  • 做半笛卡尔产品以完全填写相同的支票。任何未加入的记录都是目标记录。如果每个组('g')很大,这会很快降低性能(如果g的每个唯一值有10条记录,这将产生约50条记录并丢弃49.对于组大小100,它产生约5000条记录并丢弃4999),但 非常适合小组规模。 (@ xdazz的回答。)

  • 或使用多个子查询来确定MAX(a)然后再确定MAX(b)......

多个连续子查询...

SELECT
  yourTable.*
FROM
  (SELECT g,    MAX(a) AS a FROM yourTable GROUP BY g   ) AS searchA
INNER JOIN
  (SELECT g, a, MAX(b) AS b FROM yourTable GROUP BY g, a) AS searchB
    ON  searchA.g = searchB.g
    AND searchA.a = searchB.a
INNER JOIN
  yourTable
    ON  yourTable.g = searchB.g
    AND yourTable.a = searchB.a
    AND yourTable.b = searchB.b

根据MySQL如何优化第二个子查询,这可能会或可能不会比其他选项更高效。但是,它是给定任务的最长的(可能是最不可维护的)代码。

假设所有三个搜索字段(g, a, b)都有一个综合索引,我认为它最适合g的大型组大小。但那应该进行测试。

对于g的小组,我会选择@ xdazz的答案。

修改

还有一种蛮力方法。

  • 创建一个相同的表,但使用AUTO_INCREMENT列作为id。
  • 将表格插入此克隆中,按g,a,b排序。
  • 然后可以使用SELECT g, MAX(id)找到ID。
  • 然后,可以使用此结果查找所需的v值。

这不太可能是最好的方法。如果是这样,它实际上是MySQL优化者处理这类问题的能力的基础。

也就是说,每个发动机都有它的弱点。所以,就个人而言,我会尝试一切,直到我认为我理解RDBMS的行为方式并且可以做出我的选择:)

修改

使用ROW_NUMBER()的示例。 (Oracle,SQL Server,PostGreSQL等)

SELECT
  *
FROM
(
  SELECT
    ROW_NUMBER() OVER (PARTITION BY g ORDER BY a DESC, b DESC) AS sequence_id,
    *
  FROM
    yourTable
)
  AS data
WHERE
  sequence_id = 1

答案 3 :(得分:0)

这可以使用相关查询来解决:

SELECT g, v
FROM tab t
WHERE NOT EXISTS (
    SELECT 1
    FROM tab
    WHERE g = t.g
        AND a > t.a
        OR (a = t.a AND b > t.b)
    )