N+1 xuất hiện khi ta load một tập record bằng một query, rồi mỗi record lại âm thầm tạo thêm query cho association.
1 | users = User.limit(100) |
Kịch bản có thể là:
1 | SELECT * FROM users LIMIT 100; -- 1 query |
Code nhìn sạch, log SQL nhìn như máy in hóa đơn.
preload: nhiều query có chủ đích
1 | User.preload(:articles) |
Rails thường chạy hai query: một cho users và một cho toàn bộ articles có user_id liên quan. Đây thường là lựa chọn dễ đoán khi không cần filter/order bằng bảng association.
eager_load: một LEFT OUTER JOIN
1 | User.eager_load(:articles) |
Rails dùng join và dựng object từ result. Có thể filter bảng liên quan, nhưng result SQL bị nhân dòng khi quan hệ one-to-many. Một query không mặc định rẻ hơn hai query; database không tính performance bằng số câu lệnh cho đẹp dashboard.
includes: Rails tự chọn chiến lược
1 | User.includes(:articles) |
Thông thường hoạt động giống preload. Khi query tham chiếu bảng association, Rails có thể chuyển sang join:
1 | User.includes(:articles) |
Với SQL fragment dạng string, có thể cần references(:articles).
joins không preload association
1 | User.joins(:articles) |
joins dùng association để filter/join SQL, nhưng không đồng nghĩa association đã được eager load. Sau đó gọi user.articles vẫn có thể tạo N+1.
Chọn bằng dữ liệu, không bằng tín ngưỡng
- Chỉ cần load association → bắt đầu với
preload/includes. - Cần filter bằng bảng liên quan →
joins,eager_load, hoặcincludesphù hợp. - Quan hệ lớn → cân nhắc pagination, select ít cột, aggregate hoặc query riêng.
- Dùng log,
strict_loading, Bullet trong development và APM ở production để phát hiện.
1 | User.strict_loading.includes(:articles) |
Fix N+1 không phải trò “giảm số query bằng mọi giá”. Mục tiêu là giảm tổng chi phí: round trip, số dòng, memory, object allocation và độ phức tạp SQL.