กลับไปหน้าบทความ
#Keyset Pagination#Node.js#Database#API#PostgreSQL

Keyset Pagination สำหรับ Feed และ API ที่โตขึ้น: เร็วกว่า OFFSET อย่างไร

Keyset Pagination เป็นแนวทางแบ่งหน้าที่เหมาะกับ feed และ API ที่ข้อมูลเติบโตต่อเนื่อง เพราะช่วยลดต้นทุนจากการใช้ OFFSET ในหน้าลึก ๆ ได้อย่างชัดเจน บทความนี้อธิบายหลักการทำงาน ข้อดี ข้อควรระวัง และแนวทางออกแบบ API ใน Node

28 กรกฎาคม 2569อ่านประมาณ 2 นาที

แชร์บทความ

Keyset Pagination สำหรับ Feed และ API ที่โตขึ้น: เร็วกว่า OFFSET อย่างไร

Keyset Pagination สำหรับ Feed และ API ที่โตขึ้น: เร็วกว่า OFFSET อย่างไร

เมื่อระบบเริ่มมีข้อมูลมากขึ้น การแบ่งหน้าข้อมูลแบบเดิมที่ใช้ page และ limit อาจเริ่มกลายเป็นคอขวดโดยไม่รู้ตัว โดยเฉพาะใน feed, timeline, notification หรือ API ที่มีการเรียกดูข้อมูลต่อเนื่องเป็นจำนวนมาก

ตัวอย่าง endpoint ที่หลายคนคุ้นเคยคือ:

GET /posts?page=1000&limit=20

ซึ่งมักถูกแปลงเป็น query ลักษณะนี้:

OFFSET 19980 LIMIT 20

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

ทำไม OFFSET ถึงช้าลงเมื่อ page ลึกขึ้น

ปัญหาหลักของ OFFSET คือฐานข้อมูลยังคงต้องประมวลผลข้อมูลตามเงื่อนไข ORDER BY ก่อน จากนั้นจึงค่อยนับและทิ้งแถวตามจำนวนที่ระบุใน OFFSET แล้วจึงคืนเฉพาะแถวที่ต้องการจริง ๆ ตาม LIMIT

ตัวอย่างเช่น:

SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 200000;

คำสั่งนี้เปรียบได้กับการบอกฐานข้อมูลว่า

ช่วยเรียงโพสต์ล่าสุดทั้งหมด แล้วทิ้ง 200,000 รายการแรก ก่อนส่งมาให้ฉัน 20 รายการ

แม้จะมี index อยู่แล้ว แต่ฐานข้อมูลก็ยังมีต้นทุนในการเดินผ่านข้อมูลจำนวนมาก และถ้า ORDER BY ไม่สอดคล้องกับ index ก็อาจยิ่งหนักขึ้นเพราะมีค่าใช้จ่ายในการ sort เพิ่มอีก

ดังนั้นยิ่ง page ลึก ต้นทุนจากการข้ามข้อมูลที่ไม่ได้ใช้ก็ยิ่งสูงตามไปด้วย

Keyset Pagination คืออะไร

Keyset Pagination หรือที่หลายคนเรียกว่า cursor-based pagination คือการเปลี่ยนวิธีคิดจากการถามว่า

  • ขอ page ที่ 10,001

มาเป็น

  • ขอ 20 รายการถัดจากรายการล่าสุดที่ผู้ใช้เห็น

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

ตัวอย่างรอบแรก:

SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC, id DESC
LIMIT 20;

เมื่อ API ส่งผลลัพธ์กลับไป ก็ส่ง cursor กลับไปด้วย เช่น:

  • last_seen_created_at = 2026-07-01T10:00:00Z
  • last_seen_id = 8742

จากนั้นรอบถัดไปจึง query แบบนี้:

SELECT id, title, created_at
FROM posts
WHERE (created_at, id) < ('2026-07-01T10:00:00Z', 8742)
ORDER BY created_at DESC, id DESC
LIMIT 20;

แนวคิดสำคัญคือฐานข้อมูลไม่ต้องเสียเวลาข้ามแถวจำนวนมหาศาลอีกต่อไป แต่สามารถเริ่มค้นหาจากตำแหน่งหลัง cursor ได้ตรงกว่าและมีประสิทธิภาพกว่า

ทำไม Keyset Pagination จึงเหมาะกับ Feed และ API ที่โตต่อเนื่อง

Keyset Pagination เหมาะมากกับงานที่ผู้ใช้เลื่อนดูข้อมูลไปเรื่อย ๆ เช่น:

  • timeline
  • activity feed
  • transaction list
  • notification
  • audit log
  • infinite scroll

ข้อดีสำคัญคือประสิทธิภาพจะค่อนข้างสม่ำเสมอ แม้ข้อมูลจะเพิ่มขึ้นเรื่อย ๆ เพราะระบบไม่ต้องจ่ายต้นทุนกับการ skip ข้อมูลจำนวนมากแบบ OFFSET

นอกจากนี้ผลลัพธ์ยัง predictable กว่าในหลายกรณี เพราะการอ้างอิงตำแหน่งด้วย cursor ทำให้การดึงข้อมูลต่อจากจุดเดิมมีความชัดเจนกว่าเลขหน้า

เงื่อนไขสำคัญ: ORDER BY ต้อง stable

แม้ Keyset Pagination จะเร็ว แต่จะทำงานได้ดีจริงก็ต่อเมื่อเงื่อนไขการเรียงลำดับมีความ stable

ถ้าใช้เพียง:

ORDER BY created_at DESC

และมีหลายแถวที่ created_at เท่ากัน อาจเกิดปัญหาได้ เช่น:

  • บางรายการซ้ำระหว่างหน้า
  • บางรายการหายไป

แนวทางที่ปลอดภัยคือเพิ่ม tie-breaker ที่ unique เช่น id

ORDER BY created_at DESC, id DESC

และ cursor ก็ควรเก็บทั้ง created_at และ id เพื่อให้การเลื่อนไปหน้าถัดไปแม่นยำ

Index ที่ดีคือหัวใจของความเร็ว

Keyset Pagination จะให้ผลดีมากเมื่อมี index ที่สอดคล้องกับ query

ถ้าใช้ query แบบนี้:

ORDER BY created_at DESC, id DESC

ก็ควรมี composite index ที่เรียงตามลำดับเดียวกัน เช่นใน PostgreSQL:

CREATE INDEX idx_posts_feed
ON posts (created_at DESC, id DESC);

สำหรับ MySQL ก็ใช้หลักคิดเดียวกัน คือออกแบบ index ให้ช่วยทั้งการ filter หลัง cursor และการเรียงลำดับในคำสั่งเดียวกัน ไม่ใช่แยก index คนละชุดแล้วหวังว่า optimizer จะเลือกได้เหมาะเสมอไป

ตัวอย่าง workflow ใน Node.js API

ถ้ากำลังออกแบบ API ใหม่ใน Node.js วิธีทำงานที่ใช้ได้จริงมักเป็นดังนี้:

  1. endpoint รับ limit และ cursor แทน page number
  2. decode cursor ออกมาเป็น created_at และ id
  3. query ด้วย WHERE หลัง cursor พร้อม ORDER BY ที่ stable
  4. ดึงข้อมูล limit + 1 แถว เพื่อเช็กว่ายังมีหน้าถัดไปหรือไม่
  5. ส่ง nextCursor จากแถวสุดท้ายกลับไป

ตัวอย่าง response:

items = [...]
nextCursor = base64(created_at + id)

ฝั่ง client ไม่จำเป็นต้องรู้ว่า offset เท่าไร เพียงแค่เก็บ cursor แล้วส่งกลับมาตอนกดโหลดเพิ่มหรือเลื่อนหน้าต่อ

ข้อควรระวังในการออกแบบ cursor

cursor ไม่ควรเป็นค่าที่ผู้ใช้แก้เองได้ง่ายเกินไป เพราะอาจนำไปสู่การส่งค่าที่ผิดรูปแบบ หรือในบางกรณีอาจกระทบกับ business rule ได้

แนวทางที่เหมาะสมคือ:

  • encode cursor เป็น opaque token
  • ใช้รูปแบบเช่น base64 ของ JSON
  • validate ทุกครั้งก่อนใช้งาน
  • หากมีข้อมูลสำคัญ ควร sign cursor ด้วย HMAC เพื่อป้องกันการปลอมแปลง

แนวทางนี้ช่วยให้ API ปลอดภัยและควบคุมพฤติกรรมการเลื่อนข้อมูลได้ดีขึ้น

เมื่อไรที่ OFFSET ยังเหมาะกว่า

แม้ Keyset Pagination จะเหมาะกับ feed อย่างมาก แต่ก็ไม่ได้แปลว่าควรใช้แทน OFFSET ทุกกรณี

ตัวอย่างที่ OFFSET ยังเหมาะกว่า คือหน้าจอที่ผู้ใช้ต้องการกระโดดไปยังหน้าแบบระบุเลขได้ทันที เช่น:

  • ตาราง admin
  • รายงานหลังบ้าน
  • ชุดข้อมูลที่ไม่ใหญ่มาก

เหตุผลคือ Keyset Pagination ถูกออกแบบมาสำหรับการไหลไปข้างหน้า หรือย้อนกลับตาม cursor ไม่ได้เหมาะกับ random access แบบ "ไปหน้า 100" โดยตรง

หลายระบบจึงใช้แบบผสมกัน เช่น:

  • feed ใช้ keyset
  • admin report หน้าเล็ก ๆ ใช้ OFFSET
  • search results ใช้ cursor ของ search engine โดยเฉพาะ

การเลือกใช้จึงควรขึ้นกับลักษณะของหน้าจอและพฤติกรรมผู้ใช้ ไม่ใช่ยึดวิธีเดียวกับทุกกรณี

สรุป

OFFSET เป็นวิธีที่เข้าใจง่ายและเริ่มต้นได้เร็ว แต่เมื่อหน้าลึกขึ้น ต้นทุนจะสูงขึ้นตามจำนวนแถวที่ต้องข้าม ทำให้ประสิทธิภาพลดลงอย่างชัดเจนในระบบที่ข้อมูลเติบโตต่อเนื่อง

ในทางกลับกัน Keyset Pagination ใช้แนวคิด last_seen + WHERE + ORDER BY + LIMIT เพื่อดึงข้อมูลต่อจากจุดล่าสุดได้ตรงกว่า เร็วกว่า และคาดเดาพฤติกรรมได้ดีกว่าสำหรับ feed และ API ที่ใช้งานจริงในระยะยาว

หากกำลังออกแบบ Node.js backend ใหม่ และหน้าจอเป็นลักษณะ "โหลดเพิ่ม" หรือ infinite scroll มากกว่าการ "กระโดดไปหน้า 100" การเริ่มต้นด้วย Keyset Pagination มักเป็นตัวเลือกที่เหมาะกว่าตั้งแต่แรก