N+1 là gì?
N+1 query là một vấn đề hiệu suất phổ biến trong các ứng dụng Ruby on Rails. Nó xảy ra khi bạn truy vấn một bảng để lấy dữ liệu, sau đó truy vấn bảng khác để lấy dữ liệu bổ sung cho mỗi đối tượng trong bảng đầu tiên.
Ví dụ: Bạn có bảng posts và bảng comments
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
end
Bạn muốn lấy danh sách tất cả các bài đăng cùng với danh sách tất cả các bình luận của chúng, bạn có thể viết mã như sau:
@posts = Post.limit(5)
@posts.each do |post|
@post.comments
end
# Total: 6 query
# 1 query cho post
# 5 query cho mỗi lần each
Đoạn code này sẽ tạo ra hai truy vấn SQL:
Một truy vấn để lấy tất cả các bài đăng, và một truy vấn cho mỗi bài đăng để lấy tất cả các bình luận của nó. Điều này có thể dẫn đến hiệu suất kém, đặc biệt nếu bạn có nhiều bài đăng hoặc bình luận.
Cách giải quyết N+1
1. Sử dụng includes hoặc eager_load
Phương thức includes hoặc eager_load cho phép bạn tải trước dữ liệu từ relation khi bạn truy cập từ một bảng.
Ví dụ:
@posts = Post.includes(:comments) # Total: 1 query
@posts = Post.eager_load(:comments) # Total: 1 query
2. Sử dụng preload
preload là một phương pháp giúp giảm thiểu vấn đề n+1 query trong Rails. Khi bạn sử dụng preload, Rails sẽ thực hiện hai truy vấn riêng biệt: một truy vấn để lấy tất cả các dữ liệu cần thiết và một truy vấn để lấy dữ liệu liên quan. Sau đó, Rails sẽ kết hợp dữ liệu từ hai truy vấn này mà không cần thực hiện thêm bất kỳ truy vấn nào nữa, giảm thiểu số lần truy vấn đến cơ sở dữ liệu.
Ví dụ:
@posts = Post.preload(:comments)
@posts = Post.preload(:comments).where(published: true)
@posts = Post.all
ActiveRecord::Associations::Preloader.new.preload(@posts, :comments)
3. Sử dụng joins
Sử dụng joins để kết hợp các bảng và truy vấn dữ liệu cùng một lúc. Điều này giúp tránh việc thực hiện nhiều truy vấn riêng lẻ.
Ví dụ:
@posts = Post.joins(:comments).select('posts.*, comments.body') # rails 6
@posts = Post.joins(:comments)
.select(posts: {}, comments: [:body]) # rails 7
4. Sử dụng pluck hoặc select
Thay vì lấy toàn bộ đối tượng, sử dụng pluck hoặc select để chỉ lấy những cột dữ liệu cần thiết.
Ví dụ:
# Sử dụng pluck
@post_ids = Post.where(created_at: 1.week.ago..).pluck(:id)
@comments = Post.joins(:comments)
.where(created_at: 1.week.ago..)
.pluck('comments.id', 'comments.body')
# Sử dụng select
@comments = Comment.select(:id, :body).where(id: @post_ids)
@posts = Post.joins(:comments).select('posts.*, comments.body') # rails 6
@posts = Post.joins(:comments)
.select(posts: {}, comments: [:body]) # rails 7
5. Sử dụng scope
Khi bạn muốn sử dụng preload với một phạm vi (scope) trong Rails, bạn có thể kết hợp preload với phạm vi đó để tối ưu hóa truy vấn dữ liệu của mình.
Ví dụ:
class Post < ActiveRecord::Base
has_many :comments
scope :published, -> { where(published: true) }
end
@posts = Post.eager_load(:comments).published
@posts = Post.published.preload(:comments)
@posts_pushlished = Post.published
ActiveRecord::Associations::Preloader.new.preload(
@posts_pushlished,
:comments
)
@posts = Post.published.includes(:comments)
@posts = Post.published.joins(:comments)
Kết luận
Bằng cách sử dụng các phương pháp như includes, eager_load, preload, joins, pluck, và select, bạn có thể giải quyết vấn đề N+1 query trong Rails một cách hiệu quả. Điều này không chỉ cải thiện hiệu suất ứng dụng của bạn mà còn giúp tối ưu hóa truy vấn cơ sở dữ liệu và tăng tốc độ tải trang.