如何在 PostgreSQL 中获得随机笛卡尔积?
Posted
技术标签:
【中文标题】如何在 PostgreSQL 中获得随机笛卡尔积?【英文标题】:How can I get a random cartesian product in PostgreSQL? 【发布时间】:2012-05-23 03:02:31 【问题描述】:我有两张桌子,custassets
和 tags
。为了生成一些测试数据,我想做一个 INSERT INTO
一个多对多表和一个 SELECT
从每个表中获取随机行(以便一个表中的随机主键与随机主键配对从第二个)。令我惊讶的是,这并不像我最初想象的那么容易,所以我坚持要自学。
这是我的第一次尝试。我选择 10 custassets
和 3 tags
,但在每种情况下都相同。我可以修复第一个表,但我想随机分配分配的标签。
SELECT
custassets_rand.id custassets_id,
tags_rand.id tags_rand_id
FROM
(
SELECT id FROM custassets WHERE defunct = false ORDER BY RANDOM() LIMIT 10
) AS custassets_rand
,
(
SELECT id FROM tags WHERE defunct = false ORDER BY RANDOM() LIMIT 3
) AS tags_rand
这会产生:
custassets_id | tags_rand_id
---------------+--------------
9849 | 3322
9849 | 4871 this pattern of tag PKs is repeated
9849 | 5188
12145 | 3322
12145 | 4871
12145 | 5188
17837 | 3322
17837 | 4871
17837 | 5188
....
然后我尝试了以下方法:在SELECT
列列表中执行第二个RANDOM()
调用。然而这个更糟糕,因为它选择了一个标签 PK 并坚持下去。
SELECT
custassets_rand.id custassets_id,
(SELECT id FROM tags WHERE defunct = false ORDER BY RANDOM() LIMIT 1) tags_rand_id
FROM
(
SELECT id FROM custassets WHERE defunct = false ORDER BY RANDOM() LIMIT 30
) AS custassets_rand
结果:
custassets_id | tags_rand_id
---------------+--------------
16694 | 1537
14204 | 1537
23823 | 1537
34799 | 1537
36388 | 1537
....
这在脚本语言中很容易,而且我确信使用存储过程或临时表可以很容易地完成。但是我可以只使用INSERT INTO SELECT
吗?
我确实想过使用随机函数来选择整数主键,但不幸的是,两个表的主键在增量序列中都有间隙(因此可能会在每个表中选择一个空行)。不然就好了!
【问题讨论】:
感谢所有评论的人 - 如果由我决定,我会奖励几个滴答声!:-)
【参考方案1】:
已更新以将 CTE 替换为通常更快的子查询。
要产生真正随机的组合,将rn
随机化以得到更大的集合就足够了:
SELECT c_id, t_id
FROM (
SELECT id AS c_id, row_number() OVER (ORDER BY random()) AS rn
FROM custassets
) x
JOIN (SELECT id AS t_id, row_number() OVER () AS rn FROM tags) y USING (rn);
如果 任意 组合足够好,这会更快(尤其是对于大表):
SELECT c_id, t_id
FROM (SELECT id AS c_id, row_number() OVER () AS rn FROM custassets) x
JOIN (SELECT id AS t_id, row_number() OVER () AS rn FROM tags) y USING (rn);
如果两个表中的行数不匹配,并且您不想丢失较大表中的行,请使用modulo operator %
多次连接较小表中的行:
SELECT c_id, t_id
FROM (
SELECT id AS c_id, row_number() OVER () AS rn
FROM custassets -- table with fewer rows
) x
JOIN (
SELECT id AS t_id, (row_number() OVER () % small.ct) + 1 AS rn
FROM tags
, (SELECT count(*) AS ct FROM custassets) AS small
) y USING (rn);
正如我在评论中提到的,window functions (with appended OVER
clause) 在 PostgreSQL 8.4 或更高版本中可用。
【讨论】:
欧文,感谢您的详尽回答 - 非常感谢。我现在也应该查一下WITH
和USING
! </brainmelt> :)
@halfer:不用担心,两者都很容易理解。 CTE 基本上是可以多次使用的子查询,USING (rn)
基本上是ON x.rn = y.rn
的缩写。不过,也有细微的差别。只需点击我的链接即可。【参考方案2】:
这是一种从 2 个表中随机选择单个组合的不同方法,假设两个表 a
和 b
,均具有主键 id
。表不必大小相同,并且第二行独立于第一行选择,这对于 testdata 可能不是那么重要。
SELECT * FROM a, b
WHERE a.id = (
SELECT id
FROM a
OFFSET (
SELECT random () * (SELECT count(*) FROM a)
)
LIMIT 1)
AND b.id = (
SELECT id
FROM b
OFFSET (
SELECT random () * (SELECT count(*) FROM b)
)
LIMIT 1);
用两张表进行测试,一张有 7000 行,一张有 100k 行,结果:立即。对于多个结果,您必须重复调用查询 - 增加 LIMIT 并将 x.id =
更改为 x.id IN
将产生 (aA, aB, bA, bB) 结果模式。
【讨论】:
【参考方案3】:只是一个简单的 carthesian 乘积 ON random() 似乎工作得相当好。简单的comme bonjour ...
-- Cartesian product
-- EXPLAIN ANALYZE
INSERT INTO dirgraph(point_from,point_to,costs)
SELECT p1.the_point , p2.the_point, (1000*random() ) +1
FROM allpoints p1
JOIN allpoints p2 ON random() < 0.002
;
【讨论】:
【参考方案4】:如果您只想从每一侧获取一组随机行,请使用伪随机数生成器。我会使用类似的东西:
select *
from (select a.*, row_number() over (order by NULL) as rownum -- NULL may not work, "(SELECT NULL)" works in MSSQL
from a
) a cross join
(select b.*, row_number() over (order by NULL) as rownum
from b
) b
where a.rownum <= 30 and b.rownum <= 30
这是一个笛卡尔积,它返回 900 行,假设 a 和 b 至少有 30 行。
但是,我将您的问题解释为随机组合。再一次,我会选择伪随机方法。
select *
from (select a.*, row_number() over (order by NULL) as rownum -- NULL may not work, "(SELECT NULL)" works in MSSQL
from a
) a cross join
(select b.*, row_number() over (order by NULL) as rownum
from b
) b
where modf(a.rownum*107+b.rownum*257+17, 101) < <some vaue>
这可以让您获得任意行之间的组合。
【讨论】:
感谢您的回复;是的,这是我需要的随机组合(为了清楚起见,我已将有问题的结果集添加到问题中)。我尝试了您的第二个查询,但我不确定 Postgres (8.4) 是否支持OVER
。这是 MSSQL Server 专用的关键字吗?
@halfer:窗口函数(包括row_number()
)是supported in Postgres 8.4。但是,OVER (ORDER BY NULL)
只是噪音,可以简化为OVER ()
。两者都不擅长产生随机结果。您会得到一个特定于实现的任意顺序,大部分与输入行的顺序相同。
@ErwinBrandstetter - 谢谢。我彻底搜索了“postgresql over”,但一定错过了——也许“over”这个词太常见了!我对这组函数不熟悉,所以我会阅读它们。
windowing子句中order by的要点是生成任意顺序的序列。绝对可以包括其他值。伪随机数生成器的行为更像是“第 n 个”选择而不是“顶部”选择,因此即使具有真正的排序也可能会产生伪随机结果。
戈登,非常感谢。 PG 抱怨了modf
,所以我把我自己的%
和LIMIT 50
一起放进去,效果很好。关于伪随机性,您是对的 - 我尝试了ORDER BY RANDOM()
,并且在我取消它后查询继续运行了几分钟! (我在这些表中分别有 50k 和 5k 行)。而且我的用例也不需要真正的随机性。【参考方案5】:
WITH a_ttl AS (
SELECT count(*) AS ttl FROM custassets c),
b_ttl AS (
SELECT count(*) AS ttl FROM tags),
rows AS (
SELECT gs.*
FROM generate_series(1,
(SELECT max(ttl) AS ttl FROM
(SELECT ttl FROM a_ttl UNION SELECT ttl FROM b_ttl) AS m))
AS gs(row)),
tab_a_rand AS (
SELECT custassets_id, row_number() OVER (order by random()) as row
FROM custassets),
tab_b_rand AS (
SELECT id, row_number() OVER (order by random()) as row
FROM tags)
SELECT a.custassets_id, b.id
FROM rows r
JOIN a_ttl ON 1=1 JOIN b_ttl ON 1=1
LEFT JOIN tab_a_rand a ON a.row = (r.row % a_ttl.ttl)+1
LEFT JOIN tab_b_rand b ON b.row = (r.row % b_ttl.ttl)+1
ORDER BY 1,2;
您可以在SQL Fiddle 上测试此查询。
【讨论】:
唷,如果欧文的解决方案让我的大脑过热,这个会崩溃成黑洞!付出了巨大的努力,还有一个 SQLfiddle;谢谢和+1。【参考方案6】:让我感到困扰的是,经过这么多年的关系数据库,似乎没有很好的跨数据库方式来做这样的事情。 MSDN 文章http://msdn.microsoft.com/en-us/library/cc441928.aspx 似乎有一些有趣的想法,但那当然不是PostgreSQL。即使那样,他们的解决方案也需要一次通过,而我认为它应该能够在没有扫描的情况下完成。
我可以想象一些无需通过(在选择中)就可以工作的方法,但它会涉及创建另一个表,将表的主键映射到随机数(或稍后随机选择的线性序列,在某些情况下方法实际上可能更好),当然,这也可能存在问题。
我意识到这可能是一个无用的评论,我只是觉得我需要咆哮一下。
【讨论】:
嘿,好吧,如果答案是“不可能”,那么这很公平:)
。我们会看到其他答案。
确实,我也想看看还有什么其他答案。我并不是要暗示答案是不可能的,我只是说“不好”,因为在特定的解决方案中似乎需要大量设置或接近全表扫描。我不得不承认我不确定您的查询有什么问题。
您可能错过了当今大多数现代 RDBMS 支持窗口函数(mysql 是一个不光彩的例外)。这里的所有答案在 MSSQL、Oracle 和 PostgreSQL 中应该基本相同。以上是关于如何在 PostgreSQL 中获得随机笛卡尔积?的主要内容,如果未能解决你的问题,请参考以下文章