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

优化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

代码细节解释

  1. 销量计算与排名

    • 通过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
  2. 筛选冠军书籍

    • 把第一步的查询作为子查询,和Book表关联
    • 筛选出sales_rank = 1的记录,也就是每个分类的销量冠军
    • 添加distinct是为了避免同一本书在多个分类都是冠军时被重复返回

为什么这个方案更好?

  • 只执行两次SQL查询(子查询+主查询),相比原来的N+1查询性能提升巨大,尤其是分类数量较多时
  • 全程用Active Record的方法链构建,没有硬编码原生SQL片段,保持了代码的可读性和可维护性
  • 自动处理了同一本书属于多个分类的场景,也能正确处理分类内多本书销量并列第一的情况(RANK()会给并列的书籍都打上sales_rank = 1)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 08:41:46