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

Posted

技术标签:

【中文标题】根据其他列的顺序从组中选择一个值【英文标题】:Select one value from a group based on order from other columns 【发布时间】:2012-10-04 11:45:03 【问题描述】:

问题

假设我有这张桌子tab(fiddle 可用)。

| 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。这些数字不会与任何其他系统进行比较,只能相互比较。如果性能对您的应用程序至关重要,您可能应该使用您的真实数据进行自己的测试。当有新答案出现时,我可能会将它们添加到我的脚本中,然后重新运行所有测试。

100,000 个项目,1,000 个组可供选择,InnoDb:
    MvG (from question) 为 0.166 秒 RichardTheKiwi 为 0.520 秒 xdazz 为 2.199 秒 Dems 19.24 秒(顺序子查询) acatt 为 48.72 秒
100,000 个项目,50,000 个组可供选择,InnoDb:
    xdazz 为 0.356 秒 RichardTheKiwi 为 0.640 秒 MvG (from question) 为 0.764 秒 acatt 为 51.50 秒 对于Dems(顺序子查询)太长
100,000 个项目,100 个组可供选择,InnoDb:
    MvG (from question) 为 0.163 秒 RichardTheKiwi 为 0.523 秒 Dems(顺序子查询)为 2.072 秒 xdazz 为 17.78 秒 acatt 为 49.85 秒

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

【问题讨论】:

您对表格应用了哪些索引?另外,请注意,RichardTheKiwi 的方法似乎相当稳定。我还估计它是线性的,因为你缩放项目的总数。 @Dems,自动递增 id 作为查询中不涉及的主键,(g,a,b) 作为复合唯一键。由于排序需要 O(n log n) 并且我怀疑 MySQL 是否足够聪明以优化对选择的排序,我估计他的解决方案比线性略慢,但差异似乎很小。 Script available at dpaste 一会儿。 cc @Dems // 基准测试的第一条规则是说明误差范围。我的解决方案应该对所有 3 个产生几乎相同的结果,并且范围大约是正确的。令人惊讶的是,第二个会爆炸这么多。我现在不能大惊小怪,但是看到针对所有查询的 EXPLAIN 会很有趣。您的查询将是 (n log n) 并且严重依赖于使用 DISTINCT 领先的组密度。 【参考方案1】:
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)。

【讨论】:

这在性能方面看起来相当不错,因为您采用的想法类似于 Dems 在其他 RDBMS 中写的关于 ROW_NUMBER 的想法。我担心的是,根据MySQL docs,“涉及用户变量的表达式的求值顺序未定义 [...]”(另请阅读周围段落)。正如我所看到的,不能保证行将按照ORDER BY 指定的顺序编号。有吗? 发布 another question 询问此类查询的保证。 我已经接受了这个答案。一方面,它在我的各种基准测试中表现出良好的行为。另一方面,这是我在问这个问题之前不会使用的代码,但我现在会考虑使用,所以我从中学到了很多东西。尽管我一直对保证感到疑惑。【参考方案2】:

这种方式不使用子查询。

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。

【讨论】:

Incorrect result:您甚至没有选择v 值。你的查询基本上只做SELECT g, MAX(a) FROM tab GROUP BY g @MvG 我的错字,将t1.a 更改为t1.v 会得到正确的结果。 仍然不会打破猫和马之间的关系,但这可以解决。随意将fixed version 包含在您的答案中。 @MvG 是的,你的固定版本是对的,我没有阅读你所有的问题。 @RichardTheKiwi:根据我在问题中编辑的基准,对于小型组,此查询优于您的查询。所以渐近复杂度在这里并不是全部,它在很大程度上取决于实际数据。【参考方案3】:

许多 RDBMS 具有特别适合此问题的构造。 MySQL 不是其中之一。

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

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

做半个笛卡尔积来填满同一张支票。任何不加入的记录都是目标记录。如果每个组 ('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

【讨论】:

这并不是我最初的问题的一部分,但我欢迎一些其他 RDBMS 提供的构造示例。可能是括号中的一些名称,并在可用的地方提供链接。 @MvG - ROW_NUMBER() 已添加答案。这是一些 RDBMS 已经实现的分析函数或窗口函数。它是不断发展的 SQL 标准的一部分。该标准定义了语言结构,而不是如何实现它们 - 这意味着不同的 RDBMS 具有该标准的不同子集;)【参考方案4】:

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

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)
    )

【讨论】:

根据execution plan这个NOT EXISTS的东西仍然是一个DEPENDENT SUBQUERY:它会被重复执行,对表中的每一行执行一次。 @MvG 我的印象是存在与依赖子查询相关的性能问题,但这在某个时候得到了解决。抱歉,如果不是这种情况。此外,xdazz 的解决方案似乎是这里最好的。 @acatt - 我建议可能有一个临界点。半笛卡尔积版本可能比这个答案更糟糕。随着小组规模的扩大,这可能会变得相对更有效,并且可能会在某个时候变得更好。 我也有同样的印象,这就是我想避开它们的原因。如果问题得到解决,我想知道我的问题中的版本是否也受益于该优化。 @Dems,感谢您指出这一点。不过,这会让接受单个的答案变得更加困难。 @MvG :接受对您信息最丰富的答案,或包含您实际使用的方法的答案。并赞成所有其他人。 (然后在我的家庭住址 BEEEEEEEEEEEEEEEP 寄一张 10 英镑的支票给我)

以上是关于根据其他列的顺序从组中选择一个值的主要内容,如果未能解决你的问题,请参考以下文章

Oracle SQL:从组中选择最大值和最小值

PySpark:从组中的值创建一个向量[重复]

按值(不是列)分组后从组中选择一个随机条目?

仅从组中选择第一行的 SQL 模式

XSLT / Muenchian 分组:如何从组中选择具有某些子元素的元素?

如果选择字段,则从组中获取所有选定的选项