Cursor Pagination ในการพัฒนามือถือ — คืออะไร หลักการ และการใช้งาน

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-03-11 เวลาอ่าน: 9 นาที

Cursor Pagination เป็นวิธีการโหลดข้อมูลแบบแบ่งหน้าที่ใช้เคอร์เซอร์เฉพาะเพื่อนำทางผ่านชุดเรกคอร์ดที่เรียงลำดับ ตาม GraphQL Specification (2025) การแบ่งหน้าแบบเคอร์เซอร์เป็นมาตรฐานที่แนะนำสำหรับ API ที่ทำงานกับข้อมูลไดนามิก Cursor pagination กำจัดข้อเสียหลักของแนวทาง Offset: ความไม่เสถียรระหว่างการแทรกและการลดลงของประสิทธิภาพบน offset ขนาดใหญ่

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

  • Cursor Pagination เป็นวิธีการแบ่งหน้าที่แต่ละเรกคอร์ดมีตัวระบุเคอร์เซอร์เฉพาะสำหรับการนำทาง
  • เคอร์เซอร์ คือเครื่องหมายตำแหน่งเฉพาะในชุดข้อมูล (โดยปกติคือ ID, UUID, timestamp) ที่ไม่เปลี่ยนแปลงเมื่อมีการแทรก
  • ความเสถียร — เรกคอร์ดใหม่ที่เพิ่มระหว่างคำขอไม่เลื่อนเคอร์เซอร์ กำจัดการซ้ำซ้อนและการขาดหาย
  • ประสิทธิภาพ — คำสั่ง WHERE id > cursor ใช้ดัชนีอย่างมีประสิทธิภาพโดยไม่สูญเสียความเร็วบนชุดข้อมูลขนาดใหญ่
  • ข้อจำกัด — การแบ่งหน้าแบบเคอร์เซอร์ไม่รองรับการนำทางตามหมายเลขหน้า (ไม่สามารถข้ามไปหน้าที่ 5 ได้)

Cursor Pagination คืออะไร?

Cursor Pagination เป็นวิธีการโหลดแบบแบ่งหน้าที่เซิร์ฟเวอร์ส่งคืนตัวชี้พิเศษ — เคอร์เซอร์ — พร้อมกับข้อมูล ไคลเอ็นต์ใช้เคอร์เซอร์นี้ในคำขอถัดไปเพื่อรับชุดถัดไปของเรกคอร์ด เคอร์เซอร์คือตัวระบุเฉพาะขององค์ประกอบสุดท้ายในหน้าปัจจุบัน

ซึ่งแตกต่างจากการแบ่งหน้าแบบ Offset ที่ไคลเอ็นต์พูดว่า “ขอหน้า 5 พร้อม 20 เรกคอร์ด” การแบ่งหน้าแบบเคอร์เซอร์ทำงานต่างกัน: “ขอ 20 เรกคอร์ดหลังจากเรกคอร์ดที่มี ID = 83” เซิร์ฟเวอร์ดำเนินการคำสั่งด้วย WHERE id > 83 และ LIMIT 20 แนวทางนี้ รับประกันว่าแต่ละเรกคอร์ดจะตกอยู่ในหน้าเดียวโดยไม่คำนึงถึงการแทรก

แนวคิดของการแบ่งหน้าแบบเคอร์เซอร์ได้รับการยอมรับอย่างกว้างขวางเนื่องจากข้อกำหนด Relay Connection (GraphQL) ซึ่งทำให้การแบ่งหน้าแบบเคอร์เซอร์เป็นมาตรฐานสำหรับ API สมัยใหม่ Relay กำหนดรูปแบบการตอบสนอง: edges (อาร์เรย์ของเรกคอร์ดพร้อมเคอร์เซอร์), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor)

ประวัติความเป็นมา

การแบ่งหน้าแบบเคอร์เซอร์ไม่ใช่เทคนิคใหม่ — มันถูกใช้ในฐานข้อมูลมานานก่อนยุคเว็บ ใน SQL เรียกว่า keyset pagination หรือ seek method วิธีการนี้เป็นที่นิยมใน API หลังจากการเผยแพร่ข้อกำหนด Relay ในปี 2015 ซึ่งทำให้รูปแบบเคอร์เซอร์เป็นทางการเป็นสตริงที่เข้ารหัส base64 เพื่อความสอดคล้องในการส่งผ่าน HTTP

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

หลักการพื้นฐานของการแบ่งหน้าแบบเคอร์เซอร์คือคำสั่งใช้เงื่อนไข WHERE บนฟิลด์ที่มีดัชนีเพื่อกำหนดตำแหน่ง ไม่ใช่ offset สำหรับทิศทางไปข้างหน้าใช้ WHERE id > last_id; สำหรับทิศทางย้อนกลับใช้ WHERE id < first_id ดัชนี B-tree ค้นหาเรกคอร์ดแรกหลังจากเคอร์เซอร์ใน O(log n) ให้เวลาตอบสนองที่เสถียร

sql
-- ดึงข้อมูล 20 รายการหลังเคอร์เซอร์ '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- ดึงข้อมูล 20 รายการก่อนเคอร์เซอร์ '83' (ย้อนกลับ)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

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

เคอร์เซอร์สามารถเป็นแบบง่าย (ค่า ID) หรือซับซ้อน (ประกอบจากหลายฟิลด์) เคอร์เซอร์แบบง่ายคือคีย์หลักของเรกคอร์ด เช่น id แบบเพิ่มอัตโนมัติหรือ UUID เคอร์เซอร์แบบผสมใช้สำหรับการเรียงลำดับตามฟิลด์ที่ไม่ซ้ำกัน เช่น (created_at, id) โดยที่ id รับประกันความเป็นเอกลักษณ์เมื่อการประทับเวลาเหมือนกัน

รูปแบบ API ทั่วไปคือเคอร์เซอร์เป็นสตริงที่เข้ารหัส base64 เซิร์ฟเวอร์ถอดรหัสเคอร์เซอร์ ดึงค่า และสร้างคำสั่ง SQL การเข้ารหัส Base64 ซ่อนโครงสร้างภายในของเคอร์เซอร์จากไคลเอ็นต์และอนุญาตให้เปลี่ยนรูปแบบโดยไม่ทำลายความเข้ากันได้ย้อนหลัง ไคลเอ็นต์รับเคอร์เซอร์ในฟิลด์ endCursor ของการตอบสนองและส่งผ่านเป็นสตริงในคำขอถัดไป

การนำทางไปข้างหน้าและย้อนกลับ

การแบ่งหน้าแบบเคอร์เซอร์รองรับการนำทางสองทิศทาง สำหรับการเคลื่อนที่ไปข้างหน้า (next) ใช้เคอร์เซอร์ขององค์ประกอบสุดท้ายในหน้าปัจจุบัน; สำหรับย้อนกลับ (previous) ใช้เคอร์เซอร์ขององค์ประกอบแรก พารามิเตอร์ after และ before ในคำขอกำหนดทิศทาง: after นำเรกคอร์ดหลังจากเคอร์เซอร์ before นำเรกคอร์ดก่อนเคอร์เซอร์

Cursor เทียบกับ Offset

การเลือกระหว่างการแบ่งหน้าแบบเคอร์เซอร์และ Offset เป็นหนึ่งในการตัดสินใจทางสถาปัตยกรรมที่สำคัญเมื่อออกแบบ API แต่ละวิธีมีจุดแข็งและจุดอ่อนที่กำหนดความสามารถในการนำไปใช้ Cursor pagination ชนะในสถานการณ์ที่มีข้อมูลไดนามิก; Offset ชนะในสถานการณ์ที่มีการนำทางตามอำเภอใจ

คุณลักษณะCursorOffset
ความเสถียรเมื่อแทรกสูง (ไม่ซ้ำ)ต่ำ (หน้ากระจัดกระจาย)
ประสิทธิภาพบนชุดข้อมูลขนาดใหญ่O(log n) — เสถียรO(n) — ลดลงเมื่อเติบโต
การนำทางตามหมายเลขหน้าไม่ได้ (page=5)
ความซับซ้อนในการใช้งานปานกลางต่ำ
การรองรับ RESTcursor/before/afterpage/offset
การรองรับ GraphQLมาตรฐาน Relayไม่แนะนำ

ทำไม Offset ถึงล้มเหลวในขนาดใหญ่

การแบ่งหน้าแบบ Offset ทำการสแกนตารางทั้งหมดจนถึงตำแหน่ง OFFSET ที่ offset=100000 ฐานข้อมูลอ่านและข้าม 100000 แถว แม้ว่า LIMIT จะเป็น 20 MySQL และ PostgreSQL ไม่สามารถปรับ OFFSET ให้เหมาะสมได้ — นี่คือคุณลักษณะการใช้งาน LIMIT/OFFSET ใน SQL การแบ่งหน้าแบบเคอร์เซอร์ใช้ดัชนี B-tree ที่ค้นหาตำแหน่งใน O(log n)

ปัญหาเพิ่มเติมของ Offset คือการ “ข้าม” เรกคอร์ดเมื่อแบ่งหน้าย้อนกลับ หากผู้ใช้โหลดหน้าที่ 5 และในขณะนั้นมีการเพิ่มเรกคอร์ดใหม่ เมื่อขอหน้าที่ 6 พวกเขาจะเห็นเรกคอร์ดจากหน้าที่ 5 อีกครั้งหรือพลาดเรกคอร์ดใหม่ Cursor pagination กำจัดสถานการณ์นี้อย่างสมบูรณ์: เคอร์เซอร์ชี้ไปยังตำแหน่งเฉพาะในชุด และการแทรกไม่เปลี่ยนตำแหน่ง

การใช้งาน Cursor Pagination

มาดูการใช้งานการแบ่งหน้าแบบเคอร์เซอร์บนแบ็กเอนด์ (Kotlin + Spring) และบนไคลเอ็นต์ (Android + Retrofit) เซิร์ฟเวอร์รับพารามิเตอร์ after, before, limit และส่งคืนรายการเรกคอร์ดพร้อมเคอร์เซอร์และ pageInfo การตอบสนองทั่วไปประกอบด้วย hasNextPage และ hasPreviousPage สำหรับจัดการ UI การแบ่งหน้า

kotlin
@GetMapping("/posts")
fun getPosts(
    @RequestParam after: Long?,
    @RequestParam(defaultValue = "20") limit: Int
): CursorResponse<Post> {
    val cursor = after ?: Long.MAX_VALUE
    val posts = repository.findByIdLessThanOrderByIdDesc(
        cursor, PageRequest.of(0, limit)
    )
    val endCursor = posts.lastOrNull()?.id
    return CursorResponse(
        data = posts,
        pageInfo = PageInfo(
            hasNextPage = posts.size == limit,
            endCursor = endCursor
        )
    )
}

การใช้งานฝั่งไคลเอ็นต์บน Android

บนไคลเอ็นต์ การแบ่งหน้าแบบเคอร์เซอร์ใช้งานผ่าน PagingSource จาก Paging 3 โดยที่คีย์คือเคอร์เซอร์ (Long) PagingSource.load รับ LoadParams.key — เคอร์เซอร์ของเรกคอร์ดที่โหลดล่าสุด LoadResult.Page ส่งคืนข้อมูลและ nextKey — เคอร์เซอร์สำหรับหน้าถัดไป เมื่อ nextKey = null การแบ่งหน้าจะเสร็จสมบูรณ์

kotlin
// Retrofit API
interface PostApi {
    @GET("posts")
    suspend fun getPosts(
        @Query("after") after: Long?,
        @Query("limit") limit: Int = 20
    ): CursorResponse<Post>
}

// PagingSource พร้อมคีย์เคอร์เซอร์
class PostPagingSource(
    private val api: PostApi
) : PagingSource<Long, Post>() {

    override suspend fun load(
        params: LoadParams<Long>
    ): LoadResult<Long, Post> = try {
        val response = api.getPosts(
            after = params.key,
            limit = params.loadSize
        )
        val nextKey = response.pageInfo.endCursor
        LoadResult.Page(
            data = response.data,
            prevKey = null,
            nextKey = nextKey
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

การใช้งาน GraphQL ผ่าน Relay

ใน GraphQL การแบ่งหน้าแบบเคอร์เซอร์ใช้งานผ่านรูปแบบ Relay Connection แต่ละประเภทมี Connection (พร้อม pageInfo และ edges) และ Edge (node + cursor) คำสั่งส่งพารามิเตอร์ first, after, last, before เซิร์ฟเวอร์ส่งคืน อาร์เรย์ของ edges พร้อมเคอร์เซอร์และ pageInfo พร้อม hasNextPage/hasPreviousPage

เมื่อใดควรใช้ Cursor Pagination

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

  • แชทและ messenger — ข้อความใหม่แต่ละรายการถูกเพิ่มที่ด้านบนของรายการ การแบ่งหน้าแบบ Offset หยุดชะงักกับทุกข้อความใหม่
  • เครือข่ายสังคมและฟีด — โพสต์ถูกเผยแพร่อย่างต่อเนื่อง การแบ่งหน้าแบบเคอร์เซอร์รับประกันว่าผู้ใช้จะไม่พลาดโพสต์ใด ๆ
  • ประวัติคำสั่งซื้อและธุรกรรม — ข้อมูลเปลี่ยนแปลงน้อยครั้ง แต่ความสอดคล้องมีความสำคัญสำหรับรายงานทางการเงิน
  • API ที่มีปริมาณข้อมูลขนาดใหญ่ — เรกคอร์ดนับล้าน Cursor pagination รักษาประสิทธิภาพที่ Offset เริ่มช้าลง
  • GraphQL API — มาตรฐาน Relay ต้องการการแบ่งหน้าแบบเคอร์เซอร์เพื่อให้สอดคล้องกับข้อกำหนด
  • แอปมือถือที่มีการเลื่อนไม่สิ้นสุด — ผู้ใช้เลื่อนลงเพื่อโหลดชุดใหม่ แนวทางเคอร์เซอร์ให้ประสบการณ์ผู้ใช้ที่ราบรื่นไม่ซ้ำ

เมื่อใดที่ไม่เหมาะกับการแบ่งหน้าแบบเคอร์เซอร์

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

การแบ่งหน้าแบบเคอร์เซอร์ไม่รองรับการ “กระโดด” ไปยังหน้าตามอำเภอใจ — ผู้ใช้ไม่สามารถคลิก “หน้าที่ 5” และไปที่นั่นได้ นี่คือข้อจำกัดทางสถาปัตยกรรม: การนับจำนวนหน้าทั้งหมด ต้องใช้คำสั่ง COUNT แยกต่างหาก ซึ่งอาจมีราคาแพงสำหรับตารางขนาดใหญ่ ในกรณีเช่นนี้ ใช้แนวทางแบบผสม: เคอร์เซอร์สำหรับข้อมูล + count สำหรับการแบ่งหน้า

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

เคอร์เซอร์ใน Cursor Pagination คืออะไร?

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

ทำไม Cursor Pagination ถึงดีกว่า Offset?

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

สามารถใช้งานการแบ่งหน้าแบบเคอร์เซอร์โดยไม่มี GraphQL ได้หรือไม่?

ได้ การแบ่งหน้าแบบเคอร์เซอร์ไม่ได้ผูกติดกับ GraphQL สามารถใช้งานได้ใน REST API ใด ๆ โดยส่งเคอร์เซอร์เป็นพารามิเตอร์คำสั่ง ?after=83&limit=20 การตอบสนองควรมี pageInfo พร้อม endCursor และ hasNextPage — สิ่งนี้ช่วยให้ไคลเอ็นต์จัดการการโหลดโดยไม่ต้องรู้โครงสร้างภายในของเคอร์เซอร์

ควรใช้เคอร์เซอร์ใด — ID, UUID หรือ timestamp?

ID แบบเพิ่มอัตโนมัติ เป็นตัวเลือกที่ดีที่สุด: เพิ่มขึ้นแบบโมโนโทนิก ไม่เปลี่ยนแปลง ถูกทำดัชนีอย่างมีประสิทธิภาพ UUID v7 (เรียงตามเวลา) ก็เหมาะสมเช่นกัน การประทับเวลาอาจสร้างการซ้ำซ้อนในเวลาเดียวกัน ดังนั้นให้รวมกับ ID: (created_at, id) เพื่อรับประกันความเป็นเอกลักษณ์ของเคอร์เซอร์

จะหาจำนวนหน้าทั้งหมดด้วยการแบ่งหน้าแบบเคอร์เซอร์ได้อย่างไร?

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

สรุป

  • Cursor Pagination เป็นวิธีการแบ่งหน้าที่มีการนำทางด้วยตัวระบุเรกคอร์ดเฉพาะแทน offset
  • เคอร์เซอร์ รับประกันความเสถียรของชุดเมื่อแทรก: เรกคอร์ดใหม่ไม่เลื่อนหน้าที่โหลดแล้ว
  • ประสิทธิภาพ บนปริมาณข้อมูลขนาดใหญ่ยังคงสูง (O(log n)) ต้องขอบคุณการใช้ดัชนี B-tree
  • Cursor pagination เหมาะสำหรับข้อมูลไดนามิก: แชท ฟีดข่าว ธุรกรรม ความคิดเห็น
  • ข้อจำกัดหลัก คือการขาดการนำทางตามหมายเลขหน้าและไม่สามารถกระโดดไปหน้าตามอำเภอใจได้
  • การใช้งาน ใช้ WHERE id หลังจากเคอร์เซอร์ พารามิเตอร์ after/before และ pageInfo ในการตอบสนอง
  • มาตรฐาน — Relay Connection GraphQL แต่ REST API ที่มีพารามิเตอร์เคอร์เซอร์ก็ใช้อย่างแพร่หลายเช่นกัน

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม