用例是,我有一个表产品和 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
位于1
和X*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
优点
缺点
解决方案2:逐块PRNG
可以为使用permutate(seed, start, end, value)
随机性的域[START, END]
创建排列函数seed
。在t0
时间,用户A
有0
个匹配的产品,并观察到存在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
分配为新END
。 seed
和C
保持不变。
优点
缺点
答案 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;
请注意,这些计算需要少量时间,Products
和User_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可用产品,它将无限期运行。当然,额外的逻辑可以解决这个问题。