Khi xây dựng REST hoặc GraphQL API cho danh sách dữ liệu, hầu hết lập trình viên đều bắt đầu bằng Offset Pagination. Cách phân trang này rất quen thuộc và dễ cài đặt với các từ khóa SQL cơ bản như LIMIT và OFFSET. Tuy nhiên, khi cơ sở dữ liệu tăng trưởng lên hàng trăm nghìn hoặc hàng triệu bản ghi, Offset Pagination sẽ trở thành điểm nghẽn kiến trúc nghiêm trọng, gây ra độ trễ truy vấn lớn và các lỗi hiển thị dữ liệu rất khó chịu.
Trong bài viết này, chúng ta sẽ cùng phân tích lý do tại sao Offset Pagination lại "đuối sức" khi dữ liệu lớn và cách chuyển sang Cursor-Based Pagination để duy trì hiệu năng truy vấn O(1) ổn định cho hệ thống.
Bẫy Hiệu Năng Của OFFSET
Để hiểu tại sao Offset Pagination lại làm chậm hệ thống, hãy xem cách một database engine xử lý câu lệnh SQL khi giá trị offset lớn:
SELECT id, title, created_at
FROM articles
ORDER BY id DESC
LIMIT 20 OFFSET 500000;
Nhiều người lầm tưởng rằng database sẽ dùng index scan để nhảy ngay lập tức đến dòng thứ 500.001. Thực tế, các hệ quản trị cơ sở dữ liệu quan hệ như PostgreSQL hay MySQL không thể định vị trực tiếp vị trí của dòng thứ N mà không cần đếm.
Database engine bắt buộc phải đọc qua 500.020 bản ghi từ đĩa hoặc index, bỏ đi 500.000 bản ghi đầu tiên, và chỉ lấy ra 20 bản ghi cuối cùng. Khi số trang tăng lên, khối lượng công việc database phải thực hiện tăng lên theo tỷ lệ tuyến tính. Một query lấy trang 1 có thể chỉ tốn 2 millisecond, nhưng query lấy trang 25.000 hoàn toàn có thể tiêu tốn hơn 1.500 millisecond CPU time và I/O.
Hiện Tượng Drift Dữ Liệu (Trùng / Mất Bản Ghi)

Bên cạnh vấn đề độ trễ, Offset Pagination còn mắc phải sự cố nhất quán dữ liệu (data drift) mỗi khi có bản ghi mới được thêm hoặc xóa trong lúc người dùng đang duyệt danh sách.
Hãy tưởng tượng kịch bản người dùng lướt bảng tin:
- Người dùng tải Trang 1 (
LIMIT 10 OFFSET 0) và nhận được các bài viết từ 1 đến 10. - Trong lúc người dùng đang đọc, có 3 bài viết mới được xuất bản lên đầu trang.
- Người dùng chuyển sang Trang 2 (
LIMIT 10 OFFSET 10).
Vì có 3 bài viết mới chèn vào đầu, các bài viết số 8, 9, 10 ở Trang 1 trước đó đã bị đẩy xuống vị trí 11, 12, 13. Khi server chạy query cho Trang 2 với OFFSET 10, người dùng sẽ thấy lại các bài viết 8, 9, 10 một lần nữa. Người dùng bị lặp lại dữ liệu.
Ngược lại, nếu có dữ liệu bị xóa, danh sách bị co lại khiến người dùng bị bỏ lỡ bài viết mà không hề hay biết.
Cơ Chế Của Cursor-Based Pagination

Cursor-Based Pagination (hay còn gọi là Keyset Pagination) giải quyết triệt để cả hai vấn đề hiệu năng và drift dữ liệu bằng cách thay thế số trang bằng một con trỏ (cursor) trỏ tới bản ghi cụ thể.
Thay vì yêu cầu database "bỏ qua 500.000 dòng", Cursor Pagination yêu cầu database "lấy 20 dòng được tạo trước bản ghi X". Vì con trỏ dựa trên một cột đã được đánh index, database sẽ dùng cấu trúc B-tree index để tìm chính xác điểm bắt đầu chỉ trong thời gian logarit.
Câu lệnh SQL được tối ưu lại như sau:
-- Truy vấn O(1) siêu nhanh dùng Cursor
SELECT id, title, created_at
FROM articles
WHERE id < 499980 -- Giá trị ID của item cuối cùng ở trang trước
ORDER BY id DESC
LIMIT 20;
Do cột id đã có index, database nhảy thẳng đến vị trí id < 499980 và đọc đúng 20 bản ghi. Thời gian thực thi gần như không thay đổi, dù bạn đang ở trang đầu tiên hay trang thứ một triệu.
Thiết Kế Structure Payload Trả Về Của API
Trong thực tế sản xuất, bạn nên mã hóa giá trị cursor thành một chuỗi base64 thay vì lộ trực tiếp ID cơ sở dữ liệu ra ngoài API client. Điều này giúp bảo vệ thông tin bảng dữ liệu nội bộ và cho phép bạn linh hoạt thay đổi cấu trúc con trỏ sau này.
Ví dụ về định dạng phản hồi REST API chuẩn:
{
"data": [
{
"id": 499979,
"title": "Tối ưu hóa B-Tree Index trong PostgreSQL",
"created_at": "2026-08-20T10:00:00Z"
}
],
"pagination": {
"next_cursor": "ZXlKaWFXUWlPakE1T1RrM09Tdz0=",
"has_more": true
}
}
Khi muốn lấy trang tiếp theo, ứng dụng client chỉ cần gửi query param ?cursor=ZXlKaWFXUWlPakE1T1RrM09Tdz0= lên server.
Khi Nào Vẫn Nên Dùng Offset Pagination?
Cursor Pagination không phải là viên đạn bạc cho mọi trường hợp. Nó đi kèm với một số hạn chế:
- Không thể nhảy trang tùy ý: Người dùng không thể nhảy trực tiếp sang "Trang 42" vì để biết con trỏ Trang 42, hệ thống phải đếm qua Trang 41.
- Bắt buộc có thứ tự sắp xếp cố định: Query bắt buộc phải sắp xếp theo các cột duy nhất và có thứ tự rõ ràng (ví dụ: primary key ID hoặc kết hợp
created_atvớiid).
Nếu sản phẩm của bạn bắt buộc phải có thanh phân trang số (1, 2, 3... 50) với tính năng nhảy trang linh hoạt, Offset Pagination vẫn là lựa chọn bắt buộc. Tuy nhiên, đối với newsfeed di động, mạng xã hội, giao diện cuộn vô tận (infinite scroll) hoặc các API tích hợp hệ thống tần suất cao, Cursor Pagination là giải pháp vượt trội hoàn toàn.
Lời Kết
Tiếp tục sử dụng Offset Pagination cho bộ dữ liệu lớn sẽ sớm làm quá tải cơ sở dữ liệu và gây nhầm lẫn cho người dùng do dữ liệu xô lệch. Bằng cách chuyển sang Cursor-Based Pagination, bạn đảm bảo thời gian phản hồi O(1) dự đoán được và mang lại trải nghiệm mượt mà, tin cậy cho ứng dụng ở bất kỳ quy mô dữ liệu nào.

Phản hồi
Đang tải bình luận…