我们最近遇到一个性能问题:在 Rails 应用的某个 API 端点中有许多 N+1 的 .exists? 查询。在以下查询中,我们检查用户是否启用了 “data_imports” 功能:

user.beta_feature.where(name: "data_imports").enabled.exists?

输出:

BetaFeature Exists? (0.6ms)  SELECT 1 AS one FROM `beta_feature` WHERE `beta_feature`.`name` = 'data_imports' AND `beta_feature`.`target_type` = 'User' AND `beta_feature`.`target_id` = 1 AND `beta_feature`.`enabled_at` IS NOT NULL LIMIT 1

最初,这种模式表现良好,但随着我们添加更多的 beta 功能,每新增一个 beta 功能,都会引入额外的查询,从而开始影响 API 端点的速度。

通常解决 N+1 查询的问题

通常,Rails 应用可以通过使用 includes 来预加载数据解决 N+1 的问题。然而,这种方法对 exists? 查询无效,因为 Rails 依旧会执行查询。


解决 N+1 问题

以下是我们解决 Rails exists? 查询 N+1 问题的方法。在用户模型中,我们最初使用以下方法检查 beta 功能:

def beta_feature_enabled?(name)
  beta_features.where(name: name).enabled.exists?
end

这种方式会在每次调用时执行查询,无论 beta_features 是否已经加载。
一种避免查询的方法是预加载所有记录,然后在内存中检查它们,而不是执行查询:

# 新方法,允许预加载 beta_features
def beta_feature_enabled?(name)
  if beta_features.loaded?
    beta_features.any? { |f| f.name == name.to_s && f.enabled? }
  else
    beta_features.where(name: name.to_s).enabled.exists?
  end
end

现在,如果我们在控制器中使用 includes 预加载 beta_features,它就会被提前加载,任何对 beta_feature_enabled? 的调用都不会执行额外查询:

# 执行两次查询:加载用户和关联的 beta_features
@users = User.all.includes(:beta_features)

对于单个记录加载,也可以使用该技术减少查询次数:

@user = User.find(params[:id])
@user.beta_features.load # 预加载用户的所有 beta_features

使用作用域进行预加载

以上方法会为每个用户加载所有的 beta_features。在我们的场景中,这是我们需要的。然而,如果你的应用程序只检查少数几个功能,这可能会导致加载了不必要的记录。
如果这是你的场景,可以设置一个新的关联,仅加载所需的记录:

has_many :beta_features, as: :target, dependent: :destroy_async
PRELOADED_FLAGS = %w[dark_mode insights data_imports]
has_many :preloaded_beta_features, -> { where(name: PRELOADED_FLAGS) }, as: :target, class_name: "BetaFeature"

现在,你可以将 beta_features 替换为 preloaded_beta_features,以仅加载必需的记录。



使用 Rails exists? 查询解决 N+1 问题插图

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:http://choupangxia.cn/2025/09/11/rails-exists-n1/