You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rails 带分页的复杂群组筛选问题咨询

群组推荐查询优化落地方案

现有实现的核心问题根因

  • 所有过滤逻辑均在Ruby内存层完成:每次请求需要执行5-6次全表JOIN、GROUP BY查询,把符合/不符合的群组ID全部加载到内存后拼数组做where.not,数据量上升后数据库CPU、内存占用会随用户量线性暴涨
  • 分页逻辑割裂:将「符合偏好」「不符合偏好」两批数据作为完全独立的集合做分页,没有统一的排序优先级。当第一集合的剩余数据不足整页时,会直接切换到第二集合的首页数据,出现页码与实际内容错位、跳过展示的问题
  • 随机排序无固定锚点:直接用RANDOM()排序时,每次数据库查询返回的结果顺序完全随机,分页过程中翻到下一页时会出现数据重复、漏数据的问题

优化后实现逻辑

1. 所有筛选逻辑下沉到数据库层,彻底消除重复pluck查询

把原来需要加载到内存计算的过滤规则,全部改成单条SQL内的关联子查询,不再反复查询全量ID列表:

  • 群成员数量匹配校验:直接通过user_groups关联统计每个群的joined状态成员数,和用户偏好值做比对,不再单独查询不符合数量的群组ID
  • 群成员性别匹配校验:直接关联用户表判断群内所有joined状态用户的性别是否符合用户偏好,不再单独查询含不符合性别的群组ID
  • 屏蔽关联群过滤:直接用EXISTS子查询判断当前群是否包含已被当前用户屏蔽的成员,不再先查被屏蔽用户ID、再查用户所在群ID
  • 已操作群过滤:直接关联matches表判断当前用户是否对该群有点赞、点踩、匹配、屏蔽等操作,不再加载所有已操作的群组ID到内存

2. 统一结果集的优先级排序,解决分页跳页问题

把「符合偏好」「不符合偏好」的群组合并到同一个查询结果集里,在SQL层给每个群组打优先级标记:

  • 满足所有偏好规则(成员数匹配、性别匹配、在用户设定的距离范围内)的群组,优先级标记为0
  • 不满足上述规则的群组,优先级标记为1
    排序时先按优先级升序排列,保证所有符合偏好的群组排在结果集最前面,后面衔接不符合偏好的群组。整个结果集是连续的、顺序固定的,分页时不管翻到第几页,顺序都不会乱,从根源上避免跳页。

排序补充优化:如果需要同优先级下做随机排序,不要每次查询用随机的排序值。可以在用户第一次加载推荐时生成一个固定的随机种子(比如1-1000000的整数),后续所有分页请求都携带这个种子,SQL里用RANDOM(种子值)做排序,保证同一次浏览会话下的结果顺序完全固定,不会出现随机排序导致的分页错乱。

3. 核心实现代码参考

Group模型里新增可复用的scope

scope :recommend_for, ->(current_user, user_setting) {
  joined_user_status = UserGroup.statuses[:joined]
  blocked_friend_status = Friendship.statuses[:blocked]
  interacted_statuses = Match.statuses.values_at(:like, :match, :unlike, :block, :blocked, :report, :reported)

  # 基础过滤:活跃状态、非置顶群、无屏蔽关联用户、无用户已操作记录
  base = active
         .where(spot_light_enabled: false)
         .where.not(
           "EXISTS (
             SELECT 1 FROM user_groups ug
             INNER JOIN friendships f ON ug.user_id = f.friend_id
             WHERE ug.group_id = groups.id
               AND ug.status = ?
               AND f.user_id = ?
               AND f.status = ?
           )", joined_user_status, current_user.id, blocked_friend_status
         )
         .where.not(
           "EXISTS (
             SELECT 1 FROM matches m
             WHERE m.matcher_id = groups.id
               AND m.user_id = ?
               AND m.status IN (?)
           )", current_user.id, interacted_statuses
         )

  # 计算每个群组的推荐优先级
  base.select(
    "groups.*,
     CASE
       WHEN
         # 校验群成员数匹配
         EXISTS (
           SELECT 1 FROM user_groups ug
           WHERE ug.group_id = groups.id
             AND ug.status = #{joined_user_status}
           GROUP BY ug.group_id
           HAVING COUNT(ug.id) = #{user_setting.group_member_count}
         )
         # 校验距离范围(在用户设定的偏好距离内)
         AND groups.latitude BETWEEN ? AND ?
         AND groups.longitude BETWEEN ? AND ?
         # 校验性别偏好匹配
         AND (
           NOT user_setting.male_only
           OR EXISTS (
             SELECT 1 FROM user_groups ug
             INNER JOIN users u ON ug.user_id = u.id
             WHERE ug.group_id = groups.id
               AND ug.status = #{joined_user_status}
             GROUP BY ug.group_id
             HAVING COUNT(DISTINCT u.gender) = 1
                AND MAX(u.gender) = #{User.genders[:male]}
           )
         )
         AND (
           NOT user_setting.female_only
           OR EXISTS (
             SELECT 1 FROM user_groups ug
             INNER JOIN users u ON ug.user_id = u.id
             WHERE ug.group_id = groups.id
               AND ug.status = #{joined_user_status}
             GROUP BY ug.group_id
             HAVING COUNT(DISTINCT u.gender) = 1
                AND MAX(u.gender) = #{User.genders[:female]}
           )
         )
       THEN 0
       ELSE 1
     END AS recommend_priority"
  ).order(recommend_priority: :asc)
}

控制器里的分页逻辑

def index
  user = Current.user
  setting = user.setting
  # 首次加载时生成固定随机种子,后续分页透传
  random_seed = params[:random_seed] || rand(1..1000000)

  # 统一的推荐查询,自动按照优先级排序,自动衔接偏好范围内/外的群组
  recommended = Group.recommend_for(user, setting)
                     .includes(:group_status, :users)
                     .near(
                       [user.latitude, user.longitude],
                       setting.max_distance,
                       order: "RANDOM(#{random_seed})"
                     )
                     .page(params[:page])
                     .per(params[:per_page])

  # 分页元数据直接基于当前查询对象生成,不需要单独重复查询
  pagination = pagination_dict(recommended)

  render json: {
    groups: recommended,
    pagination: pagination.merge(random_seed: random_seed)
  }
end

性能配套优化

需要给对应表加复合索引,避免全表扫描:

  • user_groups表:复合索引(group_id, status, user_id),覆盖成员数统计、关联用户查询
  • matches表:复合索引(user_id, matcher_id, status),覆盖已操作群组的排除查询
  • friendships表:复合索引(user_id, friend_id, status)、(friend_id, user_id, status),覆盖屏蔽用户关联查询
  • groups表:复合索引(status, spot_light_enabled, latitude, longitude),覆盖基础筛选和距离范围查询

后续需要新增「排除已展示群组」「排除用户已加入的群组」等校验规则时,直接在这个统一的查询里追加对应子查询即可,不会额外增加多次pluck查询的性能开销。

内容的提问来源于stack exchange,提问作者Muhammad Faisal Iqbal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.22 08:45:04