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

Posted

技术标签:

【中文标题】按值(不是列)分组后从组中选择一个随机条目?【英文标题】:Select a random entry from a group after grouping by a value (not column)? 【发布时间】:2013-02-26 14:17:50 【问题描述】:

我想使用 Postgres 和 PostGIS 编写查询。我也在使用带有 rgeorgeo-activerecordactiverecord-postgis-adapter 的 Rails,但 Rails 的东西并不重要。

表结构:

measurement
 - int id
 - int anchor_id
 - Point groundtruth
 - data (not important for the query)

示例数据:

id | anchor_id | groundtruth | data
-----------------------------------
1  | 1         | POINT(1 4)  | ...
2  | 3         | POINT(1 4)  | ...
3  | 2         | POINT(1 4)  | ...
4  | 3         | POINT(1 4)  | ...
-----------------------------------
5  | 2         | POINT(3 2)  | ...
6  | 4         | POINT(3 2)  | ...
-----------------------------------
7  | 1         | POINT(4 3)  | ...
8  | 1         | POINT(4 3)  | ...
9  | 1         | POINT(4 3)  | ...
10 | 5         | POINT(4 3)  | ...
11 | 3         | POINT(4 3)  | ...

此表是某种手动创建的view,用于更快地查找(具有数百万行)。否则我们必须加入 8 个表,它会变得更慢。但这不是问题的一部分。


简单版:

参数:

p int d

查询应该做什么:

1. 查询从点p 中查找具有distance < d 的所有groundtruth

SQL 非常简单:WHERE st_distance(groundtruth, p) < d

2. 现在我们有一个groundtruth 点列表及其anchor_ids。如上表所示,可能有多个相同的 groundtruth-anchor_id 元组。例如:anchor_id=3groundtruth=POINT(1 4)

3. 接下来我想通过随机选择其中一个来消除相同的元组(!)。为什么不直接拿第一呢?因为data 列不同。

在 SQL 中选择随机行:SELECT ... ORDER BY RANDOM() LIMIT 1

我的问题是:我可以想象一个使用 SQL LOOPs 和大量子查询的解决方案,但是肯定有一个使用 GROUP BY 或其他一些方法的解决方案让它更快。

完整版:

基本同上,不同之处:输入参数变化:

很多点p1 ... p312456345 还有一个d

如果简单查询有效,可以在 SQL 中使用 LOOP 来完成。但也许有更好(更快)的解决方案,因为数据库真的很大!


解决方案

WITH ps AS (SELECT unnest(p_array) AS p)
SELECT DISTINCT ON (anchor_id, groundtruth)
    *
FROM measurement m, ps
WHERE EXISTS (
    SELECT 1
    FROM ps
    WHERE st_distance(m.groundtruth, ps.p) < d
)
ORDER BY anchor_id, groundtruth, random();

感谢 Erwin Brandstetter!

【问题讨论】:

您需要真正的随机性,还是任意选择就足够了?您是否希望结果以某种方式排序(例如“最近的优先”)? 任意就够了,我想。它对查询有影响吗? 差别很大。随心所欲的便宜很多。 是的,确实如此。但如果它只是使用ARBITRATY_RANDOM() 而不是RANDOM(),我可以尝试一下,看看它是否会产生预期的那么大的影响。 关键是,像我在答案中提供的查询会自动进行任意选择,我不需要使用显式函数,这要便宜得多。但它并不是真正随机的。 【参考方案1】:

为了消除重复,这可能是 PostgreSQL 中最有效的查询:

SELECT DISTINCT ON (anchor_id, groundtruth) *
FROM   measurement
WHERE  st_distance(p, groundtruth) < d

有关此查询样式的更多信息:

Select first row in each GROUP BY group?

正如 cmets 中提到的,这为您提供了一个任意选择。如果你需要随机的,稍微贵一点:

SELECT DISTINCT ON (anchor_id, groundtruth) *
FROM   measurement
WHERE  st_distance(p, groundtruth) < d
ORDER  BY anchor_id, groundtruth, random()

第二部分更难优化。 EXISTS semi-join 可能是最快的选择。对于给定的表ps (p point)

SELECT DISTINCT ON (anchor_id, groundtruth) *
FROM   measurement m
WHERE  EXISTS (
   SELECT 1
   FROM   ps
   WHERE  st_distance(ps.p, m.groundtruth) < d
   )
ORDER  BY anchor_id, groundtruth, random();

只要有一个 p 足够接近,它就会停止评估,并保持查询的其余部分简单。

请务必使用 a matching GiST index 备份。

如果您有一个数组作为输入,请即时创建一个带有unnest() 的CTE:

WITH ps AS (SELECT unnest(p_array) AS p)
SELECT ...

根据评论更新

如果您只需要单行作为答案,您可以简化:

WITH ps AS (SELECT unnest(p_array) AS p)
SELECT *
FROM   measurement m
WHERE  EXISTS (
   SELECT 1
   FROM   ps
   WHERE  st_distance(ps.p, m.groundtruth) < d
   )
LIMIT  1;

使用ST_DWithin() 更快

使用 ST_DWithin() 函数(和匹配的 GiST 索引!)可能更有效。 要获得 一个 行(在此处使用子选择而不是 CTE):

SELECT *
FROM   measurement m
JOIN  (SELECT unnest(p_array) AS p) ps ON ST_DWithin(ps.p, m.groundtruth, d)
LIMIT  1;

在距离d 内的每个点p 获得一行:

SELECT DISTINCT ON (ps.p) *
FROM   measurement m
JOIN  (SELECT unnest(p_array) AS p) ps ON ST_DWithin(ps.p, m.groundtruth, d)

添加ORDER BY random() 将使此查询成本更高。如果没有random(),Postgres 只能从 GiST 索引中选择 第一个 匹配行。否则所有可能的匹配必须随机检索和排序。


顺便说一句,LIMIT 1 里面的EXISTS 毫无意义。阅读the manual at the link I provided 或this related question。

【讨论】:

但这并没有给我一个随机的anchor-groundtruth-tuple 是的,抱歉,我忽略了* :D ... 现在Full version 呢?也许我可以用IN (p_array) 做点什么? 对于完整版来说很重要的一点:SELECT 也应该返回输入值。 And(!) 对于两个输入值 p1p2,可以返回相同的随机选择的锚定-groundtruth-tuple。 对不起。我不明白FROM ps 部分。这些点没有表格。它们是用户输入:-/ 或者您的查询是否创建了某种虚拟表? 抱歉再次打扰您。 我误解了自己的问题 ... 我真正需要的: 输入:p_array 和 d。然后:选择一个与st_distance(m.groundtruth, ps.p) &lt; d 匹配的随机groundtruth。接下来随机选择不同的anchor_id。我会试着自己弄清楚。真的很抱歉……【参考方案2】:

我现在破解了,但是查询很慢...

WITH
  ps AS (
    SELECT unnest(p_array)
    ) AS p
  ),

  gtps AS (
    SELECT DISTINCT ON(ps.p)
      ps.p, m.groundtruth
    FROM measurement m, ps
    WHERE st_distance(m.groundtruth, ps.p) < d
    ORDER BY ps.p, RANDOM()
  )

SELECT DISTINCT ON(gtps.p, gtps.groundtruth, m.anchor_id)
  m.id, m.anchor_id, gtps.groundtruth, gtps.p
FROM measurement m, gtps
ORDER BY gtps.p, gtps.groundtruth, m.anchor_id, RANDOM()

我的测试数据库包含 22000 行,我给了它两个输入值,大约需要 700 毫秒。最后可能有数百个输入值:-/


结果现在看起来像这样:

id  | anchor_id | groundtruth | p
-----------------------------------------
20  | 1         | POINT(0 2)  | POINT(1 0)
14  | 3         | POINT(0 2)  | POINT(1 0)
5   | 8         | POINT(0 2)  | POINT(1 0)
42  | 2         | POINT(4 1)  | POINT(2 2)
11  | 3         | POINT(4 8)  | POINT(4 8)
4   | 6         | POINT(4 8)  | POINT(4 8)
1   | 1         | POINT(6 2)  | POINT(7 3)
9   | 5         | POINT(6 2)  | POINT(7 3)
25  | 3         | POINT(6 2)  | POINT(9 1)
13  | 6         | POINT(6 2)  | POINT(9 1)
18  | 7         | POINT(6 2)  | POINT(9 1)

新:

SELECT
  m.groundtruth, ps.p, ARRAY_AGG(m.anchor_id), ARRAY_AGG(m.id)
FROM
  measurement m
JOIN
  (SELECT unnest(point_array) AS p) AS ps
  ON ST_DWithin(ps.p, m.groundtruth, 0.5)
GROUP BY groundtruth, ps.p

实际结果:

p           | groundtruth | anchor_arr | id_arr
--------------------------------------------------
P1          | G1          | 1,3,2,.. | 9,8,11,..
P1          | G2          | 4,3,5,.. | 1,8,23,..
P1          | G3          | 6,8,9,.. | 12,7,6,..
P2          | G1          | 6,6,2,.. | 15,2,10,..
P2          | G4          | 7,9,1,.. | 5,4,3,..
...         | ...         | ...        | ...

所以现在我得到:

每个不同的 inputValue-groundtruth-tuple 对于每个元组,我都会得到一个数组,其中所有 anchor_id 对应于元组的 groundtruth 部分 和所有ids 对应的groundtruth-anchor_id 关系的数组

记住:

两个输入值可以“选择”相同的groundtruth 一个groundtruth值可以有多个相同的anchor_ids 每个groundtruth-anchor_id-元组都有一个不同的id

那么完成时缺少什么?:

我只需要为每个ps.p 设置一个随机行 这两个数组属于彼此。意思是:里面物品的顺序很重要! 这两个数组需要过滤(随机): 对于数组中出现多次的每个anchor_id:保留一个随机的并删除所有其他的。这也意味着从id-array 中为每个删除的anchor_id 删除相应的id

【讨论】:

我现在已经删除了两个RANDOM() 语句,结果并没有变得更好:530ms 对比。 670 毫秒。所以我不认为这是我最大的问题 ;) 但是如果我将输入数组从 2 减少到 1 值,它会以两倍的速度结束(!):270ms Vs。 670 毫秒。 如果你想要性能,你需要一个 GiST 索引。如果没有索引,Postgres 必须为表中的每一行计算到每个 p 的距离。我在这里说的是数量级。 我现在在groudtruth 上设置了一个GIST 索引,在anchor_id 上设置了一个btree 索引。但它并没有变得更快。我必须以某种方式使用另一个查询... 我现在正在寻找另一种检索数据的方法。实际上它非常快,但没有进行随机化,也没有在anchor_id 中进行区分。我会把它贴在我的答案底部。 如果没有使用 GiST 索引,很可能是你没有做对。使用EXPLAIN ANALYZE 进行测试并阅读ST_DWithin() 和Gist indexes 上的PostGis 手册。明确一点:不鼓励在给出答案后改变问题的性质。在这种情况下,您应该提出一个新问题。祝你好运。

以上是关于按值(不是列)分组后从组中选择一个随机条目?的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

SQL Server:根据多个条件从组中选择特定行

.NET 正则表达式分组:从组中排除字符串

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