+2

Cải thiện performance với eager loading trong Rails

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ư:

  1. Sắp xếp theo thứ tự authors name
  2. 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()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

Viblo
Let's register a Viblo Account to get more interesting posts.