Thiết Kế Phân Trang API Cho Hàng Triệu Bản Ghi Như Thế Nào?

Ashley Innocent

Ashley Innocent

13 tháng 3 2026

Thiết Kế Phân Trang API Cho Hàng Triệu Bản Ghi Như Thế Nào?

Apidog cho doanh nghiệp

Triển khai tại chỗ

SSO & RBAC

Tuân thủ SOC 2

Khám phá Apidog Enterprise

TÓM TẮT

Đối với các tập dữ liệu lớn, hãy sử dụng phân trang dựa trên con trỏ (cursor-based) hoặc bộ khóa (keyset) thay vì phân trang dựa trên offset. Phân trang theo offset (?page=1&limit=20) hoạt động kém hiệu quả với hàng triệu bản ghi và cho phép dữ liệu không nhất quán. Modern PetstoreAPI triển khai phân trang dựa trên con trỏ với các token mờ (opaque tokens) và liên kết HATEOAS để có kết quả hiệu quả, nhất quán.

Giới thiệu

API của bạn trả về danh sách thú cưng. Bạn có 10 triệu thú cưng trong cơ sở dữ liệu. Một client gửi yêu cầu GET /pets?page=500000&limit=20. Cơ sở dữ liệu của bạn thực thi OFFSET 10000000 LIMIT 20. Truy vấn mất 30 giây. API của bạn bị hết thời gian chờ.

Đây là vấn đề của phân trang theo offset. Nó hoạt động tốt với các tập dữ liệu nhỏ nhưng bị lỗi khi mở rộng quy mô. Cơ sở dữ liệu phải quét hàng triệu hàng để đạt đến offset, mặc dù bạn chỉ trả về 20 kết quả.

Swagger Petstore cũ hoàn toàn không đề cập đến phân trang. Modern PetstoreAPI triển khai phân trang dựa trên con trỏ có thể mở rộng tới hàng triệu bản ghi với hiệu suất nhất quán.

💡
Nếu bạn đang xây dựng hoặc kiểm thử REST API, Apidog giúp bạn kiểm tra hành vi phân trang, xác thực định dạng phản hồi và đảm bảo API của bạn xử lý các tập dữ liệu lớn một cách chính xác. Bạn có thể mô phỏng các kịch bản phân trang, kiểm tra các trường hợp biên và xác minh hiệu suất.
nút

Trong hướng dẫn này, bạn sẽ tìm hiểu lý do tại sao phân trang theo offset thất bại, cách phân trang dựa trên con trỏ hoạt động và cách Modern PetstoreAPI triển khai phân trang hiệu quả.

Tại sao phân trang theo offset thất bại khi mở rộng quy mô

Phân trang theo offset là cách tiếp cận phổ biến nhất, nhưng nó có những vấn đề nghiêm trọng.

Cách phân trang theo offset hoạt động

GET /pets?page=1&limit=20    → OFFSET 0 LIMIT 20
GET /pets?page=2&limit=20    → OFFSET 20 LIMIT 20
GET /pets?page=3&limit=20    → OFFSET 40 LIMIT 20

Cơ sở dữ liệu bỏ qua offset hàng và trả về limit hàng.

Vấn đề 1: Hiệu suất giảm dần theo số trang

Trang 1:

SELECT * FROM pets OFFSET 0 LIMIT 20;
-- Nhanh: quét 20 hàng

Trang 1000:

SELECT * FROM pets OFFSET 20000 LIMIT 20;
-- Chậm: quét 20.020 hàng, trả về 20

Trang 500.000:

SELECT * FROM pets OFFSET 10000000 LIMIT 20;
-- Rất chậm: quét 10.000.020 hàng, trả về 20

Cơ sở dữ liệu phải quét tất cả các hàng cho đến offset, ngay cả khi nó loại bỏ chúng. Hiệu suất giảm dần theo cấp số cộng với số trang.

Vấn đề 2: Kết quả không nhất quán

Trong khi một client duyệt qua các kết quả, dữ liệu thay đổi:

Yêu cầu 1:

GET /pets?page=1&limit=2
Returns: [Thú cưng A, Thú cưng B]

Ai đó thêm Thú cưng Z (sắp xếp theo thứ tự bảng chữ cái đầu tiên)

Yêu cầu 2:

GET /pets?page=2&limit=2
Returns: [Thú cưng B, Thú cưng C]  ← Thú cưng B xuất hiện hai lần!

Thú cưng B xuất hiện trên cả hai trang vì một thú cưng mới đã được thêm vào. Ngược lại, thú cưng có thể bị bỏ qua nếu có xóa.

Vấn đề 3: Phân trang sâu tốn kém

Người dùng hiếm khi vượt quá trang 10. Nhưng nếu API của bạn cho phép ?page=1000000, bạn phải xử lý nó. Các truy vấn phân trang sâu tốn kém và có thể được sử dụng cho các cuộc tấn công từ chối dịch vụ.

Khi nào phân trang theo offset có thể chấp nhận được

Phân trang theo offset hoạt động tốt cho:

Đối với các API công khai hoặc tập dữ liệu lớn, hãy sử dụng phân trang dựa trên con trỏ.

Giải thích phân trang dựa trên con trỏ

Phân trang dựa trên con trỏ sử dụng một token mờ để đánh dấu vị trí trong tập kết quả.

Cách nó hoạt động

Yêu cầu 1:

GET /pets?limit=20

Phản hồi 1:

{
  "data": [...],
  "pagination": {
    "nextCursor": "eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9",
    "hasMore": true
  }
}

Yêu cầu 2:

GET /pets?cursor=eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9&limit=20

Con trỏ là một token mờ (thường được mã hóa base64) mã hóa vị trí. Client không phân tích cú pháp nó—chỉ cần chuyển nó trở lại.

Lợi ích

1. Hiệu suất nhất quán

Cơ sở dữ liệu sử dụng chỉ mục để tìm vị trí con trỏ trực tiếp:

SELECT * FROM pets
WHERE id > '019b4132-70aa-764f-b315-e2803d882a24'
ORDER BY id
LIMIT 20;

Truy vấn này nhanh chóng bất kể vị trí trong tập dữ liệu. Nó sử dụng tìm kiếm chỉ mục, không phải quét.

2. Kết quả nhất quán

Các con trỏ ổn định. Nếu dữ liệu thay đổi giữa các yêu cầu, bạn vẫn nhận được kết quả nhất quán. Các bản ghi mới không gây ra trùng lặp hoặc bỏ qua.

3. Không có tấn công phân trang sâu

Client không thể nhảy đến các vị trí tùy ý. Chúng phải phân trang tuần tự, điều này hạn chế lạm dụng.

Định dạng con trỏ

Con trỏ thường là JSON được mã hóa base64:

// Con trỏ đã giải mã
{
  "id": "019b4132-70aa-764f-b315-e2803d882a24",
  "createdAt": "2026-03-13T10:30:00Z"
}

Con trỏ chứa đủ thông tin để tiếp tục phân trang. Đối với Modern PetstoreAPI, điều này bao gồm ID tài nguyên và trường sắp xếp.

Phân trang bộ khóa cho dữ liệu đã sắp xếp

Phân trang bộ khóa là một biến thể của phân trang dựa trên con trỏ dành cho dữ liệu đã sắp xếp.

Cách nó hoạt động

Thay vì một con trỏ mờ, bạn sử dụng giá trị cuối cùng từ trang trước:

Yêu cầu 1:

GET /pets?limit=20&sortBy=createdAt

Phản hồi 1:

{
  "data": [
    {"id": "...", "createdAt": "2026-03-13T10:00:00Z"},
    ...
    {"id": "...", "createdAt": "2026-03-13T10:30:00Z"}
  ]
}

Yêu cầu 2:

GET /pets?limit=20&sortBy=createdAt&after=2026-03-13T10:30:00Z

Tham số after sử dụng giá trị createdAt cuối cùng từ trang trước.

Truy vấn SQL

SELECT * FROM pets
WHERE created_at > '2026-03-13T10:30:00Z'
ORDER BY created_at
LIMIT 20;

Điều này hiệu quả vì nó sử dụng chỉ mục trên created_at.

Khi nào nên sử dụng phân trang bộ khóa

Modern PetstoreAPI sử dụng phân trang dựa trên con trỏ theo mặc định nhưng hỗ trợ phân trang bộ khóa cho dữ liệu chuỗi thời gian.

Cách Modern PetstoreAPI triển khai phân trang

Modern PetstoreAPI sử dụng phân trang dựa trên con trỏ với các liên kết HATEOAS.

Định dạng yêu cầu

GET /pets?limit=20
GET /pets?cursor={token}&limit=20

Tham số:

Định dạng phản hồi

{
  "data": [
    {
      "id": "019b4132-70aa-764f-b315-e2803d882a24",
      "name": "Fluffy",
      "species": "CAT"
    }
  ],
  "pagination": {
    "limit": 20,
    "hasMore": true,
    "nextCursor": "eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9"
  },
  "links": {
    "self": "https://petstoreapi.com/pets?limit=20",
    "next": "https://petstoreapi.com/pets?cursor=eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9&limit=20"
  }
}

Các tính năng chính

1. Con trỏ mờ

Các con trỏ được mã hóa base64. Client không phân tích cú pháp chúng.

2. Liên kết HATEOAS

Đối tượng links cung cấp các URL sẵn sàng sử dụng. Client không cần xây dựng URL phân trang.

3. Cờ hasMore

Cho biết liệu còn kết quả nào nữa hay không. Client biết khi nào nên dừng phân trang.

4. Xác thực giới hạn

Giới hạn tối đa là 100. Ngăn client yêu cầu các trang quá lớn.

Xem tài liệu phân trang của Modern PetstoreAPI để biết chi tiết đầy đủ.

Định dạng phản hồi phân trang

Modern PetstoreAPI gói các phản hồi được phân trang trong một cấu trúc nhất quán.

Trình bao bọc tập hợp (Collection Wrapper)

{
  "data": [...],
  "pagination": {...},
  "links": {...}
}

Tại sao lại bao bọc các tập hợp?

  1. Khả năng mở rộng - Có thể thêm siêu dữ liệu mà không làm hỏng client
  2. Tính nhất quán - Tất cả các điểm cuối được phân trang đều sử dụng cùng một định dạng
  3. HATEOAS - Các liên kết hướng dẫn client thông qua phân trang

Siêu dữ liệu phân trang

"pagination": {
  "limit": 20,
  "hasMore": true,
  "nextCursor": "...",
  "totalCount": 1000  // Tùy chọn, tốn kém để tính toán
}

totalCount là tùy chọn vì việc tính toán nó rất tốn kém đối với các tập dữ liệu lớn. Hầu hết các client không cần nó.

Kiểm thử phân trang bằng Apidog

Apidog giúp bạn kiểm thử hành vi phân trang một cách toàn diện.

Các kịch bản kiểm thử

1. Trang đầu tiên

GET /pets?limit=20
Mong đợi: 20 kết quả, hasMore=true, nextCursor hiện diện

2. Các trang tiếp theo

GET /pets?cursor={token}&limit=20
Mong đợi: 20 kết quả, hasMore=true/false, nextCursor hiện diện/không hiện diện

3. Trang cuối cùng

GET /pets?cursor={lastToken}&limit=20
Mong đợi: < 20 kết quả, hasMore=false, không có nextCursor

4. Kết quả trống

GET /pets?status=NONEXISTENT&limit=20
Mong đợi: 0 kết quả, hasMore=false, không có nextCursor

5. Xác thực giới hạn

GET /pets?limit=1000
Mong đợi: 400 Bad Request (vượt quá giới hạn tối đa)

Cấu hình kiểm thử Apidog

// Kiểm thử: Cấu trúc phân trang
pm.test("Response has pagination", () => {
  pm.expect(pm.response.json()).to.have.property('pagination');
  pm.expect(pm.response.json().pagination).to.have.property('hasMore');
});

// Kiểm thử: Liên kết HATEOAS
pm.test("Response has links", () => {
  const links = pm.response.json().links;
  pm.expect(links).to.have.property('self');
  if (pm.response.json().pagination.hasMore) {
    pm.expect(links).to.have.property('next');
  }
});

Chọn chiến lược phân trang phù hợp

Các chiến lược khác nhau phù hợp với các trường hợp sử dụng khác nhau.

Phân trang theo offset

Sử dụng khi:

Không sử dụng khi:

Phân trang dựa trên con trỏ

Sử dụng khi:

Không sử dụng khi:

Phân trang bộ khóa

Sử dụng khi:

Không sử dụng khi:

Khuyến nghị của Modern PetstoreAPI: Sử dụng phân trang dựa trên con trỏ cho các API công khai và tập dữ liệu lớn.

Kết luận

Phân trang rất quan trọng đối với các API trả về tập dữ liệu lớn. Phân trang theo offset đơn giản nhưng không mở rộng được. Phân trang dựa trên con trỏ mang lại hiệu suất nhất quán và kết quả đáng tin cậy cho hàng triệu bản ghi.

Modern PetstoreAPI triển khai phân trang dựa trên con trỏ với các token mờ, liên kết HATEOAS và siêu dữ liệu phù hợp. Thiết kế này mở rộng hiệu quả và mang lại trải nghiệm tuyệt vời cho nhà phát triển.

Kiểm thử triển khai phân trang của bạn với Apidog để đảm bảo nó xử lý các trường hợp biên, xác thực giới hạn và trả về kết quả nhất quán.

Những điểm chính cần ghi nhớ:

nút

Câu hỏi thường gặp

Tại sao không chỉ trả về tất cả kết quả mà không phân trang?

Trả về hàng triệu bản ghi trong một phản hồi gây ra các vấn đề về bộ nhớ, truyền tải mạng chậm và trải nghiệm người dùng kém. Phân trang là điều cần thiết cho các tập dữ liệu lớn.

Client có thể nhảy đến một trang cụ thể bằng phân trang con trỏ không?

Không, phân trang con trỏ yêu cầu truy cập tuần tự. Nếu cần truy cập ngẫu nhiên, hãy xem xét phân trang theo offset cho các tập dữ liệu nhỏ hoặc triển khai tìm kiếm/lọc thay thế.

Làm cách nào để xử lý phân trang với tính năng lọc?

Bao gồm các tham số lọc trong các yêu cầu phân trang: GET /pets?status=AVAILABLE&cursor={token}&limit=20. Con trỏ mã hóa cả vị trí và trạng thái lọc.

Tôi có nên bao gồm tổng số lượng trong phản hồi phân trang không?

Chỉ khi client cần và tập dữ liệu của bạn nhỏ. Tính toán tổng số lượng rất tốn kém đối với các tập dữ liệu lớn (yêu cầu một truy vấn COUNT riêng biệt).

Làm cách nào để triển khai phân trang con trỏ trong SQL?

Sử dụng mệnh đề WHERE với giá trị con trỏ: SELECT * FROM pets WHERE id > ? ORDER BY id LIMIT 20. Đảm bảo bạn có một chỉ mục trên cột sắp xếp.

Điều gì xảy ra nếu token con trỏ của tôi trở nên không hợp lệ?

Trả về 400 Bad Request với thông báo lỗi. Các con trỏ có thể trở nên không hợp lệ nếu dữ liệu bị xóa hoặc trạng thái phân trang hết hạn.

Con trỏ nên có hiệu lực trong bao lâu?

Các con trỏ của Modern PetstoreAPI có hiệu lực vô thời hạn miễn là tài nguyên được tham chiếu tồn tại. Một số API hết hạn con trỏ sau 24 giờ.

Tôi có thể sử dụng phân trang con trỏ với nhiều trường sắp xếp không?

Có, nhưng con trỏ phải mã hóa tất cả các trường sắp xếp. Điều này làm cho các con trỏ phức tạp hơn. Thay vào đó, hãy xem xét sử dụng một khóa sắp xếp tổng hợp duy nhất.

Thực hành thiết kế API trong Apidog

Khám phá cách dễ dàng hơn để xây dựng và sử dụng API

Thiết Kế Phân Trang API Cho Hàng Triệu Bản Ghi Như Thế Nào?