尽管缓存

时间:2015-09-09 16:53:52

标签: sql ruby-on-rails performance postgresql activerecord

所以在我的社交网站上,类似于facebook,我的搜索速度就像这一部分的98%瓶颈一样。我想根据搜索用户拥有的共同朋友的数量对结果进行排名,以及所有结果(我们可以假设他们是用户)

我的朋友表有3列 -

  • user_id(发送请求的人)
  • friend_id(收到请求的人)
  • pending(布尔值表示请求是否被接受)

user_id和friend_id都是引用users.id的外键。

查找用户的friend_ids很简单,看起来像这样

def friends
  Friend.where(
    '(user_id = :id OR friend_id = :id) AND pending = false',
     id: self.id
  ).pluck(:user_id, :friend_id)
   .flatten
   .uniq
   .reject { |id| id == self.id }
end

因此,在获得与搜索查询匹配的结果后,由共同的朋友对结果进行排名后,需要执行以下步骤 -

  • 获取所有搜索用户的朋友的user_id - 设置(A)。上面提到的朋友方法做到了这个
  • 循环设置(A)中的每个ID -
    • 获取| id |的所有朋友的user_ids - 设置(B)。再次,由朋友方法
    • 完成
    • 查找集合A和集合B的交集长度
  • 按所有结果的交叉点长度的降序排列

这里最昂贵的操作显然得到了数百名用户的friend_ids。所以我缓存了所有用户的friend_ids以加快速度。性能上的差异是惊人的,但我很好奇是否可以进一步改进。

我想知道是否有一种方法可以在单个查询中获得所有所需用户的friend_id,这是有效的。像 -

这样的东西
SELECT user_id, [array of friend_ids of the user with id = user_id]
FROM friends
....

有人可以帮我写一个快速SQL或ActiveRecord查询吗?

这样我就可以将所有搜索结果的user_id及其对应的friend_id存储在散列或其他快速数据结构中,然后执行相同的排名操作(我在上面提到过)。由于我没有为成千上万的用户和他们的friend_ids访问缓存,我​​认为它会显着加快这个过程

3 个答案:

答案 0 :(得分:1)

如果您希望自己的网站能够扩展到大量用户,那么在RAM中缓存create table users ( user_id int not null primary key, nick varchar(32) ); create table friends ( user_id int not null, friend_id int not null, pending bool, primary key (user_id, friend_id), foreign key (user_id) references users(user_id), foreign key (friend_id) references users(user_id), check (user_id < friend_id) ); 表并不是一种可行的方法,但我确信它对少数用户来说非常有用。

通过尽可能少的通话,您可以从数据库中获得最多的工作,这对您有利。发出大量查询效率很低,因为每个查询的开销相对较大。此外,数据库是为您尝试执行的任务而构建的。我认为你在Ruby方面做了太多的工作,你应该让数据库做它最擅长的工作。

你没有提供很多细节,所以我决定首先定义一个最小模型DB:

check

friends上的create view friends_symmetric (user_id, friend_id) as ( select user_id, friend_id from friends where not pending union all select friend_id, user_id from friends where not pending ); 约束避免了两个订单中表中列出的同一对用户,当然PK会阻止同一对以相同的顺序多次注册。 PK还会自动拥有与之关联的唯一索引。

因为我认为&#39;是&#39;的朋友。关系应该是逻辑对称的,定义一个呈现对称性的视图很方便:

friends

(如果友谊对称,那么您可以删除检查约束和视图,并使用表friends_symmetric代替后面的select * from users where nick like 'Sat%'; 。)

作为一个模型查询,你要对其结果进行排名,那么,我接受这个:

select *
from (
    select
      u.*,
      count(mutual.shared_friend_id) over (partition by u.user_id) as num_shared,
      row_number() over (partition by u.user_id) as copy_num
    from 
      users u
      left join (
          select
            f1.friend_id as shared_friend_id,
            f2.friend_id as friend_id
          from friends_symmetric f1
            join friends_symmetric f2
              on f1.friend_id = f2.user_id
          where f1.user_id = ?
            and f2.friend_id != f1.user_id
        ) mutual
        on u.user_id = mutual.friend_id
    where u.nick like 'Sat%'
  ) all_rows
where copy_num = 1
order by num_shared desc

目标是按每个匹配的朋友数量的降序返回结果行,User1是代表查询运行的用户。你可以这样做:

更新:修改此查询以过滤掉重复的结果)

?

其中{{1}}是包含User1 ID的参数的占位符。

已编辑添加:

我使用窗口函数而不是聚合查询来构造此查询,并认为这样的结构将更易于查询规划器进行优化。然而,内联视图&#34; mutual&#34;可以改为被构造为聚合查询,该聚合查询计算搜索用户与共享至少一个朋友的每个用户共享的共享朋友的数量,并且这将允许避免一级内联视图。如果提供的查询的性能变得不充分,那么测试该变体是值得的。

还有其他方法可以解决在DB中执行排序的问题,其中一些可能表现更好,并且可能有办法通过调整数据库来提高每个数据的性能(添加索引或约束,修改表定义) ,计算db statistics,...)。

我无法预测该查询是否会胜过您现在正在做的事情,但我向您保证它会更好地扩展,并且更容易维护。

答案 1 :(得分:0)

假设您想要一个主键为User的{​​{1}}模型的关系,您应该能够加入一个计算共同朋友数量的子查询:

id

子查询选择具有关联朋友和内部联接的所有用户ID,并选择具有当前用户所有朋友ID的另一个用户ID。由于它按class User < ActiveRecord::Base def other_users_ordered_by_mutual_friends self.class.select("users.*, COALESCE(f.friends_count, 0) AS friends_count").joins("LEFT OUTER JOIN ( SELECT all_friends.user_id, COUNT(DISTINCT all_friends.friend_id) AS friends_count FROM ( SELECT f1.user_id, f1.friend_id FROM friends f1 WHERE f1.pending = false UNION ALL SELECT f2.friend_id AS user_id, f2.user_id AS friend_id FROM friends f2 WHERE f2.pending = false ) all_friends INNER JOIN ( SELECT DISTINCT f1.friend_id AS user_id FROM friends f1 WHERE f1.user_id = #{id} AND f1.pending = false UNION ALL SELECT DISTINCT f2.user_id FROM friends f2 WHERE f2.friend_id = #{id} AND f2.pending = false ) user_friends ON user_friends.user_id = all_friends.friend_id GROUP BY all_friends.user_id ) f ON f.user_id = users.id").where.not(id: id).order("friends_count DESC") end end 分组并选择计数,因此我们会得到每个user_id的共同朋友数。我没有测试过这个,因为我没有任何样本数据,但它应该可以工作。

由于这会返回范围,因此您可以将其他范围/条件链接到关系:

user_id

所写的current_user.other_users_ordered_by_mutual_friends.where(attribute1: value1).reorder(:attribute2) 范围还允许您访问关系中实例的字段select

friends_count

答案 2 :(得分:0)

John对friends_symetric视图有个好主意。使用两个过滤的索引(一个打开(friend_id,user_id,另一个打开(user_id,friend_id)),它会很好用。 但是查询可以更简单一些

WITH user_friends AS(
  SELECT user_id, array_agg(friend_id) AS friends
    FROM friends_symmetric
        WHERE user_id = :user_id -- id of our user
    GROUP BY user_id
)
SELECT u.*
       ,array_agg(friend_id) AS shared_friends -- aggregated ids of friends in case they are needed for something
       ,count(*) AS shared_count    
FROM user_friends AS uf     
    JOIN friends_symmetric AS f
        ON f.user_id = ANY(uf.friends) AND f.friend_id = ANY(uf.friends)
    JOIN user
        ON u.user_id = f.user_id
WHERE u.nick LIKE 'Sat%' --nickname of our user's friend
GROUP BY u.user_id