1. N+1 Query Problem là gì?

N+1 Query Problem là một vấn đề về hiệu suất thường gặp khi làm việc với cơ sở dữ liệu, đặc biệt trong các ứng dụng sử dụng ORM (Object-Relational Mapping) như Hibernate, Entity Framework, Django ORM hoặc các thư viện tương tự.

Theo mình tìm hiểu, N+1 Query Problem xảy ra khi chương trình thực hiện một truy vấn để lấy danh sách dữ liệu, sau đó lại tiếp tục thực hiện thêm nhiều truy vấn riêng biệt để lấy dữ liệu liên quan cho từng bản ghi trong danh sách đó.

Trong trường hợp này, nếu truy vấn đầu tiên lấy được N bản ghi thì chương trình có thể thực hiện tổng cộng N + 1 truy vấn. Khi số lượng bản ghi tăng lên, số lần truy vấn database cũng tăng theo, khiến thời gian xử lý lâu hơn và làm tăng tải cho hệ thống.

Ví dụ, một trang web cần hiển thị danh sách 100 bài viết cùng tên tác giả của từng bài. Chương trình có thể lấy toàn bộ 100 bài viết bằng một truy vấn, sau đó thực hiện thêm 100 truy vấn để lấy thông tin tác giả cho từng bài. Tổng cộng có 101 truy vấn, mặc dù dữ liệu có thể được lấy bằng ít truy vấn hơn.

How to Avoid the N+1 Query Problem With Eager Loading | Prakash's Blog 👨‍💻

2. Nguyên nhân gây ra N+1 Query Problem

Nguyên nhân phổ biến của vấn đề này là cách chương trình truy xuất dữ liệu liên quan giữa các bảng chưa hợp lý. Trong một ứng dụng, dữ liệu thường được chia thành nhiều bảng để dễ quản lý, chẳng hạn như bảng users, posts, products hoặc orders.

Khi lấy dữ liệu từ một bảng, chương trình có thể cần thêm thông tin từ bảng khác. Nếu mỗi lần truy cập thông tin liên quan đều khiến ORM gửi một truy vấn riêng đến database, số lượng truy vấn sẽ tăng lên theo số bản ghi ban đầu.

Một nguyên nhân thường gặp khác là cơ chế Lazy Loading. Đây là cách tải dữ liệu mà thông tin liên quan chỉ được truy xuất khi chương trình thực sự cần sử dụng đến. Cách này có thể giúp tránh tải dữ liệu không cần thiết, nhưng nếu chương trình lặp qua nhiều bản ghi và truy cập dữ liệu liên quan của từng bản ghi, nó có thể tạo ra N+1 Query Problem.

Ngoài ra, vấn đề này đôi khi xuất hiện do người lập trình chưa kiểm tra số lượng truy vấn thực tế. Khi dữ liệu thử nghiệm còn ít, chương trình vẫn có thể chạy nhanh. Nhưng khi đưa vào môi trường thực tế với hàng nghìn bản ghi và nhiều người dùng cùng truy cập, hiệu suất có thể giảm đáng kể.

3. Ví dụ về N+1 Query Problem

Giả sử một ứng dụng quản lý bài viết có hai bảng là posts và users. Bảng posts lưu thông tin bài viết, trong đó mỗi bài viết có user_id để xác định tác giả. Bảng users lưu thông tin người dùng.

Nếu cần lấy danh sách bài viết và tên tác giả, chương trình có thể thực hiện truy vấn đầu tiên để lấy tất cả bài viết:

SELECT *
FROM posts;

Sau đó, với mỗi bài viết, chương trình tiếp tục lấy thông tin tác giả bằng một truy vấn riêng:

SELECT *
FROM users
WHERE id = 1;

Nếu có 100 bài viết và chương trình thực hiện truy vấn lấy tác giả một lần cho mỗi bài, tổng số truy vấn có thể lên đến 101. Nếu nhiều bài viết cùng một tác giả, chương trình vẫn có thể truy vấn thông tin tác giả lặp lại nếu không có cơ chế tối ưu phù hợp.

Điều đáng chú ý là vấn đề không nằm ở việc câu SQL riêng lẻ quá phức tạp, mà nằm ở số lượng truy vấn được thực hiện quá nhiều so với nhu cầu lấy dữ liệu.

4. N+1 Query Problem ảnh hưởng như thế nào?

Ảnh hưởng đầu tiên là làm tăng thời gian phản hồi của ứng dụng. Mỗi truy vấn đều có chi phí gửi yêu cầu đến database, xử lý câu lệnh và trả kết quả về ứng dụng. Khi phải thực hiện nhiều truy vấn liên tiếp, các chi phí này có thể cộng dồn và khiến trang web hoặc API phản hồi chậm hơn.

Thứ hai, N+1 Query Problem làm tăng tải cho database. Thay vì xử lý một vài truy vấn để lấy dữ liệu cần thiết, database phải tiếp nhận nhiều truy vấn nhỏ. Khi số lượng người dùng tăng lên, lượng truy vấn phát sinh có thể tăng đáng kể, gây ảnh hưởng đến những chức năng khác trong hệ thống.

Thứ ba, vấn đề này có thể làm giảm khả năng mở rộng của ứng dụng. Một chức năng chạy tốt với 10 bản ghi chưa chắc hoạt động hiệu quả khi có 10.000 bản ghi. Nếu không được phát hiện sớm, N+1 Query Problem có thể trở thành nguyên nhân gây nghẽn hệ thống khi ứng dụng phát triển.

Tuy nhiên, mức độ ảnh hưởng còn phụ thuộc vào số lượng bản ghi, độ trễ kết nối, cách database xử lý truy vấn và việc dữ liệu có được lưu trong bộ nhớ đệm hay không.

5. Cách khắc phục bằng JOIN

Một cách phổ biến để hạn chế N+1 Query Problem là sử dụng JOIN để lấy dữ liệu từ nhiều bảng trong cùng một truy vấn SQL. Thay vì lấy bài viết trước rồi truy vấn tác giả từng lần, mình có thể kết hợp hai bảng để lấy thông tin cần thiết cùng lúc.

Ví dụ:

SELECT
    posts.id,
    posts.title,
    users.name AS author_name
FROM posts
JOIN users
    ON posts.user_id = users.id;

Trong câu lệnh trên, database kết hợp bảng posts và users thông qua điều kiện posts.user_id = users.id. Kết quả trả về bao gồm thông tin bài viết và tên tác giả tương ứng.

Cách làm này giúp giảm số lần gửi truy vấn đến database. Tuy nhiên, cần chọn loại JOIN phù hợp với yêu cầu. Ví dụ, INNER JOIN chỉ trả về những bản ghi có dữ liệu khớp ở cả hai bảng, trong khi LEFT JOIN vẫn giữ lại các bản ghi bên trái dù không có dữ liệu tương ứng ở bảng bên phải.

Ngoài ra, khi lấy dữ liệu cho một trang danh sách lớn, vẫn cần chú ý đến phân trang và chỉ chọn những cột cần thiết để tránh truy xuất quá nhiều dữ liệu không sử dụng.

6. Khắc phục bằng Eager Loading và Batch Loading

Ngoài việc sử dụng JOIN, có thể giải quyết N+1 Query Problem bằng các kỹ thuật tải dữ liệu của ORM.

Eager Loading là cách tải dữ liệu liên quan ngay từ đầu, thay vì chờ đến khi chương trình truy cập từng bản ghi mới tải. Tùy vào ORM, cơ chế này có thể sử dụng JOIN hoặc nhiều truy vấn được tối ưu để lấy dữ liệu liên quan.

Ví dụ, trong một ứng dụng sử dụng Django ORM, nếu cần lấy danh sách bài viết cùng thông tin tác giả, có thể sử dụng select_related() đối với quan hệ phù hợp như khóa ngoại:

posts = Post.objects.select_related("author").all()

for post in posts:
    print(post.title, post.author.name)

Trong ví dụ này, Django có thể sử dụng một truy vấn kết hợp để lấy bài viết và tác giả, thay vì truy vấn tác giả riêng cho từng bài viết.

Batch Loading là cách gom nhiều yêu cầu lấy dữ liệu thành một nhóm, giúp giảm số lượng truy vấn. Chẳng hạn, chương trình có thể lấy danh sách bài viết trước, sau đó lấy thông tin của tất cả tác giả liên quan bằng một truy vấn sử dụng IN.

SELECT *
FROM users
WHERE id IN (1, 2, 3, 4, 5);

Cách này hữu ích khi cần lấy nhiều bản ghi liên quan mà không muốn tạo một truy vấn cho từng bản ghi. Trong thực tế, lựa chọn giữa JOIN, Eager Loading và Batch Loading cần dựa vào cấu trúc dữ liệu, ORM đang sử dụng và loại truy vấn.

7. Cách phát hiện N+1 Query Problem

Để phát hiện vấn đề này, mình cần kiểm tra số lượng truy vấn mà ứng dụng thực hiện trong quá trình xử lý một yêu cầu. Nếu một API lấy danh sách 100 bài viết nhưng phát sinh hàng chục hoặc hàng trăm truy vấn để lấy thông tin tác giả, đó có thể là dấu hiệu của N+1 Query Problem.

Có thể sử dụng các công cụ ghi log SQL của ORM để xem những câu truy vấn được gửi đến database. Một số framework còn có công cụ theo dõi truy vấn, hiển thị thời gian thực thi và cảnh báo khi cùng một loại truy vấn được lặp lại quá nhiều lần.

Ngoài ra, có thể kiểm tra truy vấn bằng EXPLAIN để hiểu cách database thực thi một câu SQL cụ thể. Tuy nhiên, EXPLAIN chủ yếu giúp phân tích kế hoạch thực thi của truy vấn; để phát hiện N+1, mình thường cần kết hợp thêm log truy vấn và theo dõi toàn bộ quá trình xử lý của ứng dụng.

Khi kiểm tra hiệu suất, nên thử với lượng dữ liệu gần với môi trường thực tế. Nếu chỉ thử với vài bản ghi, số lượng truy vấn tăng lên có thể chưa tạo ra sự khác biệt rõ ràng về thời gian phản hồi.

8. Kết luận

Theo mình tìm hiểu, N+1 Query Problem là một lỗi tối ưu hiệu suất thường gặp khi ứng dụng truy xuất dữ liệu từ database, đặc biệt khi sử dụng ORM và các quan hệ giữa nhiều bảng. Vấn đề xảy ra khi chương trình thực hiện một truy vấn lấy danh sách dữ liệu rồi tiếp tục thực hiện thêm nhiều truy vấn riêng lẻ cho từng bản ghi.

Để khắc phục, mình có thể sử dụng JOIN, Eager Loading hoặc Batch Loading tùy theo yêu cầu. Đồng thời, cần kiểm tra log SQL, theo dõi số lượng truy vấn và thử nghiệm với dữ liệu đủ lớn để phát hiện những vấn đề có thể không xuất hiện trong giai đoạn phát triển ban đầu.

Hiểu rõ N+1 Query Problem giúp mình viết các truy vấn hiệu quả hơn, giảm tải cho database và cải thiện tốc độ phản hồi của ứng dụng. Đây cũng là một kiến thức quan trọng khi phát triển API, làm việc với ORM và tối ưu các hệ thống có lượng dữ liệu lớn.

Bài viết khác

Message Broker

1. Message Broker là gì? Message Broker là một thành phần trung gian được sử dụng để tiếp nhận, quản lý và chuyển tiếp thông điệp giữa các ứng dụng hoặc các thành phần trong một hệ thống phần mềm. Công nghệ này thường xuất hiện trong những hệ thống Backend, kiến trúc Microservices và […]

Background Jobs and Queues

1. Background Jobs and Queues là gì? Background Jobs & Queues là hai khái niệm thường gặp trong quá trình phát triển phần mềm, đặc biệt là khi xây dựng các ứng dụng web, ứng dụng di động hoặc những hệ thống cần xử lý nhiều yêu cầu cùng lúc. Hai khái niệm này liên […]

Redis and Caching

1. Caching là gì? Caching là kỹ thuật lưu trữ tạm thời những dữ liệu thường xuyên được sử dụng để có thể truy xuất lại nhanh chóng khi cần thiết. Thay vì mỗi lần người dùng thực hiện một thao tác, hệ thống đều phải truy vấn cơ sở dữ liệu hoặc xử lý […]

Database Migration

1. Database Migration là gì? Database Migration là quá trình quản lý và cập nhật cấu trúc cơ sở dữ liệu theo từng phiên bản trong quá trình phát triển phần mềm. Khi xây dựng một ứng dụng, cơ sở dữ liệu thường xuyên thay đổi để đáp ứng các yêu cầu mới. Ví dụ, […]

Bun ORM

1. Bun ORM là gì? Bun ORM là một thư viện ORM dành cho ngôn ngữ lập trình Go (Golang), được sử dụng để làm việc với cơ sở dữ liệu quan hệ. Bun giúp lập trình viên xây dựng và thực hiện các truy vấn database thông qua những cấu trúc dữ liệu và […]

Date / Number / Currency Formatting

       Trong quá trình phát triển phần mềm, đặc biệt là các ứng dụng web và di động phục vụ người dùng ở nhiều quốc gia, việc hiển thị dữ liệu theo đúng định dạng ngày tháng, số và tiền tệ đóng vai trò vô cùng quan trọng. Người dùng không chỉ quan […]

Leave a Reply

Your email address will not be published. Required fields are marked *