优化ActiveRecord查询:获取各分类最畅销书籍(无循环无原生SQL)
问题:高效查询每个分类的最畅销书籍
我在查找每个分类的最畅销书籍时遇到了性能优化问题,现有模型关联如下:
模型定义
# Book 模型 class Book < ApplicationRecord has_many :books_categories, dependent: :destroy has_many :categories, through: :books_categories has_many :order_items, dependent: :destroy end # BooksCategory 模型 class BooksCategory < ApplicationRecord belongs_to :category belongs_to :book end # Category 模型 class Category < ApplicationRecord has_and_belongs_to_many :books end # OrderItem 模型 class OrderItem < ApplicationRecord belongs_to :order belongs_to :book end # Order 模型 class Order < ApplicationRecord has_many :order_items, dependent: :destroy has_many :books, through: :order_items end
需求
找出每个分类的最畅销书籍,仅当订单状态为2或3时,书籍才算售出。
我自己写了一段可运行但未优化的代码,用了循环遍历每个分类:
def best_sellers books_ids = [] Category.all.each do |category| temp_hash = OrderItem.joins(book: :books_categories) .where(books: { books_categories: { category: category } }) .joins(:order).where(orders: { status: [2, 3] }) .group(:book_id).sum(:quantity) books_ids << temp_hash.key(temp_hash.values.max) end Book.where(id: books_ids) end
现在想把它合并为单个无循环、无原生SQL的Active Record查询,请问该如何实现?
优化方案:无循环的Active Record查询
嘿,我来帮你搞定这个优化!你的循环写法会触发N+1查询(每个分类一次查询),当分类数量多的时候性能会拉胯。我们可以利用Active Record支持的窗口函数(rank())来实现单查询解决,完全不需要循环,也不用硬写原生SQL:
def best_sellers # 第一步:计算每本书在对应分类下的总销量,并按分类给书籍销量排名 ranked_sales = OrderItem.joins(:order, book: :categories) .where(orders: { status: [2, 3] }) .select( 'books.id AS book_id', 'categories.id AS category_id', 'SUM(order_items.quantity) AS total_sold', 'RANK() OVER (PARTITION BY categories.id ORDER BY SUM(order_items.quantity) DESC) AS sales_rank' ) .group('books.id, categories.id') # 第二步:筛选每个分类销量排名第一的书籍,关联获取完整Book对象 Book.joins("JOIN (#{ranked_sales.to_sql}) ranked_sales_sub ON books.id = ranked_sales_sub.book_id") .where('ranked_sales_sub.sales_rank = 1') .distinct # 处理同一本书属于多个分类且都是该分类销量冠军的情况 end
代码细节解释
销量计算与排名
- 通过
joins同时关联order(过滤状态为2/3的有效订单)和book的categories,确保我们能把书籍和它所属的分类关联起来 - 用
SUM(order_items.quantity)统计每本书在对应分类下的总销量 RANK() OVER (PARTITION BY categories.id ORDER BY SUM(order_items.quantity) DESC)是核心:按分类分组(PARTITION BY categories.id),对每个分类内的书籍按销量降序排名,销量最高的书籍会得到sales_rank = 1
- 通过
筛选冠军书籍
- 把第一步的查询作为子查询,和
Book表关联 - 筛选出
sales_rank = 1的记录,也就是每个分类的销量冠军 - 添加
distinct是为了避免同一本书在多个分类都是冠军时被重复返回
- 把第一步的查询作为子查询,和
为什么这个方案更好?
- 只执行两次SQL查询(子查询+主查询),相比原来的N+1查询性能提升巨大,尤其是分类数量较多时
- 全程用Active Record的方法链构建,没有硬编码原生SQL片段,保持了代码的可读性和可维护性
- 自动处理了同一本书属于多个分类的场景,也能正确处理分类内多本书销量并列第一的情况(
RANK()会给并列的书籍都打上sales_rank = 1)
内容的提问来源于stack exchange,提问作者Jason
相关产品推荐
相关产品推荐

