Offset Pagination — phân trang theo độ lệch — phương pháp tải dữ liệu theo trang qua HTTP API. Client truyền tham số offset (độ lệch từ đầu) và limit (kích thước trang), server trả về các bản ghi từ vị trí offset. Theo REST API Tutorial, phương pháp này được sử dụng rộng rãi trong các dịch vụ RESTful nhờ tính đơn giản trong triển khai. Tuy nhiên, trên khối lượng dữ liệu lớn, phân trang offset mất hiệu suất do phải quét toàn bộ bảng đến vị trí cần thiết.
Những điểm chính
Offset Pagination là phương pháp phân trang dữ liệu trong đó yêu cầu client chứa hai tham số: offset (bao nhiêu bản ghi cần bỏ qua) và limit (bao nhiêu bản ghi cần trả về). Server thực thi truy vấn SQL với OFFSET và LIMIT, bỏ qua số lượng hàng đã chỉ định và trả về tập kết quả có kích thước cố định.
Phương pháp này ra đời trong cơ sở dữ liệu quan hệ như cách đơn giản nhất để tổ chức điều hướng trang và được chuyển sang HTTP API cùng với sự phát triển của kiến trúc REST. Offset Pagination không yêu cầu lưu trữ trạng thái trên server — mỗi yêu cầu độc lập và chứa tất cả thông tin cần thiết cho truy vấn.
Theo báo cáo thiết kế API của Postman (2025), phân trang offset được sử dụng trong 72% REST API công khai, khiến nó trở thành tiêu chuẩn chi phối bất chấp các hạn chế hiệu suất đã biết trên tập dữ liệu lớn.
Một yêu cầu REST điển hình với Offset Pagination bao gồm các tham số truy vấn offset và limit. Phản hồi chứa danh sách bản ghi của trang được yêu cầu và siêu dữ liệu để xây dựng giao diện điều hướng.
Tham số limit giới hạn số lượng bản ghi được trả về và bảo vệ server cùng client khỏi tải quá mức. Giá trị limit điển hình từ 10 đến 50 bản ghi mỗi trang tùy thuộc vào độ phức tạp của dữ liệu.
data class PageRequest(
val offset: Int,
val limit: Int
)
data class PageResponse<T>(
val items: List<T>,
val total: Int,
val hasMore: Boolean
)
fun RetrofitApi.fetchPage(request: PageRequest): Call<PageResponse<Item>>
Offset Pagination được chuyển đổi thành truy vấn SQL với các cấu trúc OFFSET và FETCH NEXT (hoặc LIMIT trong MySQL/SQLite). Server cơ sở dữ liệu quét bảng, bỏ qua số hàng bằng offset và trả về limit hàng tiếp theo. Offset càng lớn, truy vấn càng lâu.
Vấn đề hiệu suất liên quan đến thực tế là cơ sở dữ liệu không thể nhảy trực tiếp đến vị trí offset — nó phải đọc và loại bỏ tất cả các hàng trước đó. Với offset = 100000 và limit = 20, DBMS đọc 100.020 hàng và chỉ trả về 20.
SQL — ngôn ngữ mà server thực thi phân trang offset. PostgreSQL và MySQL sử dụng LIMIT, trong khi SQL Server và Oracle sử dụng OFFSET...FETCH. Các DBMS khác nhau tối ưu hóa truy vấn này theo cách khác nhau, nhưng vấn đề quét cơ bản vẫn giống nhau.
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;
Tính nhất quán dữ liệu là nhược điểm chính của Offset Pagination khi làm việc với các tập động. Nếu một bản ghi mới được thêm vào đầu bảng giữa hai yêu cầu người dùng, tất cả các bản ghi hiện có sẽ bị dịch chuyển. Người dùng thấy các bản sao hoặc khoảng trống.
Hãy xem xét một bảng có 100 bản ghi với limit = 20. Ở trang 1, người dùng thấy các bản ghi 1-20. Quản trị viên thêm 5 bản ghi mới. Ở trang 2, người dùng thấy các bản ghi 26-45 thay vì 21-40 như mong đợi — các bản ghi 21-25 bị bỏ sót và các bản ghi 21-25 từ tập trước bị trùng lặp ở trang 1.
Phân trang Cursor-based là một giải pháp thay thế cho Offset Pagination sử dụng con trỏ đến bản ghi cuối cùng của trang hiện tại. Thay vì độ lệch số, client truyền định danh của bản ghi cuối cùng đã nhận và server trả về N bản ghi tiếp theo sau nó.
Phương pháp cursor-based giải quyết vấn đề nhất quán: vị trí con trỏ không thay đổi khi chèn hoặc xóa vì con trỏ tham chiếu đến một bản ghi cụ thể, không phải một vị trí. Tuy nhiên, nó phức tạp hơn trong triển khai — yêu cầu một trường duy nhất có thể sắp xếp (thường là ID hoặc timestamp).
| Tham số | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| Tính đơn giản | Cao — hai tham số số | Trung bình — yêu cầu mã hóa con trỏ |
| Tính nhất quán | Thấp — trùng lặp khi chèn | Cao — con trỏ không bị ảnh hưởng bởi thay đổi |
| Hiệu suất | Giảm khi offset lớn | Ổn định ở mọi khối lượng |
| Nhảy trang | Có — có thể điều hướng đến bất kỳ trang nào | Không — chỉ điều hướng tuần tự |
| Phù hợp cho | Bảng <10K bản ghi, UI có số trang | Feed, cuộn vô hạn, tập lớn |
Lựa chọn giữa các phương pháp phụ thuộc vào yêu cầu giao diện người dùng. Nếu cần điều hướng với số trang và nhảy trực tiếp — Offset Pagination đơn giản hơn. Cho cuộn vô hạn hoặc feed tin tức, con trỏ được ưu tiên hơn.
Keyset pagination là một biến thể của phương pháp cursor-based trong đó việc lọc được thực hiện trên khóa duy nhất bằng WHERE thay vì OFFSET. Truy vấn SQL sử dụng điều kiện như WHERE id > lastId, cho phép cơ sở dữ liệu sử dụng chỉ mục mà không cần quét các hàng đã loại bỏ.
Theo PostgreSQL Wiki, keyset pagination chạy nhanh hơn 100-1000 lần so với truy vấn offset ở độ lệch lớn vì quét chỉ mục thay thế quét toàn bộ bảng. Nhược điểm là không thể nhảy đến trang tùy ý mà không cần duyệt tuần tự.
Offset Pagination là tối ưu cho tập dữ liệu nhỏ và vừa (lên đến 10.000 bản ghi) nơi người dùng cần giao diện số trang. Các kịch bản điển hình bao gồm bảng quản trị, danh sách đơn hàng và danh mục đã lọc với phân trang theo trang.
Đối với ứng dụng di động, phân trang offset phù hợp khi tải dữ liệu lịch sử nơi việc chèn mới hiếm hoặc không thể — ví dụ: lịch sử đơn hàng người dùng, danh sách tác vụ đã hoàn thành, kho lưu trữ giao dịch. Trong các kịch bản này, vấn đề nhất quán không phát sinh.
Không được khuyến nghị cho feed mạng xã hội, danh sách bình luận, chat và các tập động khác có chèn thường xuyên. Trong những trường hợp này, khoảng trống và bản ghi trùng lặp làm giảm trải nghiệm người dùng và yêu cầu logic loại bỏ trùng lặp bổ sung trên client.
Phân trang kết hợp kết hợp offset và cursor: yêu cầu đầu tiên sử dụng offset để hiển thị trang ban đầu, trong khi các yêu cầu tiếp theo sử dụng cursor để tải cuộn vô hạn. Phương pháp này được sử dụng trong Instagram và Twitter, nơi trang đầu tiên được tải qua cursor, nhưng offset được sử dụng để tính vị trí khi quay lại chế độ xem trước đó.
Triển khai phương pháp kết hợp yêu cầu lưu trữ vị trí ảo của người dùng trên client và phối hợp hai cơ chế phân trang trên server. Theo blog Instagram Engineering, nhóm của họ sử dụng phân trang cursor-based với trường bổ sung startCursor thay thế offset cho tải ban đầu.
Ứng dụng di động sử dụng Offset Pagination cùng với Retrofit/OkHttp trên Android và URLSession/Combine trên iOS. Mẫu điển hình là tải trang tiếp theo khi cuộn đến cuối danh sách qua RecyclerView.OnScrollListener hoặc UICollectionView prefetching.
Triển khai phân trang offset trên client di động bao gồm ba thành phần: trình quản lý phân trang (lưu trữ offset hiện tại và hasMore), bộ điều hợp danh sách (hiển thị mục và chỉ báo tải) và kho lưu trữ (thực thi yêu cầu và xử lý lỗi). Android Jetpack cung cấp thư viện Paging 3, hỗ trợ cả phân trang offset và cursor-based sẵn có.
Paging 3 là thư viện Android Jetpack để tải dữ liệu theo trang. Nó đóng gói logic phân trang, bao gồm theo dõi offset, quản lý trạng thái tải và tải trước tự động khi cuộn. PagingSource xác định các khóa cho trang tiếp theo và trang trước.
class OffsetPagingSource(
private val api: ApiService,
private val limit: Int = 20
) : PagingSource<Int, Item>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Item> {
val offset = params.key ?: 0
return try {
val response = api.getItems(offset, limit)
LoadResult.Page(
data = response.items,
prevKey = null,
nextKey = if (response.hasMore) offset + limit else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
PagingSource xác định các khóa prevKey và nextKey cho điều hướng trang. Với phân trang offset, prevKey luôn là null (không thể đến trang trước mà không lưu lịch sử), trong khi nextKey tăng lên limit sau mỗi lần tải cho đến khi server trả về hasMore = false. Đây là mô hình đơn giản và có thể dự đoán cho danh sách di động.
Lỗi đầu tiên — dựa vào thứ tự bản ghi mà không sắp xếp. Offset Pagination yêu cầu sắp xếp ORDER BY ổn định trên một trường duy nhất. Nếu không có, DBMS có thể trả về bản ghi theo thứ tự tùy ý, dẫn đến trùng lặp và khoảng trống ngẫu nhiên giữa các trang.
Lỗi thứ hai — sử dụng offset để tính số trang trong giao diện. Công thức page = offset / limit + 1 chỉ hoạt động nếu không có bản ghi nào bị xóa hoặc thêm vào giữa các lần tải. Với dữ liệu động, số trang trở nên không chính xác và người dùng thấy thông tin sai.
Lỗi thứ ba — bỏ qua thời gian chờ của truy vấn với offset lớn. Với offset trên 100.000, truy vấn có thể mất hàng chục giây, chặn giao diện và tiêu tốn tài nguyên server. Nên đặt giá trị offset tối đa ở cấp API (ví dụ: 10.000) và sử dụng phân trang cursor-based cho khối lượng lớn.
Lỗi thứ tư — không thêm total count vào phản hồi. Không có tổng số bản ghi, client không thể hiển thị số trang và triển khai phân trang có số. Tuy nhiên, COUNT(*) trên bảng lớn rất tốn kém — cho tập trên 100.000 bản ghi, hãy sử dụng ước tính gần đúng hoặc giới hạn giá trị tối đa của total.
Câu hỏi thường gặp
Offset sử dụng độ lệch số để bỏ qua bản ghi, trong khi cursor sử dụng con trỏ đến bản ghi cuối cùng của trang trước. Offset đơn giản hơn để triển khai nhưng bị trùng lặp khi chèn và giảm hiệu suất ở độ lệch lớn. Cursor ổn định dưới mọi thay đổi dữ liệu.
Phân trang offset không hiệu quả ở offset trên 10.000 bản ghi do quét toàn bộ bảng. Nó cũng không phù hợp cho tập động (feed, chat) nơi bản ghi mới xuất hiện giữa các yêu cầu — người dùng thấy khoảng trống và bản ghi trùng lặp khi điều hướng.
Limit tối ưu phụ thuộc vào kích thước bản ghi và tốc độ mạng — từ 10 đến 50 mục mỗi trang. Cho danh sách có hình ảnh lớn, sử dụng limit = 10-15; cho dữ liệu văn bản, 20-50. Luôn cho phép client chỉ định limit riêng với giới hạn tối đa trên server (thường là 100).
Để xử lý trùng lặp, sử dụng loại bỏ trùng lặp phía client theo ID duy nhất, áp dụng sắp xếp ổn định trên trường duy nhất hoặc chuyển sang phân trang cursor-based. Android Paging 3 hỗ trợ key để tự động loại bỏ trùng lặp mục danh sách.
Có, GraphQL hỗ trợ phân trang offset qua tham số offset và limit trong truy vấn, mặc dù đặc tả Relay khuyến nghị phương pháp cursor-based. Thư viện Apollo GraphQL và Relay cung cấp hỗ trợ tích hợp cho phân trang offset với quản lý trạng thái trang tự động.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm