

หน้ารายการที่มีข้อมูลมากมักเริ่มจากแบ่งด้วย LIMIT และ OFFSET เปิดหน้าแรกแล้วข้ามจำนวนรายการไปหน้าถัดไป วิธีนี้อธิบายกับเลขหน้าได้ง่าย แต่เมื่อข้อมูลมีการเพิ่มหรือลบระหว่างที่คนกำลังเปิดแต่ละหน้า ขอบเขตที่เคยอยู่ตรงลำดับหนึ่งก็อาจเลื่อนไปแล้ว
อาการที่เห็นอาจเป็นรายการเดิมปรากฏซ้ำหรือบางรายการถูกข้าม ทั้งที่ query แต่ละครั้งทำงานสำเร็จ และถ้าเรียงด้วย field ที่มีค่าซ้ำโดยไม่มีตัวตัดสินเพิ่ม ลำดับภายในกลุ่มเดียวกันก็อาจไม่แน่นอนตั้งแต่แรกด้วย
ก่อนเปลี่ยนวิธีแบ่งหน้า จึงควรกำหนดความหมายของรายการก่อนครับ ว่าต้องการชุดข้อมูล ณ จุดเวลาเดียวกัน หรือยอมอ่านรายการที่ยังเปลี่ยนอยู่ได้ แล้วค่อยเลือกลำดับกับขอบเขตที่ระบบรักษาให้ได้จริง
PostgreSQL ไม่รับรองลำดับแถวโดยไม่มี ORDER BY และการใช้ LIMIT หรือ OFFSET ยังมีผลต่อการเลือกแผนได้ จึงไม่ควรอาศัยลำดับที่บังเอิญเห็นตอนตารางยังเล็กมาเป็นข้อตกลงของ API
ถ้าเรียง created_at ล่าสุดก่อน แต่หลายรายการมีเวลาเท่ากัน ควรมีตัวตัดสินที่เหมาะสมเพิ่ม เช่น id ที่ไม่ซ้ำ เพื่อให้ลำดับรวมระบุได้ชัดว่าใครมาก่อนหลัง ไม่ปล่อยให้ฐานข้อมูลเลือกลำดับภายในกลุ่มเองทุกครั้ง

ตัวอย่างสมมติที่ใช้ created_at กับ id ต้องกำหนดชนิด ความหมาย และการมีค่าของทั้งคู่ให้ชัด หากอนุญาต NULL หรือผู้ใช้เปลี่ยนเวลาย้อนหลังได้ ต้องออกแบบพฤติกรรมส่วนนี้เพิ่ม ไม่คิดว่าเพียงต่อ id ท้าย ORDER BY จะจัดการการเปลี่ยนข้อมูลทุกแบบแทนเรา
ลำดับเดียวกันต้องถูกใช้ทั้งหน้าแรก หน้าถัดไป และการสร้าง cursor หาก API ใช้คนละทิศหรือปัดความละเอียดเวลาไม่เหมือนกัน ขอบเขตที่ส่งกลับมาอาจไม่ตรงกับแถวสุดท้ายที่คนเพิ่งเห็น
ลองนึกถึงรายการเรียงล่าสุดก่อน เมื่ออ่านหน้าแรกเสร็จมีรายการใหม่เข้าด้านหน้า ตำแหน่งของข้อมูลเก่าจะขยับลง หากหน้าถัดไปข้ามจำนวนเดิมในชุดใหม่ ก็อาจกลับมาอ่านบางรายการที่เห็นแล้วได้
หากมีการลบด้านหน้า ขอบเขตก็เลื่อนอีกแบบจนรายการบางส่วนอาจถูกข้าม ปัญหานี้เกิดจากการอ้างตำแหน่งในชุดที่เปลี่ยน ไม่ได้แปลว่า SQL คำนวณ OFFSET ผิด
ด้านภาระงาน PostgreSQL ระบุว่าแถวที่ข้ามด้วย OFFSET ยังต้องถูกคำนวณภายใน server จึงมีโอกาสไม่มีประสิทธิภาพเมื่อข้ามมาก แต่จำนวนที่เริ่มเป็นปัญหาต้องวัดจาก query กับข้อมูลจริง ไม่ใช้เกณฑ์จำนวนหน้าตายตัวเป็นคำตอบทุกระบบ
OFFSET ยังมีประโยชน์ในงานที่ต้องกระโดดเลขหน้าหรือชุดข้อมูลมีขอบเขตเหมาะสม เพียงต้องบอกความแน่นอนของข้อมูลกับต้นทุนให้ตรง ไม่เปลี่ยนเป็น cursor ทุกงานโดยไม่ดูว่าคนต้องการนำทางแบบใด
Keyset pagination ใช้ค่าของจุดที่อ่านถึงมาเป็นขอบเขต แทนการนับข้ามจากต้นชุด ตัวอย่างสมมติด้านล่างกำหนด created_at กับ id เป็น NOT NULL และใช้ลำดับลดลงทั้งสองคอลัมน์ โดยรอบถัดไปส่งค่าคู่นี้จากรายการสุดท้ายกลับมา
SELECT id, created_at, title
FROM articles
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT $3;
นี่เป็น fragment สำหรับ schema สมมติและ parameter ที่ไลบรารีผูกชนิดให้เหมาะสม ไม่ใช่ query ที่นำไปใช้กับตารางเว็บจริงโดยตรง ในระบบจริงยังต้องมีเงื่อนไขผู้ใช้ โครงการ และตัวกรองเดิมครบอยู่ด้วย

การเปรียบเทียบ tuple ต้องตรงกับรูปการเรียง หากเปลี่ยนทิศบางคอลัมน์หรืออนุญาต NULL ต้องทบทวนเงื่อนไข ไม่คัดลอกเครื่องหมายเปรียบเทียบจากตัวอย่างนี้ไปใช้ทุก sort order โดยไม่ตรวจ
ข้อดีคือข้อมูลใหม่ที่เข้ามาก่อนจุดเดิมไม่ทำให้ขอบเขตเลื่อนจากการนับแบบเดียวกับ OFFSET แต่ cursor ยังไม่สร้าง snapshot ให้เอง ถ้ารายการเปลี่ยนค่าที่ใช้เรียงหรือถูกลบระหว่างหน้า ผลก็ยังขึ้นกับนโยบายข้อมูลที่กำลังเปลี่ยนอยู่
ข้อมูลใน cursor ควรรักษาความละเอียดของค่าที่ใช้เรียง โดยเฉพาะเวลา หากแปลงไปเป็นข้อความสำหรับคนอ่านแล้วตัดเศษส่วนทิ้งก่อนใช้เปรียบเทียบ อาจสร้างขอบเขตที่ต่างจากแถวจริงได้
ควรผูกกับลำดับและตัวกรองที่ใช้สร้างมันด้วย หากเปลี่ยนหมวดหรือช่วงวันที่แล้วนำ cursor เดิมมาต่อ อาจได้ผลที่ความหมายไม่ตรงหน้าจอ การมีรุ่นรูปแบบ cursor ช่วยให้จัดการการเปลี่ยนข้อตกลงในอนาคตได้ชัดกว่าเดาจาก payload
Cursor เป็นข้อมูลจากผู้เรียกจึงยังต้องตรวจรูปแบบกับสิทธิ์ทุกครั้ง การเข้ารหัสแบบ base64 ไม่ใช่การให้สิทธิ์หรือการป้องกันการแก้ค่าโดยตัวมันเอง และไม่ควรถูกใช้ให้ข้ามเงื่อนไขการเข้าถึงแถวที่ระบบกำหนดไว้
ผลตอบกลับก็ควรตรงกับสิ่งที่หน้าจอต้องใช้ หากต้องรู้เพียงว่ามีหน้าถัดไปหรือไม่ อาจประเมินจากการขอเพิ่มอีกหนึ่งรายการตามขอบเขตงานได้ ส่วนจำนวนรวมทั้งหมดเป็นอีก query และอีกต้นทุนที่ต้องเลือกว่าจะคำนวณเมื่อไร ไม่ผูกสองความต้องการเป็นเรื่องเดียวโดยอัตโนมัติ
ฝั่ง query ควรใช้ parameter binding ตามปกติ จำกัดขนาดหน้าที่รองรับ และไม่ให้ cursor ส่งชื่อคอลัมน์หรือคำสั่ง SQL ที่นำไปประกอบโดยไม่ควบคุม ส่วน index ที่เหมาะต้องประเมินร่วมกับ filter และการเรียงของงานจริง
งานส่งออกข้อมูลหรือรายงานบางชนิดอาจต้องอ่านชุดที่สอดคล้อง ณ จุดเวลาเดียวกัน การใช้ cursor เพียงอย่างเดียวไม่ได้รับรองความสอดคล้องระดับนั้น ต้องเลือกขอบเขต snapshot หรือวิธีตรึงชุดข้อมูลตามความสามารถและภาระของระบบ
ส่วนหน้ารายการที่ยอมรับข้อมูลเปลี่ยนได้ อาจแสดงว่ามีข้อมูลใหม่และให้คนเลือกเริ่มจากต้นอีกครั้ง แทนการแทรกจนบริบทที่กำลังอ่านขยับไปมา กติกานี้เป็นการออกแบบประสบการณ์ใช้งานร่วมกับวิธี query
ทดสอบด้วยชุดที่มีค่าลำดับซ้ำ เพิ่มรายการก่อนขอบเขต ลบรายการ และเปลี่ยนตัวกรองระหว่างหน้า แล้วดูรายการที่ได้รับตามรหัสจริง ไม่ตรวจเพียงจำนวนแถวต่อหน้าซึ่งอาจครบแต่มีรายการซ้ำอยู่ข้างใน
การแบ่งหน้าต้องมีลำดับที่ระบุได้ชัดและข้อตกลงเมื่อข้อมูลเปลี่ยน OFFSET อ้างตำแหน่ง ส่วน cursor อ้างค่าที่ใช้เรียงซึ่งช่วยรักษาขอบเขตอีกแบบ เมื่อผูกความละเอียด ตัวกรอง และสิทธิ์ให้ตรงกัน แล้วแยกจากความต้องการ snapshot เราจะเลือกวิธีนำทางได้ตามงานและอธิบายผลที่คนเห็นได้ตรงความหมาย