我有一个应用程序,用户可以从300种可能的兴趣中选择各种兴趣。每个选定的兴趣都存储在包含user_id和interest_id列的连接表中。
典型用户从300中选择约50个。
我想构建一个系统,用户可以在其中找到与他们最感兴趣的前20位用户。
现在我可以使用以下查询完成此操作:
SELECT i2.user_id, count(i2.interest_id) AS count
FROM interests_users as i1, interests_users as i2
WHERE i1.interest_id = i2.interest_id AND i1.user_id = 35
GROUP BY i2.user_id
ORDER BY count DESC LIMIT 20;
但是,此查询在连接表中使用10,000个用户和500,000行执行大约需要500毫秒。所有索引和数据库配置设置都已尽力调整。
我还尝试使用以下查询完全避免使用连接:
select user_id,count(interest_id) count
from interests_users
where interest_id in (13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,508)
group by user_id
order by count desc
limit 20;
但这个更慢(约800毫秒)。
我怎样才能最好地将收集此类数据的时间降低到100毫秒以下?
我考虑将这些数据放入像Neo4j这样的图形数据库中,但我不确定这是否是最简单的解决方案,或者它是否比我目前所做的更快。
答案 0 :(得分:1)
您所谈论的内容称为群集。
群集是一个难题,在运行中计算它需要的资源比我们想要的更多,我担心,因为完整的计算是O(N 2 )。
我认为在这条路上寻找想法不太可能产生任何结果(我可能是错的),因为问题固有的复杂性。
但是,我们不必每次都从头开始计算。我无法弄清楚一个不断发展的图片(合理)以及如何更新它。
但我可以弄清楚如何缓存结果!
UserId* | LinkedUserId* | Count
35 | 135 | 47
35 | 192 | 26
(UserId的一个索引和LinkedUserId的另一个索引,unicity的约束是永远不应该有2行具有相同的UserId / LinkedUserId对)
每当您需要为此用户获取组时,请先查阅缓存表。
现在,我们还需要不时地使一些缓存条目无效:每次用户添加或删除兴趣时,它都可能影响链接到她的所有用户。
当用户添加条目时,使用此兴趣使用户的所有缓存行无效。
当用户删除条目时,使与她链接的用户的所有缓存行无效。
老实说,我不确定它会表现得更好。
答案 1 :(得分:1)
SELECT DISTINCT TOP 20 b.user_id, SUM(1) OVER (PARTITION BY b.user_id) AS match
FROM interests_users a
LEFT OUTER JOIN interests_users b ON a.interest_id = b.interest_id AND b.user_id <> 35
WHERE a.user_id = 35 AND b.user_id IS NOT NULL
ORDER BY 2 DESC
如果你建立了良好的索引,你应该没事。
答案 2 :(得分:1)
在纯Ruby中,我实际上能够基本上得到我想要的东西。
首先,我创建一个二维数组,其中每列是用户,每行都是感兴趣的。数组中的每个值都是0或1,具体取决于当前用户是否具有该兴趣。该数组存储在内存中,其中包含添加或修改行和列的函数。
然后,当我想要计算与当前用户具有相似兴趣的用户时,我将为当前用户将列设置为“1”的每一行添加所有列。这意味着我需要遍历10,000列并且每列平均运行50次添加操作,然后在最后执行排序操作。
您可能会猜测这需要很长时间,但实际上我的机器(Core 2 Duo,3ghz.Ruby 1.9.1)大约需要50-70毫秒,而我们的生产服务器大约需要110毫秒。好消息是我甚至不需要限制结果集。
这是我用来测试算法的ruby代码。
USERS_COUNT = 10_000
INTERESTS_COUNT = 500
users = []
0.upto(USERS_COUNT) { |u| users[u] = rand(100000)+100000 }
a = []
0.upto(INTERESTS_COUNT) do |r|
a[r] = []
0.upto(USERS_COUNT) do |c|
if rand(10) == 0 # 10% chance of picking an interest
a[r][c] = 1
else
a[r][c] = 0
end
end
end
s = Time.now
countable_rows = []
a.each_index { |i| countable_rows << i unless a[i][0].zero? }
b = {}
0.upto(USERS_COUNT) do |c|
t = 0
countable_rows.each { |r| t+= a[r][c] }
b[t] = users[c]
end
b = b.sort {|x,y| y[0] <=> x[0] }
puts Time.now.to_f - s.to_f
前几行用于创建模拟二维数组。程序的其余部分运行我上面描述的算法。
上面的算法在一段时间内相当好地扩展。显然它不适合50,000多个用户,但由于我们的产品将社区细分为较小的组,因此这种方法运行良好(并且比SQL快得多)。
欢迎任何关于如何调整以获得更好性能的建议。
答案 3 :(得分:1)
您发布的代码答案不正确。通过将计数存储在哈希中,您将忘记许多用户,因为每个用户只保留一个用户。例如,如果两个用户具有相同的兴趣(或者至少与当前用户具有相同数量的匹配兴趣),那么您的t
变量将是相同的,并且第一个看到的将被覆盖。第二
以下是您发布的答案代码的正确版本。它更短,更惯用,也应该更快。请注意,我使用的是true
和false
,而不是1
和0
。
USERS_COUNT = 10_000
INTERESTS_COUNT = 500
users = Array.new(USERS_COUNT) { rand(100000)+100000 }
table = Array.new(INTERESTS_COUNT) do
Array.new(USERS_COUNT) { rand(10) == 0 }
end
s = Time.now
cur_user = 0
cur_interests = table.each_index.select{|i| table[i][cur_user]}
scores = Array.new(USERS_COUNT) do |user|
nb_match = cur_interests.count{|i| table[i][user] }
[nb_match, users[user]]
end
scores.sort!
puts Time.now.to_f - s.to_f
顺便说一句,您可以通过转置table
来提高性能,这样可以避免一半的查找。