为每个用户选择随机非重复行

时间:2014-11-15 22:25:27

标签: sql database algorithm math random

用例是,我有一个表产品 user_match_product 。对于特定用户,我想选择该用户不匹配的X随机产品。

这种天真的方式,就是制作类似

的东西

SELECT * FROM products WHERE id NOT IN (SELECT p_id FROM user_match_product WHERE u_id = 123) ORDER BY random() LIMIT X

但是当拥有数百万行时,这将成为性能瓶颈。

我想到了一些可能的解决方案,我现在将在这里介绍。我很想知道您对该问题的解决方案或有关我的解决方案的建议。

解决方案1:信任随机性

基于产品ID单调递增的事实,可以乐观地生成X*C随机数R_i,其中i位于1X*C之间,它们在[min_id, max_id]范围内,并希望像下面这样的选择将返回X元素。

SELECT * FROM products p1 WHERE p1.id IN (R_1, R_2, ..., R_XC) AND NOT EXISTS (SELECT * FROM user_match_product WHERE u_id = 123 AND p_id = p1.id) LIMIT X

优点

  • 如果随机数生成器是好的,这可能会在 O(1)
  • 中很好地工作
  • 旧的和新添加的产品具有相同的选择概率

缺点

  • 如果匹配数接近产品数量,则碰撞概率可能非常高。

解决方案2:逐块PRNG

可以为使用permutate(seed, start, end, value)随机性的域[START, END]创建排列函数seed。在t0时间,用户A0个匹配的产品,并观察到存在E0个产品。 A处的用户t0的第一个块是域[1, E0]。用户会记住最初为C的计数器0

要选择X产品,用户A首先必须创建排列P_i,如

P_i = permutate(seed, START, END, C + i)

以下必须保留该功能。

  • permutate(seed, start, end, value)[start, end]
  • 的元素
  • value[start, end]
  • 的元素

以下查询将返回X个非重复元素。

SELECT * FROM products WHERE id IN (P_1, ..., P_X)

C到达END时,使用END + 1作为新START分配下一个块,将当前产品数E1分配为新ENDseedC保持不变。

优点

  • 不可能发生碰撞
  • 保证 O(1)

缺点

  • 必须先完成当前块才能选择新产品

2 个答案:

答案 0 :(得分:0)

我采用方法#1。

您可以通过计算C中的用户行(假定为唯一)来获得user_match_product的初步估算值。如果他已经拥有一半的可能产品,选择两倍于随机产品的数量似乎是一个很好的启发式。

您还可以进行最后一次修正,以验证提取的产品数量实际上是X.如果是X / 3,您需要再次运行相同的提取两次(避免已生成的随机产品ID),并将用户的C常数增加三倍。

另外,知道产品ID的范围是什么,您可以选择该范围内未出现在user_match_product中的随机数(即您的第一阶段查询仅针对user_match_product)比[{1}}有一个(多?)低的基数。然后,可以从products安全地选择那些通过测试的ID。

答案 1 :(得分:0)

如果您想选择用户没有的X产品,首先想到的是枚举产品并使用order by rand()(或等效产品,具体取决于数据库)。这是您的第一个解决方案:

SELECT p.*
FROM products p
WHERE NOT EXISTS (SELECT 1 FROM user_match_product WHERE ump.p_id = p.id and u_id = 123)
ORDER BY random()
LIMIT X;

提高效率的一种简单方法是选择任意子集。您实际上也可以使用random()执行此操作,但在where子句中

SELECT p.*
FROM products p
WHERE random() < Y AND
      NOT EXISTS (SELECT 1 FROM user_match_product WHERE ump.p_id = p.id and u_id = 123)
ORDER BY random()
LIMIT X;

问题是:什么是&#34; Y&#34;?好吧,让我们说产品的数量是P而用户有U.那么,如果我们选择一组随机的(X + U)产品,我们肯定可以得到用户没有的X产品。这表明表达式random() < (X + U) / P就足够了。唉,随机数字的变幻莫测说,有时我们会得到足够的,有时甚至不够。让我们添加3等因素是安全的。对于X,U和P的大多数值,这实际上非常非常非常安全。

这个想法是这样的查询:

SELECT p.*
FROM Products p CROSS JOIN
     (SELECT COUNT(*) as p FROM Products) v1 CROSS JOIN
     (SELECT COUNT(*) as u FROM User_Match_Product WHERE u_id = 123) v2
WHERE random() < 3 * (u + x) / p AND
      NOT EXISTS (SELECT 1 FROM User_Match_Product WHERE ump.p_id = p.id and ump.u_id = 123)
ORDER BY random()
LIMIT X;

请注意,这些计算需要少量时间,ProductsUser_Match_Product上有适当的索引。

因此,如果您有1,000,000个产品,而典型用户有20个。您想要推荐10个以上。然后表达式(20 + 10)* 3/1000000 - >百万分之九十零。此查询将扫描产品表,随机抽取90行 ,然后对它们进行排序并选择适当的10行。排序90行基本上是相对于原始操作的恒定时间。

出于许多目的,表扫描的成本是可以接受的。例如,它确实胜过了对所有数据进行排序的成本。

另一种方法是将用户的所有产品加载到应用程序中。然后拉出一个随机产品并与列表进行比较:

select p.id
from Products p cross join
     (select min(id) as minid, max(id) as maxid as p from Products) v1
where p.id >= minid + random() * (maxid - minid)
order by p.id
limit 1;

(注意,计算可以在查询之外完成,因此您只需插入一个常量。)

许多查询优化器将通过执行索引扫描来解析此查询常量时间。然后,您可以在应用程序中检查用户是否已拥有该产品。然后,这将为用户运行大约X次,从而提供O(1)性能。然而,这有一个相当糟糕的最坏情况表现:如果没有X可用产品,它将无限期运行。当然,额外的逻辑可以解决这个问题。