ข้ามไปยังเนื้อหา

Pagination

Pagination คือการคืน collection ออกมาเป็นชิ้น (slice) มีกลยุทธ์ที่นิยมใช้กันสองแบบ และการเลือกที่ถูกต้องขึ้นอยู่กับว่าข้อมูลของคุณใหญ่และเปลี่ยนแปลงบ่อยแค่ไหน

client ขอหน้าตามตำแหน่ง: ?page=3&limit=20 (หรือ ?offset=40&limit=20) เรียบง่าย กระโดดไปหน้าไหนก็ได้ และแสดงจำนวนรวมได้ง่าย

GET /articles?page=2&limit=20

จุดอ่อนมีสองข้อ: offset ที่ลึกจะช้า (database ต้องข้ามทุกแถวที่อยู่ก่อนหน้า) และ การ insert จะทำให้ window เลื่อน — ถ้ามีแถวเพิ่มเข้ามาระหว่างที่คุณกำลังเปิดหน้าอยู่ คุณอาจเห็นข้อมูลซ้ำหรือข้ามรายการไป

client ส่ง cursor แบบ opaque ที่บอกตำแหน่งที่หน้าก่อนหน้าจบลง: ?limit=20&cursor=eyJpZCI6MTQwfQ server จะคืน slice ถัดไปพร้อม cursor สำหรับหน้าหลังจากนั้น

flowchart LR
  A[GET /articles?limit=20] --> B[20 items + nextCursor]
  B --> C[GET /articles?limit=20&cursor=...]
  C --> D[next 20 + nextCursor]
แต่ละหน้าจะส่ง cursor สำหรับหน้าถัดไปกลับมาด้วย

cursor มีความเสถียรเมื่อมีการ insert และเร็วไม่ว่าจะอยู่ลึกแค่ไหน แต่คุณจะกระโดดไปหน้าไหนก็ได้ตามใจไม่ได้ และการนับจำนวนรวมให้แม่นยำก็ทำได้ยากกว่า ถึงอย่างนั้นก็ยังเป็นค่าเริ่มต้นที่ดีกว่าสำหรับ collection ขนาดใหญ่หรือเปลี่ยนแปลงบ่อย (feed, log, search)

หนึ่งหน้าควรถูกห่อไว้ใน envelope ที่มี data พร้อมกับ metadata ของการแบ่งหน้าและ links:

JavaScript
Pagination Styleข้อดีข้อเสียเหมาะกับ
Offset (?page=2&limit=20)ง่าย, random accessunstable เมื่อ data เปลี่ยนAdmin table, fixed dataset
Cursor (?after=cursor123)stable, performance ดีกับ large datasetไม่รู้ total, ข้ามหน้าไม่ได้Feed, infinite scroll
Keyset (?after_id=42)เร็ว, stableต้อง unique sortable keyLarge sorted collection

Offset Pagination สำหรับ Real-time Feed อาการ:

  • feed ที่ข้อมูลเพิ่มบ่อย — หน้า 2 เลื่อนเป็นหน้า 3 เพราะข้อมูลใหม่เข้า
  • user เห็น item ซ้ำหรือ item หายระหว่าง paginate
  • ใช้ cursor pagination สำหรับ real-time data

ไม่มี Pagination Default อาการ:

  • GET /users return ทุก record โดยไม่จำกัด
  • database และ memory ระเบิดเมื่อ user มีล้านคน
  • ตั้ง default limit (limit=20) และ max limit (limit=100) เสมอ

💡 ตัวอย่างจากของจริง

GitHub API:

  • cursor pagination ผ่าน Link header: <https://api.github.com/repos?page=2>; rel="next"
  • default 30, max 100 items per page

Stripe API:

  • cursor pagination: GET /charges?starting_after=ch_xxx&limit=20
  • has_more: true บอก client ว่ามีข้อมูลเพิ่ม
  • cursor-based เพราะ charge เพิ่มตลอดเวลา
จุดอ่อนหลักของ offset pagination บนข้อมูลขนาดใหญ่ที่เปลี่ยนแปลงบ่อยคืออะไร?
cursor แทนอะไร?
กลยุทธ์ใดเป็นค่าเริ่มต้นที่ดีกว่าสำหรับ feed แบบไม่สิ้นสุด?