1. 00:40 06/08/2026
Khi ứng dụng còn ít dữ liệu, trang danh sách thường được viết rất đơn giản: lấy 20 bản ghi đầu tiên, bấm “trang sau” thì lấy tiếp 20 bản ghi nữa. Nhưng khi dữ liệu tăng lên, phân trang không còn là chuyện nhỏ. Nếu làm chưa đúng, người dùng có thể thấy bản ghi bị lặp, bị mất, hoặc trang tải ngày càng chậm.
Bài viết này chia sẻ một số cách phân trang phổ biến và kinh nghiệm chọn cách phù hợp trong dự án web/app.
1. Offset pagination: dễ hiểu nhưng có giới hạn
Cách quen thuộc nhất là dùng limit và offset. Ví dụ: trang 1 lấy 20 dòng đầu, trang 2 bỏ qua 20 dòng và lấy 20 dòng tiếp theo.
Ưu điểm:
- Dễ triển khai.
- Dễ hiển thị số trang: 1, 2, 3, 4...
- Phù hợp với màn hình quản trị, danh sách ít thay đổi, lượng dữ liệu vừa phải.
Nhược điểm:
- Offset càng lớn thì truy vấn có thể càng chậm, vì database vẫn phải “đi qua” nhiều dòng trước khi lấy kết quả cần trả về.
- Nếu dữ liệu thay đổi liên tục, người dùng có thể gặp bản ghi lặp hoặc bị nhảy mất. Ví dụ đang xem trang 2, trong lúc đó có bài viết mới được thêm vào đầu danh sách, thứ tự dữ liệu thay đổi và kết quả trang sau không còn như cũ.
Offset pagination vẫn dùng được, nhưng nên tránh lạm dụng cho các danh sách rất lớn hoặc feed cập nhật liên tục.
2. Cursor pagination: phù hợp cho feed và danh sách lớn
Thay vì truyền page=3, cursor pagination truyền một “mốc” để lấy dữ liệu tiếp theo. Ví dụ danh sách bài viết sắp xếp theo created_at giảm dần, API có thể trả về next_cursor là thời điểm hoặc id của bản ghi cuối cùng trong trang hiện tại. Lần sau client gửi cursor đó để lấy phần tiếp theo.
Ví dụ luồng đơn giản:
- Request đầu: lấy 20 bài mới nhất.
- Response trả về 20 bài và next_cursor tương ứng với bài cuối.
- Request sau: lấy 20 bài có created_at nhỏ hơn cursor.
Ưu điểm:
- Ổn định hơn khi dữ liệu mới được thêm vào.
- Thường hiệu quả hơn với bảng lớn nếu có index phù hợp.
- Rất hợp với “tải thêm”, infinite scroll, danh sách thông báo, lịch sử giao dịch, feed nội dung.
Nhược điểm:
- Khó nhảy trực tiếp tới trang số 10.
- Cần thiết kế cursor cẩn thận, nhất là khi có nhiều điều kiện sắp xếp.
3. Luôn sắp xếp theo thứ tự xác định
Một lỗi khá phổ biến là phân trang nhưng chỉ sort theo một cột không đủ duy nhất, ví dụ ORDER BY created_at DESC. Nếu nhiều bản ghi có cùng created_at, thứ tự giữa chúng có thể không ổn định.
Nên thêm một khóa phụ, thường là id:
- ORDER BY created_at DESC, id DESC
Khi dùng cursor, cũng nên lưu cả created_at và id để truy vấn trang tiếp theo chính xác hơn. Điều này giúp tránh trường hợp bản ghi bị lặp hoặc mất khi nhiều dữ liệu có cùng thời gian tạo.
4. Đừng quên index
Phân trang tốt không chỉ nằm ở API mà còn ở database. Nếu danh sách thường lọc theo user_id và sắp xếp theo created_at, hãy cân nhắc index phù hợp, ví dụ tổ hợp user_id, created_at, id tùy hệ quản trị cơ sở dữ liệu và truy vấn thực tế.
Không nên tạo index theo cảm tính quá nhiều, vì index cũng làm tăng chi phí ghi dữ liệu. Cách tốt hơn là xem các truy vấn thường dùng, kiểm tra kế hoạch thực thi, rồi tối ưu những nơi thật sự cần.
5. Response API nên rõ ràng
Một response phân trang dễ dùng có thể gồm:
- data: danh sách bản ghi.
- next_cursor: cursor cho lần tải tiếp theo, null nếu hết dữ liệu.
- has_more: còn dữ liệu hay không.
Với offset pagination, có thể trả thêm page, per_page, total nếu cần hiển thị tổng số trang. Tuy nhiên, total trên bảng lớn đôi khi cũng tốn kém, nên hãy cân nhắc có thật sự cần hiển thị “tổng cộng 1.234.567 kết quả” hay không.
6. Chọn cách nào trong thực tế?
Một số gợi ý nhanh:
- Trang quản trị cần nhảy tới trang bất kỳ: offset pagination thường đủ tốt.
- Feed bài viết, thông báo, chat, lịch sử hoạt động: ưu tiên cursor pagination.
- Danh sách nhỏ, ít thay đổi: không cần làm phức tạp.
- Dữ liệu lớn, truy cập nhiều: thiết kế phân trang cùng với index ngay từ đầu.
Kết luận
Phân trang không chỉ là chia dữ liệu thành nhiều phần. Nó ảnh hưởng trực tiếp tới hiệu năng, trải nghiệm người dùng và độ ổn định của ứng dụng. Nếu dự án đang dùng offset pagination ở mọi nơi, hãy xem lại các màn hình có dữ liệu lớn hoặc thay đổi liên tục. Chuyển một số danh sách sang cursor pagination có thể giúp ứng dụng mượt hơn và giảm nhiều lỗi khó chịu về sau.