使用 Rails exists? 查询解决 N+1 问题
我们最近遇到一个性能问题:在 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
,以仅加载必需的记录。
关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台
除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接