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

Rails自关联模型预加载优化:如何减少重复SQL查询

问题描述

数据表结构

  • Category: id, name, parent_id(parent_id是自关联外键,指向其他Category记录)
  • CategoryImage: id, url, category_id
  • CategoryImageHistory: id, user_id, category_image_id

当前代码与问题

我需要加载所有层级的分类(从顶层到子级)及其关联的图片和图片历史,目前用的代码是:

Category.where(parent_id: nil)
  .includes(category_image: :category_image_history, children: [category_image: :category_image_history])

这段代码能正常返回数据,关联方法(.children、.category_images等)也能正常调用,但会生成重复的SQL查询:

SELECT categories.* FROM categories WHERE parent_id is NULL
SELECT category_images.* FROM category_images WHERE category_images.category_id IN (..)
SELECT category_image_histories.* FROM category_image_histories WHERE category_image_histories.category_image_id IN (..)
SELECT categories.* FROM categories WHERE categories.parent_id IN (..)
SELECT category_images.* FROM category_images WHERE category_images.category_id IN (..)
SELECT category_image_histories.* FROM category_image_histories WHERE category_image_histories.category_image_id IN (..)

能看到category_images和category_image_histories的查询被执行了两次——先查顶层分类的关联数据,再查子分类的。想找Rails原生的优化方式,实现先一次性查出所有分类(含子级),再一次性查出所有关联的图片和历史记录,同时保证数据关联正确。

优化方案

可以分两步处理,先拿到全量分类集合,再一次性预加载所有关联数据:

1. 获取所有层级的分类

先拿到顶层分类,再获取所有子分类,合并成完整的分类集合。这里提供两种方式:

方式1:递归查询(适合层级少的场景)

# 先拿顶层分类ID
top_category_ids = Category.where(parent_id: nil).pluck(:id)
all_category_ids = top_category_ids.dup
current_child_ids = top_category_ids

# 递归取出所有子分类ID
loop do
  child_ids = Category.where(parent_id: current_child_ids).pluck(:id)
  break if child_ids.empty?
  all_category_ids += child_ids
  current_child_ids = child_ids
end

# 获取包含层级结构的全量分类
all_categories = Category.where(id: all_category_ids).includes(:children)

方式2:CTE递归查询(适合层级多的场景,Rails 6+支持)

用with_recursive可以一次性查出所有层级的分类,性能更优:

all_categories = Category.with_recursive(all_categories: <<-SQL)
  SELECT * FROM categories WHERE parent_id IS NULL
  UNION ALL
  SELECT c.* FROM categories c JOIN all_categories ac ON c.parent_id = ac.id
SQL

2. 一次性预加载所有关联数据

基于全量分类集合,一次性预加载category_image和对应的category_image_history:

all_categories = all_categories.includes(category_image: :category_image_history)

3. 保持原返回结构(可选)

如果需要和原代码一样返回顶层分类数组,直接从全量集合里筛选即可:

top_categories_with_includes = all_categories.select { |c| c.parent_id.nil? }

优化后的SQL效果

优化后只会生成3条核心SQL(CTE方式是1条分类查询+2条关联查询):

-- 递归方式:先查顶层分类ID,再查全量分类
SELECT categories.* FROM categories WHERE id IN (所有分类ID)
-- CTE方式:一条查询拿到所有分类
WITH RECURSIVE all_categories AS (...) SELECT * FROM all_categories
-- 一次性查所有图片
SELECT category_images.* FROM category_images WHERE category_id IN (所有分类ID)
-- 一次性查所有图片历史
SELECT category_image_histories.* FROM category_image_histories WHERE category_image_id IN (所有图片ID)

这样就彻底避免了重复的关联查询,同时保持了Rails原生的关联加载逻辑,数据关联的正确性完全没问题。

内容的提问来源于stack exchange,提问作者nanakondor

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 15:10:25