N+1 query sử dụng gem Dase và Includes Count
Bài đăng này đã không được cập nhật trong 7 năm
Trong Rails đã hỗ trợ một method includes dùng để hổ trợ việc giảm N+1 query trong truy vấn cơ sở dữ liệu quan hệ. Mình có table categories has_many với table posts và mình muốn lấy list categories và count các bài post tương ứng của category đó thì mình làm như sau:
# Controller
class CategoryController < ApplicationController
def index
categories = Category.all
end
end
# View
<% @categories.each do |category| %>
<h1><%= category.name %>(<%= category.posts.count %>)<h1>
<% end %>
Nhìn vào console bạn sẽ thấy như sau:
Category Load (0.1ms) SELECT categories.* FROM categories
(0.2ms) SELECT COUNT() FROM posts WHERE posts.category_id = 2
(0.3ms) SELECT COUNT() FROM posts WHERE posts.category_id = 3
(0.2ms) SELECT COUNT(*) FROM posts WHERE posts.category_id = 4
3 câu lệnh SELECT count được thực hiện. Điều này sẽ ảnh hưởng đến tốc độ truy vấn dữ liệu khi hệ thống dữ liệu lớn. Để giải quyết vấn đề này mình xin hướng dãn 2 cách
1. Tạo một câu query
Mình tạo một cái scope với dòng query như sau:
# Model
class Category < ActiveRecord::Base
has_many :posts
scope :with_count_posts, -> {joins(:posts).select("categories.* ,Count(posts.id) AS posts_count").group("categories.id")}
end
# Controller
class CategoryController < ApplicationController
def index
@categories = Category.with_count_posts
end
end
# View
<% @categories.each do |category| %>
<h1><%= category.name %>(<%= category.posts_count %>)<h1>
<% end %>
Nhìn vào console bạn sẽ thấy sự khác biệt
Category Load (0.1ms) SELECT categories.* ,Count(posts.id) AS posts_count FROM categories INNER JOIN posts ON posts.category_id = categories.id GROUP BY categories.id
Nó chỉ cần chạy một truy vấn sơ với ban đầu. Dòng trên có ý nghĩa là khi khi mình lấy hết dữ của bảng categories mình sẽ tạo thêm 1 field là posts_count để chứa toàn bộ tổng số những record posts tương ứng với category đó.
Lưu Ý
scope ở trên sẽ không lấy ra những category không có record posts nào. Nếu muốn lấy hết thì bạn có thể tạo một scope như dưới đây.
scope :count_posts, -> {joins("LEFT JOIN posts ON categories.id = posts.category_id").select("categories.* ,Count(posts.id) AS posts_count").group("categories.id")}
2. Dùng Gem
Ở đây mình xin giới thiệu 2 gem là includes-count và dase
2.1 Gem Includes Count
Gem này gọi một phương thức includes_count để truy vấn các bản ghi đang hoạt động, hỗ trợ count các association với các quan hệ bằng cách sử dụng một truy vấn SQL SELECT đơn giản theo một cách tương tự như includes method
2.1.1 Usage
VD: Ta có model
class Blog
has_many :posts
has_many :comments, :through => :posts
end
class Post
belongs_to :blog
has_many :comments
end
class Comment
belongs_to :post
end
Ta có thể lấy các post ra từ blog với lệnh
blogs_with_posts_count = Blog.scoped.includes_count(:posts)
Điều này sẽ thực hiện một câu truy vấn SELECT đơn giản để lấy ra và gán chúng trong memory, do đó không yêu cầu INNER JOIN xử lý trong cơ sở dữ liệu: Ta có Select
SELECT SQL_NO_CACHE posts.blog_id, COUNT(id) AS posts_count
FROM `posts`
WHERE `posts`.`blog_id` IN (1, 2, 3, 4, 5, 6, 7, 8)
GROUP BY `posts`.`blog_id`
2.2 Gem Dase
Gem dase cung cấp phương thức includes_count_of trên một mối quan hệ hoạt động tương tự như ActiveRecord's preload method và giải quyết vấn đề truy vấn N + 1 khi đếm các bản ghi trong has_many ActiveRecord associations.
2.2.1 Usage
Ta có db:
class Author
has_many :articles
end
Bạn có thể thực hiện:
authors = Author.includes_count_of(:articles)
billy = authors.first # => #<Author name: 'Billy'>
billy.articles_count # => 2
lọc các articles trong năm 2012
Author.includes_count_of(:articles, where: {year: 2012} )
Sử dụng cú pháp lambda (trong v4.1 trở lên):
Author.includes_count_of(:articles, -> { where(year: 2012) } )
Với nhiều associations được counted cùng 1 lúc: Author.includes_count_of(:articles, :photos, :tweets)
All rights reserved