1. Lý do viết bài viết

Đợt làm project của mình vừa rồi gặp lỗi khi sử dụng includes khi load màn hình gây ra lỗi timeout 504

2. Cách khắc phục mà mình đã sử dụng

@posts = Post.by_user_gender(@gender).newest.limit(Settings.home.limit_comment).includes :replies, :user, :watches, :category, :comments
↓
@posts = Post.by_user_gender(@gender).newest.limit Settings.home.limit_comment

Quá đơn giản, không cần phải sử dụng includes.
OK! nếu đơn giản chỉ là không sử dụng vậy còn gì để bàn nữa, nhưng cái mình sắp trình bày sau đây sẽ giúp các bạn hiểu rõ đôi chút về "thần chú" includes. Giúp các bạn hiều rõ hơn không phải lúc nào includes cũng tốt. Khi nào dùng và khi nào tránh dùng. 😘

3. Giới thiệu về includes

Cho phép chú ta hiện thực eager loading, các data cần sử dụng sẽ được pre-cached. Vì thề khi chúng ta sử dụng kết quả có tính chất iterating (lặp), sẽ giúp rút gọn số câu query cần thiết từ 1+x xuống còn 2 lần.

With includes, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries.

Trích: http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations

Xét qua 1 ví dụ nhỏ:

1  @companies = Company.includes(:persons).where(:persons => { active: true } ).all
2  
3  @companies.each do |company|
4       company.person.name
5  end

Nếu bạn bỏ không dùng includes ở đây, thì ở dòng code thứ 4, cứ mỗi lần .person thì sẽ thực hiện 1 cấu query xuống database để lấy. Vì thế nếu bạn có 100 cái companies thì sẽ thực hiện 100 lần gọi xuống DB. Điều đó làm giảm đi khả năng truy xuất kết nối của DB.
Nhưng hay nhớ bản chất của includes là

ensures that all of the specified associations are loaded using the minimum possible number of queries

Trích: http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
Nên đừng suy nghĩ là lúc nào cũng sẽ giúp trang web của bạn chạy nhanh hơn, includes chỉ đảm bảo số lượng query ít nhất cần thực hiện. 😁

4. Bàn xâu hơn về vấn đề

Nói xâu hơn 1 chút về câu query mình đã đề cập

@posts = Post.by_user_gender(@gender).newest.limit(Settings.home.limit_comment).includes :replies, :user, :watches, :category, :comments

Chạy thử bằng cách do thời gian access khi enter URL vào page

Các bạn có thể phóng to lên sẽ thấy phải mất gần 6s mới load page xong, quá chậm cho 1 lần access lại localhost (thực chất xử lý bị timeout ngay trên môi trường production)
Trong hình bạn có thể thấy có 2 câu query được thực hiện cho xử lý includes.
Câu query đầu thì hầu hết thời gian tốn vào việc LEFT OUTER JOIN để tìm ra có targets sẽ được trả về

Câu query thứ 2 là sau khi có các targets rồi, thì sẽ lấy thông tin của targets và các included records, câu này không tốn nhiều thời gian.
Kinh nghiệm rút ra là đừng có includes quá nhiều thông tin vào target.

Nói đến đây chắc sẽ có bạn nói: "Nếu không includes vào thì lúc load page, sẽ thực hiện nhiều câu query hơn, cũng tốn time vậy thôi"
Và đây là kết quả mình bỏ hẳn includes.

Page load chỉ mất khoảng có chưa đầy 1s, nhanh hơn gấp 6 lần. 🤤
Nếu bạn quan sát kỹ thì sau khi bỏ includes, chỉ có khoảng 4 query được thị hiện thêm, và mất chỉ khoảng 10ms cho 4 query này, nên việc gi phải includes để đánh đổi lại hơn 6s load page.

5. Kết luận nguyên nhân của việc slow query khi sử dụng include nếu ra ở bước 2.

  1. includes vào những thông tin mình không cần thiết.
  2. Số lượng iterating (lặp) không nhiều, chí có 2 lần (2 targets, 2 bài Post được lấy ra).
  3. Xử lý left join giữa các bảng tốn time hơn hẳn so với query trực tiếp lấy ra (đối với trường hợp trong bài viết này, còn 1 số trường hợp khác thì includes sẽ mang lại quá awesome)