根据其他列的顺序从组中选择一个值
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)的某种附加组件或补丁,它确实为此提供了一个功能。但我也欢迎其他有用的灵感。任何没有依赖子查询的工作都可以作为答案。
如果您的解决方案仅适用于单个排序列,即无法区分 cat
和 horse
,请随意提出该答案,因为我希望它对大多数用例仍然有用.例如,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 秒
-
xdazz 为 0.356 秒
RichardTheKiwi 为 0.640 秒
MvG (from question) 为 0.764 秒
acatt 为 51.50 秒
对于Dems(顺序子查询)太长
-
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 英镑的支票给我)以上是关于根据其他列的顺序从组中选择一个值的主要内容,如果未能解决你的问题,请参考以下文章