Cục Nợ N + 1 Và Hướng Xử Lý
Đặt vấn đề
Có thể bạn đã từng nghe qua về N+1, đây là một chủ để cơ bản trong vấn đề quản lý csdl trong code, bài viết này sẽ trình bày về nó dưới góc độ sử dụng trong ruby on rails với framework ActiveRecord
Luận bàn
N+1 query
Xét một ví dụ:
Tôi có một ứng dụng đơn giản, với hai bảng csdl, bảng thứ nhất là bảng users, bảng thứ hai là bảng articles, một user có thể có nhiều articles nhưng một article chỉ thuộc về user. Hãy xem đoạn mã dưới đây:
1 | User.all.each do |user| |
Đoạn code trên sẽ duyệt qua tất cả user có trong csdl rồi lấy tất cả các articles ứng với từng user. Rất tường minh và dễ hiểu.
Tối có 5 user mẫu trong csdl nên các câu lệnh sql sinh ra sau đoạn code trên như sau:
1 | # câu lệnh lấy hết users lên |
Đây chính là ví dụ điển hình về N+1
, N
ở đây là 5 (số lượng user có trong csdl) và 1
chính là câu lệnh sql đầu tiên dùng để lấy tất cả user lên.
Nhìn như vô hại, mà thực ra ở ví dụ này cũng vô hại thật vì chỉ có 5 record user, nên thời gian load cũng không thấm tháp gì, tuy nhiên trong thực tế thì dữ liệu có thể rất lớn, lên đến hàng triệu record và nếu load dữ liệu như này thì đây là một vấn đề lớn cho hiệu năng của trang web. Hãy đọc tiếp mục 2 để tìm về giải pháp cho tình huống này.
2. Giải pháp
Vấn đề đã có, giờ chúng ta cần tìm một giải pháp làm sao để kết quả trả về không đổi, nhưng lượng truy vấn sql trong csdl phải nhỏ hơn.
Các phương án:
Sử dụng
select in()
.Sử dụng
joins
. Để đọc kỹ hơn về joins, thì nên xem ở đây
Sử dụng select in() sẽ tiết kiệm truy vấn đi rất nhiều, nếu ví dụ bên trên ta sử dụng select in() thì các sql query cần thiết sẽ là như sau:
1 | # lấy tất cả query |
Tuyệt vời, từ 6(N + 1) truy vấn giờ đã trở thành còn 2.
Với cách thứ 2 là sử dụng joins
:
joins hiểu đơn giản là ghép hai bảng lại với nhau (lấy một số field cần thiết ở bảng này, gộp với vài field cần thiết ở bảng kia khi mà record ở hai bên thoả mãn một điều kiện nào đó), rồi từ đó thành một bảng tạm dùng trong quá trình bạn sử dụng.
Quay trở về ví dụ ban đầu, câu truy vấn bây giờ sẽ trở thành:
1 | SELECT User.name, Article.title |
Lấy vài field cần thiết(name của user và title của Article).
Bằng cách thoả mãn điều kiện nào đó(id của user bằng với user_id của article).
Vậy từ N+1 query ban đầu, đã trở thành một query duy nhất.
Đến lúc này đã có thể kết luận là joins tốt hơn select in() được chưa? Chưa, câu trả lời sẽ là như vậy. Sang mục 3 chúng ta sẽ tìm hiểu về cách xử lý N+1 thông qua ActiveRecord
3. Xử lý N+1 query trong ActiveRecord
Trong ActiveRecord cung cấp 3 phương thức để loại bỏ N+1,
1) Sử dụng preload
1 | User.preload(:articles) |
Preload sẽ luôn sử dụng select in()
2) Sử dụng Eagerload
1 | User.eager_load(:articles) |
eager_load luôn sử dụng joins
3) Sử dụng Inludes
1 | # cách 1 |
Vậy mặc định thì includes
sử dụng select in(), nhưng cũng có thể chuyển qua sử dụng joins nếu thêm method references phía sau.
Kết luận
Qua ví dụ trên, ta đã tìm hiểu được một số cách cơ bản để loại bỏ N+1, với việc sử dụng joins sẽ ít query phải thực thi nhất, nhưng điều này không đảm bảo rằng sử dụng joins là tối ưu, vì với mỗi query sql, thời gian và công sức máy tính phải dùng là khác nhau, không phải câu sql nào cũng có thời gian thực hiện ngang nhau.
Sử dụng joins trong nhiều trường hợp csdl quá lớn cũng không phải là một ý hay, khi bản chất của joins là duyệt tuần tự qua hai (nhiều) bảng cần joins, để tìm những phần tử phù hợp rồi cho vào một bảng tạm. Hai vấn đề thấy ngay là việc phải duyệt tuần tự hai hay nhiều bảng là một truy vấn tốn tài nguyên, thứ hai dữ liệu được bỏ vào mảng tạm hiển nhiên ta cần một vùng nhớ để lưu cái bảng tạm này.
Để đi sâu hơn về vấn đề hiệu năng, các bài tiếp sau chúng ta sẽ thảo luận về nó.