Offset Pagination trong phát triển di động: nó là gì và cách triển khai

Tác giả: IT Sectr Đã đăng: 2026-03-11 Thời gian đọc: 10 phút

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 — phương pháp phân trang trong đó server bỏ qua N bản ghi và trả về M bản ghi tiếp theo.
  • Tính đơn giản trong triển khai khiến nó trở thành tiêu chuẩn cho REST API và client di động.
  • Vấn đề bỏ sót — khi chèn bản ghi giữa các yêu cầu, người dùng thấy các bản sao.
  • Sự dịch chuyển dữ liệu — xóa bản ghi dẫn đến dịch chuyển trang và mất nội dung.
  • Phân trang Cursor-based giải quyết những vấn đề này thông qua con trỏ đến bản ghi cuối cùng thay vì độ lệch.

Offset Pagination là gì?

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 OFFSETLIMIT, 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.

Cấu trúc yêu cầu và phản hồi

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.

kotlin
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>>

Cách Offset Pagination hoạt động

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.

Truy vấn SQL bên trong

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.

sql
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;

Vấn đề nhất quán

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.

Offset vs Cursor-based: so sánh các phương pháp

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 PaginationCursor-based Pagination
Tính đơn giảnCao — hai tham số sốTrung bình — yêu cầu mã hóa con trỏ
Tính nhất quánThấp — trùng lặp khi chènCao — con trỏ không bị ảnh hưởng bởi thay đổi
Hiệu suấtGiảm khi offset lớnỔn định ở mọi khối lượng
Nhảy trangCó — có thể điều hướng đến bất kỳ trang nàoKhông — chỉ điều hướng tuần tự
Phù hợp choBảng <10K bản ghi, UI có số trangFeed, 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

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ự.

Khi nào sử dụng Offset Pagination

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ương pháp kết hợp

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.

Offset Pagination trong ứng dụng di động

Ứ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ó.

Triển khai trong Kotlin với Paging 3

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.

kotlin
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 thường gặp với Offset Pagination

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 Pagination khác Cursor-based như thế nào?

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.

Khi nào Offset Pagination hoạt động kém?

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 nào là tối ưu cho Offset Pagination?

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).

Làm thế nào để xử lý trùng lặp với Offset Pagination?

Để 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ó thể sử dụng Offset Pagination với GraphQL không?

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

  • Offset Pagination — phương pháp phân trang với tham số offset và limit để bỏ qua và giới hạn bản ghi khi tải dữ liệu theo trang từ API.
  • Tính đơn giản trong triển khai và tính độc lập của yêu cầu khiến Offset Pagination trở thành phương pháp tiêu chuẩn cho 72% REST API (dữ liệu Postman, 2025).
  • Hiệu suất giảm ở offset trên 10.000 do quét bảng đến vị trí mục tiêu — cơ sở dữ liệu đọc tất cả các hàng đã loại bỏ.
  • Vấn đề nhất quán — việc chèn và xóa bản ghi giữa các yêu cầu dẫn đến trùng lặp và khoảng trống trong kết quả trang.
  • Phân trang Cursor-based giải quyết các vấn đề của Offset Pagination bằng cách sử dụng con trỏ đến bản ghi cuối cùng thay vì độ lệch số.
  • Phương pháp kết hợp kết hợp offset trang đầu tiên với tải cursor cho cuộn vô hạn trong ứng dụng di động.
  • Khuyến nghị — sử dụng Offset Pagination cho tập tĩnh lên đến 10.000 bản ghi và chuyển sang con trỏ cho khối lượng lớn và dữ liệu động.

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.

Thảo luận dự án

Đọc thêm