【问题标题】:Select random non-repeating rows per user为每个用户选择随机的非重复行
【发布时间】:2015-01-13 02:39:32
【问题描述】:

用例是,我有一个表 productsuser_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,其中i1X*C之间,在[min_id, max_id]范围内,以及希望像下面这样的 select 会返回 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

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

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

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)

缺点

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

【问题讨论】:

  • 您使用的是哪个 DBMS?后格雷斯?甲骨文?
  • Postgres 但我猜这没关系? Oracle 是否提供任何特殊功能来帮助我解决这个问题?

标签: sql database algorithm math random


【解决方案1】:

如果要选择用户没有的 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;

问题是:什么是“Y”?好吧,假设产品数量为 P,用户有 U。那么,如果我们随机选择一组 (X + U) 个产品,我们肯定可以得到用户没有的 X 个产品。这表明表达式random() &lt; (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/1000000。该查询将扫描 products 表,随机抽出 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 可用产品,它将无限期地运行。当然,额外的逻辑可以解决这个问题。

【讨论】:

  • 我要多想想你写的东西,但是这个操作会经常被调用,所以不能把它放到内存中。因此,表扫描也不是一种选择。
  • @ChristianBeikov 。 . .除了给定用户已经拥有的产品列表之外,我的解决方案中没有关于将“它”放入内存的任何内容。
【解决方案2】:

我会采用方法 #1。

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

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

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

【讨论】:

  • 虽然我可以随时从#2切换到#1,但不能从#1切换到#2,你会推荐这个吗?大多数情况下,检索将在 O(1) 中完成,但有时可能需要更长的时间。这就是我对方法 1 的担忧。
猜你喜欢
  • 1970-01-01
  • 2017-10-25
  • 2013-04-09
  • 1970-01-01
  • 2012-10-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-04
相关资源
最近更新 更多