Làm chậm hệ thống với N+1 queries

Làm chậm hệ thống với N+1 queries

Dòng đời vội vã lại 1 năm trôi qua, nhớ lại những lần deadline đẩy đưa khiến mình phải code cho lẹ nhưng ngờ đâu lại vô tình gây ra những ảnh hưởng ít nhiều đến chất lượng của cả hệ thống về sau. Một trong những vấn đề mình từng mắc phải đã kéo ì performance của api chính là N+1 queries.

1. Tại sao là N+1 ?

Lấy ví dụ mình muốn lấy danh sách các bài posts và comment mỗi bài post theo userId.

dbdiagram.png

Theo thói quen, mình sẽ viết code xử lý như sau:

var result []*Data
posts, err := db.postRepo.GetPostsByUserId(userId)
for _, post := range posts {
     data := &Data{}
     comment, err := db.commentRepo.GetCommentByPostId(post.Id)
     data.Post = post
     data.Comment = comment  
}

Tốn 1 query để lấy danh sách bài post theo userId:

SELECT * FROM posts WHERE user_id = ...
// Result: posts.id  [1, 2, 3, ... n]

Tốn N query để lấy comment tương ứng bài post:

SELECT * FROM comments WHERE post_id = 1
SELECT * FROM comments WHERE post_id = 2
SELECT * FROM comments WHERE post_id = 3
...
SELECT * FROM comments WHERE post_id = n

Như vậy, để lấy được một list kết quả thỏa yêu cầu trên, mình đã phải tốn N+1 câu queries. Tính toán sơ nếu 1 câu query mất tầm 2 - 3ms, lỡ xui N là 10k hay 100k thì cày nát database, thời gian tăng tuyến tính như O(n), hệ thống thì chậm và không mang đến trải nghiệm tốt cho người dùng.

2. Giải pháp ?

Lụm lặt từ các anh đi trước và kinh nghiệm làm sai trong quá khứ, mình có 2 hướng khắc phục cho bài toán N+1, sử dụng: WHERE..IN hoặc JOINS

  • Where .. in
var result []*Data
posts, _ := db.postRepo.GetPostsByUserId(userId)
postIds := getPostIds(posts)
comments, err := db.commentRepo.GetCommentsByPostIds(postIds)

Tương tự, mình cũng tốn 1 query để lấy danh sách bài post theo userId. Nhưng mình sử dụng Where .. In để lấy danh sách comments mà chỉ tốn thêm 1 query thôi

SELECT * FROM posts WHERE user_id = ...
SELECT * FROM comments WHERE post_id in (1, 2, 3, ... n)
  • Joins

Where .. in là giải pháp đơn giản và hiệu quả có thể khắc phục được N+1, ngoài ra còn 1 chiêu thức lợi hại khác cũng tận dụng sự hỗ trợ của database chính là dùng Join để chắt lọc và lấy dữ liệu như yêu cầu mà chỉ cần tốn duy nhất 1 câu query.

SELECT * FROM posts 
JOIN comments on comments.post_id = posts.id
WHERE posts.user_id = ...

Đến đây hy vọng phần kiến thức nhỏ của mình về vấn đề N+1 có thể chia sẻ với mọi người về cách cải thiện hệ thống cũng như giảm bớt phần việc cho database. Ngoài ra, một tip giúp mình có thể phát hiện lỗi này ở codebase chính là khi refactor mình sẽ lưu ý các vòng loop, thông thường sẽ rất dễ đặt các câu query để lấy từng item như ví dụ đầu bài mình từng viết.

Bài viết chắc chắn còn nhiều thiếu sót, mình hi vọng được mọi người đóng góp và chia sẻ để mình có thể học hỏi và cải thiện. Xin chân thành cảm ơn mọi người đã dành thời gian đọc bài viết của mình. Nếu mọi người có thêm những giải pháp nào hay thì comment vào bên dưới chia sẻ với mình nhé.

Nguồn tham khảo