0

N+1 Query: Kẻ "Sát Thủ Thầm Lặng" Phá Hủy Hiệu Năng App Rails Của Bạn

Trong thế giới của Ruby on Rails, Active Record giống như một chiếc đũa phép thuật giúp bạn thao tác với Database cực kỳ dễ dàng. Nhưng hãy cẩn thận, vì "phép thuật" này có một tác dụng phụ rất tai hại nếu không được sử dụng đúng cách: Lỗi N+1 Query.

1. N+1 Query thực chất là gì?

Nói một cách dễ hiểu: Thay vì lấy tất cả dữ liệu cần thiết trong một lần đi chợ, bạn lại đi chợ N + 1 lần để mua từng món đồ một.

Giả sử bạn có 2 Model: Author (Tác giả) và Post (Bài viết). Một tác giả có nhiều bài viết.

Đoạn code "ngây thơ":

# Controller
@authors = Author.limit(10)

# View (index.html.erb)
<% @authors.each do |author| %>
  <p><%= author.name %> - Bài viết mới nhất: <%= author.posts.first.title %></p>
<% end %>

Chuyện gì xảy ra ở phía sau (Log SQL)?

  1. 1 Query để lấy ra 10 ông tác giả: SELECT * FROM authors LIMIT 10;
  2. 10 Query tiếp theo để lấy bài viết cho từng ông một:
  • SELECT * FROM posts WHERE author_id = 1 LIMIT 1;
  • SELECT * FROM posts WHERE author_id = 2 LIMIT 1;
  • ... (lặp lại đến hết 10 người)

Tổng cộng: queries. Nếu có 1000 tác giả? Bạn sẽ "nã" 1001 câu lệnh vào Database. Đó chính là thảm họa hiệu năng!


2. Giải pháp: Eager Loading (Nạp dữ liệu sớm)

Rails cung cấp cho chúng ta "vũ khí" để giải quyết việc này, đó là nạp sẵn các dữ liệu liên quan ngay từ câu lệnh đầu tiên.

Cách 1: Sử dụng .includes (Khuyên dùng)

Đây là cách phổ biến nhất. Rails sẽ tự động quyết định giữa Preload hoặc Eager Load để tối ưu nhất.

@authors = Author.includes(:posts).limit(10)

Bây giờ, log SQL của bạn sẽ chỉ còn 2 Query:

  1. SELECT * FROM authors LIMIT 10;
  2. SELECT * FROM posts WHERE author_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Cách 2: Sử dụng .joins

Dùng khi bạn chỉ muốn lọc (filter) dữ liệu dựa trên bảng liên quan chứ không thực sự cần lấy dữ liệu từ bảng đó ra dùng ở View.


3. Phân biệt: Preload, Eager_load và Includes

Nhiều bạn thường nhầm lẫn giữa 3 phương thức này. Hãy cùng so sánh nhanh:

Phương thức Cơ chế hoạt động Khi nào dùng?
Preload Luôn tách thành 2 query riêng biệt. Khi muốn nạp dữ liệu đơn giản.
Eager_load Dùng LEFT OUTER JOIN để lấy tất cả trong 1 query duy nhất. Khi bạn có các điều kiện WHERE phức tạp trên bảng liên quan.
Includes "Thông minh" nhất. Tự động chọn Preload hoặc Eager_load tùy trường hợp. Hầu hết mọi trường hợp.

4. Làm sao để phát hiện N+1 mà không cần "soi" Log?

Đừng ngồi đếm log SQL bằng mắt thường, hãy để công cụ làm việc đó thay bạn:

  1. Gems "Bullet": Đây là tiêu chuẩn vàng. Nó sẽ bắn ra một cái thông báo (alert) ngay trên trình duyệt hoặc ghi vào log khi nó phát hiện bạn đang có N+1 query.
  2. Rack Mini Profiler: Hiển thị thời gian thực thi của từng query ngay trên góc màn hình ứng dụng.
  3. Goldiloader: Một gem thú vị giúp tự động hóa việc eager loading mà bạn không cần phải viết .includes.

5. Kết luận

N+1 Query giống như một "vết rò rỉ" nhỏ trên con tàu của bạn. Khi ứng dụng còn ít dữ liệu, bạn sẽ không thấy vấn đề gì. Nhưng khi dữ liệu lớn dần, ứng dụng sẽ chạy chậm như rùa.

Quy tắc vàng: Luôn luôn nhìn vào số lượng SQL queries được thực thi trong một request. Nếu thấy các câu lệnh giống hệt nhau lặp đi lặp lại Chắc chắn là N+1.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí