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
相关产品推荐
相关产品推荐

