N + 1: When More Queries Is a Good Thing
Trong tuần qua tôi đã cố gắng để hiểu làm thế nào để eager loading works in Rails để loại bỏ các vấn đề về N+1 query bằng cách làm giảm số lượng các queries fired. Giả thuyết ban đầu của tôi là giảm số lượng truy vấn càng nhiều càng tốt như là mục tiêu. Tuy nhiên, tôi đã rất ngạc nhiên bởi những gì ...
Trong tuần qua tôi đã cố gắng để hiểu làm thế nào để eager loading works in Rails để loại bỏ các vấn đề về N+1 query bằng cách làm giảm số lượng các queries fired. Giả thuyết ban đầu của tôi là giảm số lượng truy vấn càng nhiều càng tốt như là mục tiêu. Tuy nhiên, tôi đã rất ngạc nhiên bởi những gì tôi phát hiện ra.
1. Using includes to Reduce Queries
Hầu hết các bài đăng về vấn đề N+1, vấn đề dẫn đến phương thức includes để giải quyết vấn đề. includes được sử dụng để tải các quan hệ liên quan đến model bằng cách sử dụng tối thiểu các queries có thể. Bên dưới nó sử dụng một preload hoặc left outer join, tùy thuộc vào vị trí.
2. When and How to Use includes?
Giả sử rằng user của chúng ta có thể có nhiều post và có thể comment trên bất cứ post nào. Mỗi post có thể có nhiều comments. Cấu trúc cơ bản được hiển thị như dưới:
# models/users.rb class User < ApplicationRecord has_many :posts has_many :comments end # models/posts.rb class Post < ApplicationRecord has_many :comments belongs_to :user end # models/comments.rb class Comment < ApplicationRecord belongs_to :user belongs_to :post end
Bây giờ, nếu chúng tôi muốn thông tin của user với cá bài viết của user cùng với các comment của họ, đơn giản chỉ cần gọi User.all trước tiên load các user, sau đó nó sẽ lấy các post của mỗi user. Sau khi lấy các post, nó sẽ lấy các comment của user cho các post. Nếu chúng ta có 10 user, mỗi user có 5 post và trung bình 2 comment trong mỗi post, mỗi User.all sẽ có 1+5+10 queries.
# users_controller.rb def index @users = User.all render json: @users end
Một giải pháp đơn giản là sử includes để cho Active Record biết rằng chúng ta muốn lấy các user và tất cả các post có liên quan.
@users = User.all.includes(:posts)
Nhìn vào ảnh trên các bạn đã thấy rằng các post đã được load nhưng comment thì chưa.
Điều này cải thiệt hiệu năng một chút, vì nó lấy các user đầu tiên và sau đó trong truy vấn tiếp theo nó lấy các post liên quan đến user đó. Bây giờ, từ 1+5+10 queries đã giảm xuống còn 1+1+10 queries. Nhưng điều này sẽ tốt hơn rất nhiều nếu các comment liên quan đến các post cũng được nạp trước. Điều này sẽ làm giảm xuống còn 1+1+1 queries, tổng số còn là 3 queries để lấy tất cả các dữ liệu, chúng ta có thể viết như sau:
# users_controller.rb def index @users = User.all.includes(posts: [:comments]) render json: @users end
Nhìn vào ảnh chúng ta thấy tất cả data đã được load chỉ với 3 queries, một cho các user, một cho các post và một cho các comment liên quan đến các post.
Các comment trong một mảng sẽ cho biết các record để load trước các comment liên quan đến các post. Nếu một vài quan hệ với comment cần load trước, thì chúng ta có thể thay đổi tham số truyền vào phương thức includes, ví dụ:
User.all.includes(posts: [comments: [:another_relationship]])
Bằng cách này, bất kỳ mối quan hệ nested nào cũng có thể được tải trước.
3.Fetching Posts with a Specific Title
User.all.includes(posts: [:comments]).where('posts.title = ?', some_title)
Đoạn code trên sẽ rails một error, trong khi:
User.all.includes(posts: [:comments]).where(posts: { title: some_title })
sẽ cho chúng ta kết quả mong muốn. Điều này xảy ra khi các hash conditions đã đc pass, một left outer join của các user và post được thức hiện để lấy các user với các post có title cụ thể.
Nhưng, nếu chúng ta muốn sử dụng một điều kiện chuỗi hoặc mảng thay vì các hash conditions để xác định điều kiện về quan hệ bao gồm? Xem ví dụ sau:
User.all.includes(posts: [:comments]).where('posts.title = ?', some_title).references(:posts)
Chú ý phần references(:posts), references báo với includes để join post quan hệ với left outer join. Để hiểu nó, hãy xem query dưới đây:
Chúng ta đã giảm số lượng truy vấn từ 1 + 5 + 10 thành 1 truy vấn. Thật tuyệt!
4. But, Less is NOT Always More
Xem hai truy vấn ví dụ cuối. Cả hai đều dài từ 3 đến 4 dòng và có các chuỗi con như t0_r1, t0_r2, ..., t2_r5. Điều này có vẻ không bình thường. Tôi không phải là chuyên gia về SQL và không biết điều này có ý nghĩa gì. Đây được gọi là CROSS JOIN hoặc CARTESIAN join.
Vì vậy, sử dụng các tham chiếu hoặc hash condition để chỉ các điều kiện cho các quan hệ bao hàm có thể gây ra các truy vấn rất dài và các kết nối bên ngoài không cần thiết, có thể gây ảnh hưởng xấu đến hiệu năng và bộ nhớ. Thay vào đó, việc tách một truy vấn lớn thành một vài truy vấn sẽ mang lại hiệu quả hơn.
Một cách tốt hơn để xác định các điều kiện trên các liên kết phức tạp:
User.all.joins(:posts).where('posts.title = ? ', some_title).includes(posts: [:comments])
Nó sẽ sinh ra 1+1+1 queries và chỉ load các user có các post phù hợp với điều kiện đã cho, như là title đặc biệt hoặc ...
Tham khảo https://www.sitepoint.com/n-1-when-more-queries-is-a-good-thing/