ออกแบบ API Pagination อย่างไรให้รองรับข้อมูลจำนวนมหาศาล

Ashley Innocent

Ashley Innocent

13 March 2026

ออกแบบ API Pagination อย่างไรให้รองรับข้อมูลจำนวนมหาศาล

enterprise.banner.title

enterprise.banner.feature1

enterprise.banner.feature2

enterprise.banner.feature3

enterprise.banner.ctaB

สรุปย่อ

สำหรับชุดข้อมูลขนาดใหญ่ ให้ใช้การแบ่งหน้าแบบเคอร์เซอร์ (cursor-based) หรือคีย์เซ็ต (keyset) แทนการแบ่งหน้าแบบออฟเซ็ต (offset-based) การแบ่งหน้าแบบออฟเซ็ต (?page=1&limit=20) มีประสิทธิภาพต่ำเมื่อมีข้อมูลหลายล้านรายการและอาจทำให้ข้อมูลไม่สอดคล้องกัน PetstoreAPI สมัยใหม่ใช้การแบ่งหน้าแบบเคอร์เซอร์พร้อมโทเค็นแบบทึบและลิงก์ HATEOAS เพื่อผลลัพธ์ที่มีประสิทธิภาพและสอดคล้องกัน

บทนำ

API ของคุณส่งคืนรายการสัตว์เลี้ยง คุณมีสัตว์เลี้ยง 10 ล้านตัวในฐานข้อมูล ลูกค้าขอ GET /pets?page=500000&limit=20 ฐานข้อมูลของคุณดำเนินการ OFFSET 10000000 LIMIT 20 การสืบค้นใช้เวลา 30 วินาที API ของคุณหมดเวลา

นี่คือปัญหาการแบ่งหน้าแบบออฟเซ็ต มันทำงานได้ดีสำหรับชุดข้อมูลขนาดเล็ก แต่จะล้มเหลวเมื่อขยายขนาด ฐานข้อมูลต้องสแกนหลายล้านแถวเพื่อไปถึงออฟเซ็ต แม้ว่าคุณจะต้องการคืนค่าเพียง 20 รายการเท่านั้น

Swagger Petstore แบบเก่าไม่ได้กล่าวถึงการแบ่งหน้าเลย Modern PetstoreAPI ใช้การแบ่งหน้าแบบเคอร์เซอร์ที่สามารถขยายขนาดได้ถึงหลายล้านรายการพร้อมประสิทธิภาพที่สอดคล้องกัน

💡
หากคุณกำลังสร้างหรือทดสอบ REST API, Apidog ช่วยให้คุณทดสอบพฤติกรรมการแบ่งหน้า, ตรวจสอบรูปแบบการตอบกลับ, และมั่นใจว่า API ของคุณจัดการชุดข้อมูลขนาดใหญ่ได้อย่างถูกต้อง คุณสามารถจำลองสถานการณ์การแบ่งหน้า, ทดสอบกรณีขอบ, และยืนยันประสิทธิภาพได้
button

ในคู่มือนี้ คุณจะได้เรียนรู้ว่าทำไมการแบ่งหน้าแบบออฟเซ็ตถึงล้มเหลว, การแบ่งหน้าแบบเคอร์เซอร์ทำงานอย่างไร, และ Modern PetstoreAPI ใช้การแบ่งหน้าที่มีประสิทธิภาพอย่างไร

ทำไมการแบ่งหน้าแบบออฟเซ็ตถึงล้มเหลวเมื่อขยายขนาด

การแบ่งหน้าแบบออฟเซ็ตเป็นวิธีที่ใช้กันมากที่สุด แต่ก็มีปัญหาใหญ่

การแบ่งหน้าแบบออฟเซ็ตทำงานอย่างไร

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

ฐานข้อมูลจะข้ามแถวตามค่า offset และส่งคืนแถวตามค่า limit

ปัญหาที่ 1: ประสิทธิภาพลดลงตามเลขหน้า

หน้า 1:

SELECT * FROM pets OFFSET 0 LIMIT 20;
-- เร็ว: สแกน 20 แถว

หน้า 1000:

SELECT * FROM pets OFFSET 20000 LIMIT 20;
-- ช้า: สแกน 20,020 แถว, คืนค่า 20

หน้า 500,000:

SELECT * FROM pets OFFSET 10000000 LIMIT 20;
-- ช้ามาก: สแกน 10,000,020 แถว, คืนค่า 20

ฐานข้อมูลต้องสแกนทุกแถวไปจนถึงตำแหน่งออฟเซ็ต แม้ว่าจะทิ้งข้อมูลเหล่านั้นไป ประสิทธิภาพจะลดลงเป็นเส้นตรงตามเลขหน้า

ปัญหาที่ 2: ผลลัพธ์ไม่สอดคล้องกัน

ขณะที่ไคลเอ็นต์กำลังดูหน้าข้อมูลไปเรื่อยๆ ข้อมูลอาจมีการเปลี่ยนแปลง:

คำขอ 1:

GET /pets?page=1&limit=2
ส่งคืน: [สัตว์เลี้ยง A, สัตว์เลี้ยง B]

มีคนเพิ่มสัตว์เลี้ยง Z (จัดเรียงตามตัวอักษร)

คำขอ 2:

GET /pets?page=2&limit=2
ส่งคืน: [สัตว์เลี้ยง B, สัตว์เลี้ยง C]  ← สัตว์เลี้ยง B ปรากฏซ้ำสองครั้ง!

สัตว์เลี้ยง B ปรากฏในทั้งสองหน้าเนื่องจากมีการแทรกสัตว์เลี้ยงใหม่ ในทางกลับกัน สัตว์เลี้ยงอาจถูกข้ามไปได้หากมีการลบเกิดขึ้น

ปัญหาที่ 3: การแบ่งหน้าลึกมีค่าใช้จ่ายสูง

ผู้ใช้ไม่ค่อยไปไกลเกินหน้า 10 แต่ถ้า API ของคุณอนุญาต ?page=1000000 คุณต้องจัดการกับมัน การสืบค้นการแบ่งหน้าลึกมีค่าใช้จ่ายสูงและสามารถนำไปใช้โจมตีแบบปฏิเสธการให้บริการได้

เมื่อใดที่การแบ่งหน้าแบบออฟเซ็ตเป็นที่ยอมรับ

การแบ่งหน้าแบบออฟเซ็ตทำงานได้ดีสำหรับ:

สำหรับ API สาธารณะหรือชุดข้อมูลขนาดใหญ่ ให้ใช้การแบ่งหน้าแบบเคอร์เซอร์

คำอธิบายการแบ่งหน้าแบบเคอร์เซอร์

การแบ่งหน้าแบบเคอร์เซอร์ใช้โทเค็นแบบทึบเพื่อทำเครื่องหมายตำแหน่งในชุดผลลัพธ์

การทำงาน

คำขอ 1:

GET /pets?limit=20

การตอบกลับ 1:

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

คำขอ 2:

GET /pets?cursor=eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9&limit=20

เคอร์เซอร์เป็นโทเค็นแบบทึบ (โดยทั่วไปจะเข้ารหัสแบบ base64) ที่เข้ารหัสตำแหน่ง ไคลเอนต์ไม่จำเป็นต้องแยกวิเคราะห์มัน – แค่ส่งกลับไป

ประโยชน์

1. ประสิทธิภาพที่สอดคล้องกัน

ฐานข้อมูลใช้ดัชนีเพื่อค้นหาตำแหน่งเคอร์เซอร์โดยตรง:

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

การสืบค้นนี้รวดเร็วไม่ว่าจะอยู่ในตำแหน่งใดในชุดข้อมูล มันใช้การค้นหาดัชนี ไม่ใช่การสแกน

2. ผลลัพธ์ที่สอดคล้องกัน

เคอร์เซอร์มีความเสถียร หากข้อมูลเปลี่ยนแปลงระหว่างคำขอ คุณยังคงได้รับผลลัพธ์ที่สอดคล้องกัน การเพิ่มเรคคอร์ดใหม่ไม่ทำให้เกิดข้อมูลซ้ำหรือข้ามข้อมูล

3. ไม่มีการโจมตีการแบ่งหน้าลึก

ไคลเอนต์ไม่สามารถข้ามไปยังตำแหน่งที่กำหนดได้ พวกเขาต้องเลื่อนหน้าไปตามลำดับ ซึ่งจำกัดการใช้งานในทางที่ผิด

รูปแบบเคอร์เซอร์

เคอร์เซอร์มักจะเป็น JSON ที่เข้ารหัสแบบ base64:

// เคอร์เซอร์ที่ถอดรหัสแล้ว
{
  "id": "019b4132-70aa-764f-b315-e2803d882a24",
  "createdAt": "2026-03-13T10:30:00Z"
}

เคอร์เซอร์มีข้อมูลเพียงพอที่จะดำเนินการแบ่งหน้าต่อ สำหรับ Modern PetstoreAPI ซึ่งรวมถึง ID ทรัพยากรและฟิลด์การจัดเรียง

การแบ่งหน้าแบบคีย์เซ็ตสำหรับข้อมูลที่จัดเรียง

การแบ่งหน้าแบบคีย์เซ็ตเป็นรูปแบบหนึ่งของการแบ่งหน้าแบบเคอร์เซอร์สำหรับข้อมูลที่จัดเรียง

การทำงาน

แทนที่จะใช้เคอร์เซอร์แบบทึบ คุณใช้ค่าสุดท้ายจากหน้าก่อนหน้า:

คำขอ 1:

GET /pets?limit=20&sortBy=createdAt

การตอบกลับ 1:

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

คำขอ 2:

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

พารามิเตอร์ after ใช้ค่า createdAt ล่าสุดจากหน้าก่อนหน้า

การสืบค้น SQL

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

นี่มีประสิทธิภาพเพราะใช้ดัชนีบน created_at

เมื่อใดควรใช้การแบ่งหน้าแบบคีย์เซ็ต

Modern PetstoreAPI ใช้การแบ่งหน้าแบบเคอร์เซอร์เป็นค่าเริ่มต้น แต่รองรับการแบ่งหน้าแบบคีย์เซ็ตสำหรับข้อมูลอนุกรมเวลา

Modern PetstoreAPI ใช้การแบ่งหน้าอย่างไร

Modern PetstoreAPI ใช้การแบ่งหน้าแบบเคอร์เซอร์พร้อมลิงก์ HATEOAS

รูปแบบคำขอ

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

พารามิเตอร์:

รูปแบบการตอบกลับ

{
  "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"
  }
}

คุณสมบัติหลัก

1. เคอร์เซอร์แบบทึบ

เคอร์เซอร์ถูกเข้ารหัสแบบ base64 ไคลเอนต์ไม่แยกวิเคราะห์มัน

2. ลิงก์ HATEOAS

วัตถุ links มี URL พร้อมใช้งาน ไคลเอนต์ไม่จำเป็นต้องสร้าง URL การแบ่งหน้า

3. ธง hasMore

ระบุว่ามีผลลัพธ์เพิ่มเติมหรือไม่ ไคลเอนต์จะรู้ว่าเมื่อใดควรหยุดแบ่งหน้า

4. การตรวจสอบขีดจำกัด

ขีดจำกัดสูงสุดคือ 100 ป้องกันไคลเอนต์ร้องขอหน้าขนาดใหญ่เกินไป

ดู เอกสารการแบ่งหน้าของ Modern PetstoreAPI สำหรับรายละเอียดเพิ่มเติม

รูปแบบการตอบกลับการแบ่งหน้า

Modern PetstoreAPI ห่อหุ้มการตอบกลับที่มีการแบ่งหน้าด้วยโครงสร้างที่สอดคล้องกัน

ตัวห่อหุ้มคอลเลกชัน

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

ทำไมต้องห่อหุ้มคอลเลกชัน?

  1. ความสามารถในการขยาย - สามารถเพิ่มเมตาดาต้าได้โดยไม่ทำให้ไคลเอนต์เสียหาย
  2. ความสอดคล้อง - ปลายทางที่แบ่งหน้าทั้งหมดใช้รูปแบบเดียวกัน
  3. HATEOAS - ลิงก์นำทางไคลเอนต์ผ่านการแบ่งหน้า

ข้อมูลเมตาการแบ่งหน้า

"pagination": {
  "limit": 20,
  "hasMore": true,
  "nextCursor": "...",
  "totalCount": 1000  // ทางเลือก, คำนวณได้มีค่าใช้จ่ายสูง
}

totalCount เป็นทางเลือกเนื่องจากการคำนวณมันมีค่าใช้จ่ายสูงสำหรับชุดข้อมูลขนาดใหญ่ ไคลเอนต์ส่วนใหญ่ไม่จำเป็นต้องใช้มัน

การทดสอบการแบ่งหน้าด้วย Apidog

Apidog ช่วยให้คุณทดสอบพฤติกรรมการแบ่งหน้าได้อย่างครอบคลุม

สถานการณ์การทดสอบ

1. หน้าแรก

GET /pets?limit=20
คาดหวัง: 20 ผลลัพธ์, hasMore=true, nextCursor ปรากฏ

2. หน้าถัดไป

GET /pets?cursor={token}&limit=20
คาดหวัง: 20 ผลลัพธ์, hasMore=true/false, nextCursor ปรากฏ/ไม่ปรากฏ

3. หน้าสุดท้าย

GET /pets?cursor={lastToken}&limit=20
คาดหวัง: < 20 ผลลัพธ์, hasMore=false, ไม่มี nextCursor

4. ผลลัพธ์ว่างเปล่า

GET /pets?status=NONEXISTENT&limit=20
คาดหวัง: 0 ผลลัพธ์, hasMore=false, ไม่มี nextCursor

5. การตรวจสอบขีดจำกัด

GET /pets?limit=1000
คาดหวัง: 400 Bad Request (เกินขีดจำกัดสูงสุด)

การกำหนดค่าการทดสอบ Apidog

// ทดสอบ: โครงสร้างการแบ่งหน้า
pm.test("Response has pagination", () => {
  pm.expect(pm.response.json()).to.have.property('pagination');
  pm.expect(pm.response.json().pagination).to.have.property('hasMore');
});

// ทดสอบ: ลิงก์ 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');
  }
});

การเลือกกลยุทธ์การแบ่งหน้าที่เหมาะสม

กลยุทธ์ที่แตกต่างกันเหมาะสำหรับกรณีการใช้งานที่แตกต่างกัน

การแบ่งหน้าแบบออฟเซ็ต

ใช้เมื่อ:

อย่าใช้เมื่อ:

การแบ่งหน้าแบบเคอร์เซอร์

ใช้เมื่อ:

อย่าใช้เมื่อ:

การแบ่งหน้าแบบคีย์เซ็ต

ใช้เมื่อ:

อย่าใช้เมื่อ:

คำแนะนำของ Modern PetstoreAPI: ใช้การแบ่งหน้าแบบเคอร์เซอร์สำหรับ API สาธารณะและชุดข้อมูลขนาดใหญ่

สรุป

การแบ่งหน้าเป็นสิ่งสำคัญสำหรับ API ที่ส่งคืนชุดข้อมูลขนาดใหญ่ การแบ่งหน้าแบบออฟเซ็ตนั้นเรียบง่ายแต่ไม่สามารถขยายขนาดได้ การแบ่งหน้าแบบเคอร์เซอร์ให้ประสิทธิภาพที่สอดคล้องกันและผลลัพธ์ที่เชื่อถือได้สำหรับข้อมูลหลายล้านรายการ

Modern PetstoreAPI ใช้การแบ่งหน้าแบบเคอร์เซอร์ด้วยโทเค็นแบบทึบ, ลิงก์ HATEOAS, และเมตาดาต้าที่เหมาะสม การออกแบบนี้สามารถขยายขนาดได้อย่างมีประสิทธิภาพและมอบประสบการณ์ที่ดีเยี่ยมสำหรับนักพัฒนา

ทดสอบการใช้งานการแบ่งหน้าของคุณด้วย Apidog เพื่อให้แน่ใจว่ามันจัดการกรณีขอบ, ตรวจสอบขีดจำกัด, และคืนค่าผลลัพธ์ที่สอดคล้องกัน

ประเด็นสำคัญ:

button

คำถามที่พบบ่อย

ทำไมไม่คืนค่าผลลัพธ์ทั้งหมดโดยไม่ต้องแบ่งหน้า?

การคืนค่าข้อมูลหลายล้านรายการในการตอบกลับครั้งเดียวทำให้เกิดปัญหาหน่วยความจำ, การถ่ายโอนเครือข่ายช้า, และประสบการณ์ผู้ใช้ที่ไม่ดี การแบ่งหน้าเป็นสิ่งจำเป็นสำหรับชุดข้อมูลขนาดใหญ่

ไคลเอนต์สามารถข้ามไปยังหน้าใดหน้าหนึ่งด้วยการแบ่งหน้าแบบเคอร์เซอร์ได้หรือไม่?

ไม่ได้ การแบ่งหน้าแบบเคอร์เซอร์ต้องใช้การเข้าถึงตามลำดับ หากต้องการการเข้าถึงแบบสุ่ม ให้พิจารณาการแบ่งหน้าแบบออฟเซ็ตสำหรับชุดข้อมูลขนาดเล็ก หรือใช้การค้นหา/การกรองแทน

ฉันจะจัดการการแบ่งหน้ากับการกรองได้อย่างไร?

รวมพารามิเตอร์การกรองในคำขอการแบ่งหน้า: GET /pets?status=AVAILABLE&cursor={token}&limit=20 เคอร์เซอร์จะเข้ารหัสทั้งตำแหน่งและสถานะการกรอง

ฉันควรใส่จำนวนรวมทั้งหมดในการตอบกลับการแบ่งหน้าหรือไม่?

เฉพาะในกรณีที่ไคลเอนต์ต้องการและชุดข้อมูลของคุณมีขนาดเล็ก การคำนวณจำนวนรวมทั้งหมดมีค่าใช้จ่ายสูงสำหรับชุดข้อมูลขนาดใหญ่ (ต้องมีการสืบค้น COUNT แยกต่างหาก)

ฉันจะใช้การแบ่งหน้าแบบเคอร์เซอร์ใน SQL ได้อย่างไร?

ใช้เงื่อนไข WHERE ด้วยค่าเคอร์เซอร์: SELECT * FROM pets WHERE id > ? ORDER BY id LIMIT 20 ตรวจสอบให้แน่ใจว่าคุณมีดัชนีบนคอลัมน์ที่ใช้จัดเรียง

จะเกิดอะไรขึ้นถ้าโทเค็นเคอร์เซอร์ของฉันไม่ถูกต้อง?

คืนค่า 400 Bad Request พร้อมข้อความแสดงข้อผิดพลาด เคอร์เซอร์อาจไม่ถูกต้องได้หากข้อมูลถูกลบหรือสถานะการแบ่งหน้าหมดอายุ

เคอร์เซอร์ควรคงความถูกต้องนานแค่ไหน?

เคอร์เซอร์ของ Modern PetstoreAPI มีอายุใช้งานไม่จำกัดตราบใดที่ทรัพยากรอ้างอิงยังคงมีอยู่ API บางแห่งจะให้เคอร์เซอร์หมดอายุหลังจาก 24 ชั่วโมง

ฉันสามารถใช้การแบ่งหน้าแบบเคอร์เซอร์กับฟิลด์การจัดเรียงหลายฟิลด์ได้หรือไม่?

ได้ แต่เคอร์เซอร์ต้องเข้ารหัสฟิลด์การจัดเรียงทั้งหมด สิ่งนี้ทำให้เคอร์เซอร์มีความซับซ้อนมากขึ้น ลองใช้คีย์การจัดเรียงแบบรวมเพียงคีย์เดียวแทน

ฝึกการออกแบบ API แบบ Design-first ใน Apidog

ค้นพบวิธีที่ง่ายขึ้นในการสร้างและใช้ API

ออกแบบ API Pagination อย่างไรให้รองรับข้อมูลจำนวนมหาศาล