按值(不是列)分组后从组中选择一个随机条目?
Posted
技术标签:
【中文标题】按值(不是列)分组后从组中选择一个随机条目?【英文标题】:Select a random entry from a group after grouping by a value (not column)? 【发布时间】:2013-02-26 14:17:50 【问题描述】:我想使用 Postgres 和 PostGIS 编写查询。我也在使用带有 rgeo
、rgeo-activerecord
和 activerecord-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_id
s。如上表所示,可能有多个相同的 groundtruth-anchor_id 元组。例如:anchor_id=3
和 groundtruth=POINT(1 4)
。
3. 接下来我想通过随机选择其中一个来消除相同的元组(!)。为什么不直接拿第一呢?因为data
列不同。
在 SQL 中选择随机行:SELECT ... ORDER BY RANDOM() LIMIT 1
我的问题是:我可以想象一个使用 SQL LOOP
s 和大量子查询的解决方案,但是肯定有一个使用 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(!) 对于两个输入值 p1
和 p2
,可以返回相同的随机选择的锚定-groundtruth-tuple。
对不起。我不明白FROM ps
部分。这些点没有表格。它们是用户输入:-/ 或者您的查询是否创建了某种虚拟表?
抱歉再次打扰您。 我误解了自己的问题 ... 我真正需要的: 输入:p_array 和 d。然后:选择一个与st_distance(m.groundtruth, ps.p) < 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
部分
和所有id
s 对应的groundtruth
-anchor_id
关系的数组
记住:
两个输入值可以“选择”相同的groundtruth
一个groundtruth
值可以有多个相同的anchor_id
s
每个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 手册。明确一点:不鼓励在给出答案后改变问题的性质。在这种情况下,您应该提出一个新问题。祝你好运。以上是关于按值(不是列)分组后从组中选择一个随机条目?的主要内容,如果未能解决你的问题,请参考以下文章