สรุปย่อ
สำหรับชุดข้อมูลขนาดใหญ่ ให้ใช้การแบ่งหน้าแบบเคอร์เซอร์ (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 ใช้การแบ่งหน้าแบบเคอร์เซอร์ที่สามารถขยายขนาดได้ถึงหลายล้านรายการพร้อมประสิทธิภาพที่สอดคล้องกัน
ในคู่มือนี้ คุณจะได้เรียนรู้ว่าทำไมการแบ่งหน้าแบบออฟเซ็ตถึงล้มเหลว, การแบ่งหน้าแบบเคอร์เซอร์ทำงานอย่างไร, และ 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 คุณต้องจัดการกับมัน การสืบค้นการแบ่งหน้าลึกมีค่าใช้จ่ายสูงและสามารถนำไปใช้โจมตีแบบปฏิเสธการให้บริการได้
เมื่อใดที่การแบ่งหน้าแบบออฟเซ็ตเป็นที่ยอมรับ
การแบ่งหน้าแบบออฟเซ็ตทำงานได้ดีสำหรับ:
- ชุดข้อมูลขนาดเล็ก (น้อยกว่า 10,000 รายการ)
- API ภายในที่มีการใช้งานที่ควบคุมได้
- ส่วนต่อประสานสำหรับผู้ดูแลระบบที่ผู้ใช้จะไม่แบ่งหน้าลึก
- ข้อมูลที่มีการเปลี่ยนแปลงไม่บ่อยนัก
สำหรับ 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
เมื่อใดควรใช้การแบ่งหน้าแบบคีย์เซ็ต
- ข้อมูลมีการจัดเรียงตามธรรมชาติ (ตามเวลา, ID, ฯลฯ)
- ไคลเอนต์จำเป็นต้องเข้าใจคีย์การแบ่งหน้า
- คุณต้องการการแบ่งหน้าที่โปร่งใส (ไม่ใช่เคอร์เซอร์แบบทึบ)
Modern PetstoreAPI ใช้การแบ่งหน้าแบบเคอร์เซอร์เป็นค่าเริ่มต้น แต่รองรับการแบ่งหน้าแบบคีย์เซ็ตสำหรับข้อมูลอนุกรมเวลา
Modern PetstoreAPI ใช้การแบ่งหน้าอย่างไร
Modern PetstoreAPI ใช้การแบ่งหน้าแบบเคอร์เซอร์พร้อมลิงก์ HATEOAS
รูปแบบคำขอ
GET /pets?limit=20
GET /pets?cursor={token}&limit=20
พารามิเตอร์:
limit- จำนวนผลลัพธ์ต่อหน้า (ค่าเริ่มต้น: 20, สูงสุด: 100)cursor- โทเค็นการแบ่งหน้าแบบทึบจากการตอบกลับก่อนหน้า
รูปแบบการตอบกลับ
{
"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": {...}
}
ทำไมต้องห่อหุ้มคอลเลกชัน?
- ความสามารถในการขยาย - สามารถเพิ่มเมตาดาต้าได้โดยไม่ทำให้ไคลเอนต์เสียหาย
- ความสอดคล้อง - ปลายทางที่แบ่งหน้าทั้งหมดใช้รูปแบบเดียวกัน
- 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');
}
});
การเลือกกลยุทธ์การแบ่งหน้าที่เหมาะสม
กลยุทธ์ที่แตกต่างกันเหมาะสำหรับกรณีการใช้งานที่แตกต่างกัน
การแบ่งหน้าแบบออฟเซ็ต
ใช้เมื่อ:
- ชุดข้อมูลมีขนาดเล็ก (น้อยกว่า 10,000 รายการ)
- ผู้ใช้ต้องการการเข้าถึงแบบสุ่ม (ข้ามไปหน้า 50)
- ข้อมูลมีการเปลี่ยนแปลงไม่บ่อยนัก
- API ภายในที่มีการใช้งานที่ควบคุมได้
อย่าใช้เมื่อ:
- ชุดข้อมูลมีขนาดใหญ่ (มากกว่า 100,000 รายการ)
- ประสิทธิภาพเป็นสิ่งสำคัญ
- ข้อมูลมีการเปลี่ยนแปลงบ่อยครั้ง
การแบ่งหน้าแบบเคอร์เซอร์
ใช้เมื่อ:
- ชุดข้อมูลมีขนาดใหญ่
- ประสิทธิภาพเป็นสิ่งสำคัญ
- ข้อมูลมีการเปลี่ยนแปลงบ่อยครั้ง
- การเข้าถึงตามลำดับเพียงพอ
อย่าใช้เมื่อ:
- ผู้ใช้ต้องการการเข้าถึงแบบสุ่ม
- ความซับซ้อนของเคอร์เซอร์เป็นข้อกังวล
การแบ่งหน้าแบบคีย์เซ็ต
ใช้เมื่อ:
- ข้อมูลมีการจัดเรียงตามธรรมชาติ
- ต้องการการแบ่งหน้าที่โปร่งใส
- ประสิทธิภาพเป็นสิ่งสำคัญ
อย่าใช้เมื่อ:
- ลำดับการจัดเรียงซับซ้อน
- ต้องการฟิลด์การจัดเรียงหลายฟิลด์
คำแนะนำของ Modern PetstoreAPI: ใช้การแบ่งหน้าแบบเคอร์เซอร์สำหรับ API สาธารณะและชุดข้อมูลขนาดใหญ่
สรุป
การแบ่งหน้าเป็นสิ่งสำคัญสำหรับ API ที่ส่งคืนชุดข้อมูลขนาดใหญ่ การแบ่งหน้าแบบออฟเซ็ตนั้นเรียบง่ายแต่ไม่สามารถขยายขนาดได้ การแบ่งหน้าแบบเคอร์เซอร์ให้ประสิทธิภาพที่สอดคล้องกันและผลลัพธ์ที่เชื่อถือได้สำหรับข้อมูลหลายล้านรายการ
Modern PetstoreAPI ใช้การแบ่งหน้าแบบเคอร์เซอร์ด้วยโทเค็นแบบทึบ, ลิงก์ HATEOAS, และเมตาดาต้าที่เหมาะสม การออกแบบนี้สามารถขยายขนาดได้อย่างมีประสิทธิภาพและมอบประสบการณ์ที่ดีเยี่ยมสำหรับนักพัฒนา
ทดสอบการใช้งานการแบ่งหน้าของคุณด้วย Apidog เพื่อให้แน่ใจว่ามันจัดการกรณีขอบ, ตรวจสอบขีดจำกัด, และคืนค่าผลลัพธ์ที่สอดคล้องกัน
ประเด็นสำคัญ:
- หลีกเลี่ยงการแบ่งหน้าแบบออฟเซ็ตสำหรับชุดข้อมูลขนาดใหญ่
- ใช้การแบ่งหน้าแบบเคอร์เซอร์เพื่อความสามารถในการขยายขนาด
- ห่อหุ้มคอลเลกชันด้วยเมตาดาต้าและลิงก์
- ทดสอบการแบ่งหน้าอย่างละเอียดด้วย Apidog
- ปฏิบัติตามรูปแบบการแบ่งหน้าของ Modern PetstoreAPI
คำถามที่พบบ่อย
ทำไมไม่คืนค่าผลลัพธ์ทั้งหมดโดยไม่ต้องแบ่งหน้า?
การคืนค่าข้อมูลหลายล้านรายการในการตอบกลับครั้งเดียวทำให้เกิดปัญหาหน่วยความจำ, การถ่ายโอนเครือข่ายช้า, และประสบการณ์ผู้ใช้ที่ไม่ดี การแบ่งหน้าเป็นสิ่งจำเป็นสำหรับชุดข้อมูลขนาดใหญ่
ไคลเอนต์สามารถข้ามไปยังหน้าใดหน้าหนึ่งด้วยการแบ่งหน้าแบบเคอร์เซอร์ได้หรือไม่?
ไม่ได้ การแบ่งหน้าแบบเคอร์เซอร์ต้องใช้การเข้าถึงตามลำดับ หากต้องการการเข้าถึงแบบสุ่ม ให้พิจารณาการแบ่งหน้าแบบออฟเซ็ตสำหรับชุดข้อมูลขนาดเล็ก หรือใช้การค้นหา/การกรองแทน
ฉันจะจัดการการแบ่งหน้ากับการกรองได้อย่างไร?
รวมพารามิเตอร์การกรองในคำขอการแบ่งหน้า: 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 ชั่วโมง
ฉันสามารถใช้การแบ่งหน้าแบบเคอร์เซอร์กับฟิลด์การจัดเรียงหลายฟิลด์ได้หรือไม่?
ได้ แต่เคอร์เซอร์ต้องเข้ารหัสฟิลด์การจัดเรียงทั้งหมด สิ่งนี้ทำให้เคอร์เซอร์มีความซับซ้อนมากขึ้น ลองใช้คีย์การจัดเรียงแบบรวมเพียงคีย์เดียวแทน
