Cải thiện performance với eager loading trong Rails
Bài đăng này đã không được cập nhật trong 8 năm
Tạo một Example
Trước hết hãy tạo nhanh một ứng dụng để làm việc.
Step1
Mở terminal của bạn ra và gõ câu lệnh để tạo một application với tên là blog
Rails new blog
cd blog
Step2
Sử dụng scaffold để tạo đầy đủ resful Author và Post
Rails g scaffold Author name
Rails g scaffold Post title body:text author:references
Step3
Chạy migrate và khởi động server
Rake db:migrate
Rails s
Và như vậy chúng ta có hai model Post và Author như bạn đã thấy. Post thì thuộc về tác giả, và tác giả có nhiều post. Đây là quan hệ cơ bản giữa hai model và chúng ta sẽ làm việc với chúng.
class Post < ActiveRecord::Base
belongs_to :author
end
class Author < ActiveRecord::Base
has_many :posts
end
mở posts_controller hãy thấy hàm index ở đây chúng ta lấy toàn bộ bài viết trong cơ sở dữ liệu và sắp xếp theo thời gian mới nhất.
def index
_@posts = Post.ordercreated_at: :desc
end
tương ứng với action index trong controller thì đó là phần view index.html.erb hãy để ý phần hiển thị ra toàn bộ bài viết có dòng show ra tên của tác giả
tương ứng với nó là phần view. index.html.erb nơi mà lặp mỗi bài viết(Post) nhận từ controller và gửi một câu query để lấy ra tác giả của mỗi bài viết đó post.author.name
<tbody>
<% @posts.each do |post| %>
<tr>
<td><%= post.title %></td>
<td><%= post.body %></td>
<td><%= post.author.name %></td>
</tr>
<% end %>
</tbody>
Trước tiên hãy vàoRails c
và tạo một vài dữ liệu mẫu cho application .
authors = Author.create([{ name: 'Hungnv' }, { name: 'Namnv' }, { name: 'Toandv' }])
Post.create(title: 'The gioi do day', body: '', author: Author.first)
Post.create(title: 'Tam quoc', body: '', author: Author.second)
Post.create(title: 'Nha gia Kim', body: '', author: Author.last)
Bây giờ thử truy cập vào đường link: http://localhost:3000/posts/new
và hãy để trong màn hình console các câu query sẽ thực thi như sau:
Post Load (0.6ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC
Author Load (0.5ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT 1 [["id", 3]]
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT 1 [["id", 2]]
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT 1 [["id", 1]]
Nếu có N author vậy thì để show ra toàn bộ bài viết thì cần N +1 query. Vậy hãy tưởng tượng nếu blog của chúng ta có một lượng bài viết lớn ?. 3000 bài viết thì cần 3000 +1 query và đây là một vấn đề. Cách giải quyết vấn đề trên
Để thoát khỏi tình huống trên Rails đã đưa cho chúng ta một tính năng đó là eager loading
Eager loading cho phép chúng ta load trước những dữ liệu association (Author) cho toàn bộ post. Điều này làm tăng hiệu năng tổng thể nhờ việc giảm số lượng query
Rails cung cấp 3 method để phục vụ cho việc eager loading đó là
preload()
eager_load()
includes()
mỗi hàm sẽ có những điểm khác biệt riêng. Hãy bắt đầu bằng cách thứ nhất preload() và sửa lại hàm index như dưới
def index
@posts = Post.order(created_at: :desc).preload :author
end
Và refresh lại trang show và xem trong màn hình console, bạn có thể thấy số lượng query đã thay đổi
SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC
SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (3, 2, 1)
Preload() sử dụng hai câu query riêng biệt, load dữ liệu chính(posts) và một câu query cho data association. Thực tế cách này hay hơn rất nhiều so với việc từng câu query mộ cho mỗi post(N + 1 queries).
Tuy nhiên preload() sẽ raise lên exception nếu chúng ta làm thêm các điều kiện liên quan đến data association ví dụ như:
- Sắp xếp theo thứ tự authors name
- Tìm kiếm bài viết của tác giả nào đó Do vậy chúng ta cần eager_load() và includes()
Hay thử các trường hợp trên với eager_load()
1. Xắp xếp bài viết theo tên tác giả
def index
@posts = Post.order("authors.name").eager_load :author
end
Bạn có thể thấy kết quả dưới màn hình console
SELECT "posts"."id" AS t0_r0, "posts"."title" AS t0_r1, "posts"."body" AS t0_r2, "posts"."author_id" AS t0_r3, "posts"."created_at" AS t0_r4, "posts"."updated_at" AS t0_r5, "authors"."id" AS t1_r0, "authors"."name" AS t1_r1, "authors"."created_at" AS t1_r2, "authors"."updated_at" AS t1_r3
FROM "posts"
LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
ORDER BY authors.name
2. Tìm các bài viết của một tác giả nào đó
def index
@posts = Post.order(created_at::desc).eager_load(:author).where("authors.name = ?", "Hungnv")
end
Và kết quả trong consle:
SELECT "posts"."id" AS t0_r0, "posts"."title" AS t0_r1, "posts"."body" AS t0_r2, "posts"."author_id" AS t0_r3, "posts"."created_at" AS t0_r4, "posts"."updated_at" AS t0_r5, "authors"."id" AS t1_r0, "authors"."name" AS t1_r1, "authors"."created_at" AS t1_r2, "authors"."updated_at" AS t1_r3
FROM "posts"
LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
WHERE (authors.name = 'Hungnv')
ORDER BY "posts"."created_at" DESC
3. Vấn đề N + 1 queries
def index
@posts = Post.order(created_at: :desc).eager_load(:author)
end
Và kết quả là
SELECT "posts"."id" AS t0_r0, "posts"."title" AS t0_r1, "posts"."body" AS t0_r2, "posts"."author_id" AS t0_r3, "posts"."created_at" AS t0_r4, "posts"."updated_at" AS t0_r5, "authors"."id" AS t1_r0, "authors"."name" AS t1_r1, "authors"."created_at" AS t1_r2, "authors"."updated_at" AS t1_r3
FROM "posts"
LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
ORDER BY "posts"."created_at" DESC
Trong cả ba kịch bản trên có hai điều chúng ta có thể thấy: Thứ nhất là eager_load() luôn luôn sử dụng left outer join trong mọi trường hợp và thứ hai là nó lấy toàn bộ data association trong duy nhất một câu query điều này sẽ khắc phục được exception của preload() trong các trường hợp như là filter hay order. Nhưng một câu query và left outer join có vẻ như phải trả cái giá hơi đắt cho nhưng kịch bản như tren vì thế Rails đã gửi cho chúng ta includes(), thông minh hơn hai hàm preload() và eager_load
Hay thử các trường hợp trên với includes()
1. Xắp xếp bài viết theo tên tác giả
def index
@posts = Post.order("authors.name").includes(:author)
end
kết quả trong màn hình console
SELECT "posts"."id" AS t0_r0, "posts"."title" AS t0_r1, "posts"."body" AS t0_r2, "posts"."author_id" AS t0_r3, "posts"."created_at" AS t0_r4, "posts"."updated_at" AS t0_r5, "authors"."id" AS t1_r0, "authors"."name" AS t1_r1, "authors"."created_at" AS t1_r2, "authors"."updated_at" AS t1_r3
FROM "posts"
LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
ORDER BY authors.name
2. Tìm kiếm các bài viết của một tac giả
def index
@posts = Post.order(created_at::desc).includes(:author).where "authors.name = ?", "Hungnv"
end
kết quả trong màn hình console
SELECT "posts"."id" AS t0_r0, "posts"."title" AS t0_r1, "posts"."body" AS t0_r2, "posts"."author_id" AS t0_r3, "posts"."created_at" AS t0_r4, "posts"."updated_at" AS t0_r5, "authors"."id" AS t1_r0, "authors"."name" AS t1_r1, "authors"."created_at" AS t1_r2, "authors"."updated_at" AS t1_r3
FROM "posts"
LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
WHERE (authors.name = 'Manish')
ORDER BY "posts"."created_at" DESC
3. Vấn đề N + 1 queries
def index
@posts = Post.order(created_at: :desc).includes :author
End
Và kết quả
SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC
SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (3, 2, 1)
Nếu so sánh với eager_load() thì kịch bản 1 và 2 cho ra kết quả giống nhau. Nhưng ở trường hợp cuối cùng thì nó giải quyết thông minh như preload(), tách làm hai query riêng và điều này mang lại hiệu năng cao hơn
Kết luận
Như vậy để tăng hiệu suất và giải quyết vấn đề N + 1 query trên Rails đã cung cấp eager loading. Trong đó có ba hàm preload()
,preload()
và includes()
. Bài viết trên cũng đã làm rõ sự khác nhau giữa 3 hàm trong một só các trường hợp.
Hy vọng bài viết sẽ giúp ích cho các bạn. Thank you
All rights reserved