Topik wawancara representatif

Wawancara Backend: Bagaimana Anda mendesain cursor pagination yang stabil untuk daftar besar yang dinamis?

BackendSedang
Tim Redaksi Offer.ccDipublikasikan Diperbarui

Pertanyaan

Sebuah daftar dengan ratusan juta baris terus menerima operasi insert dan delete, dan halaman-halamannya memuat data duplikat atau celah data (gap). Bagaimana Anda mendiagnosisnya dan memilih antara OFFSET, pagination keyset, dan database cursor?

Pertanyaan dan konteks

Pertanyaan ini menguji apakah seorang backend engineer dapat menangani performa deep-page, pengurutan yang stabil di bawah operasi concurrent write, dan kontrak kursor yang eksplisit secara bersamaan. Asumsikan klien sedang menelusuri daftar pesanan atau feed yang terus berubah dalam urutan waktu menurun (descending), 50 baris per halaman, dengan baris baru yang terus berdatangan; klien sebagian besar hanya membutuhkan halaman berikutnya alih-alih melompat ke halaman sembarang.

Topik ini cocok untuk peran backend, layanan data, dan platform. Jangan langsung menyatakan bahwa kursor selalu lebih cepat. Pertama-tama, perjelas pengurutan, lompatan halaman, ekspor data, konsistensi, semantik penghapusan, replika, dan indeks karena setiap batasan dapat mengubah pilihan solusi.

Apa yang sedang dievaluasi oleh pewawancara

Jawaban yang kuat menjelaskan mengapa OFFSET yang dalam memindai dan membuang baris-baris sebelumnya, mengapa sort key yang tidak unik dapat bergeser (drift) di bawah concurrent write, dan mengapa keyset pagination memerlukan total order yang stabil serta indeks yang cocok. Jawaban tersebut juga membedakan antara keyset token yang stateless dan database cursor yang menahan sebuah transaksi, serta mengusulkan pengujian untuk duplikat, celah (gap), baris batas yang dihapus, dan token yang dipalsukan.

Pertanyaan klarifikasi yang perlu diajukan

  • Apakah pengguna harus melompat ke halaman N atau melihat jumlah total (total count)? Keyset pagination murni tidak menyediakan hal itu secara langsung.
  • Bagaimana urutan penyortirannya? Apakah timestamp bersifat unik dan tidak dapat diubah (immutable), atau memerlukan ID unik sebagai pemecah seri (tie-breaker)?
  • Apakah klien menginginkan live view atau snapshot untuk satu kali penelusuran? Operasi insert dan delete terlihat berbeda pada masing-masing pendekatan.
  • Apakah kueri mencakup replika, shard, atau filter penyewa (tenant)? Indeks dan token harus mengikat kondisi-kondisi tersebut.
  • Bisakah klien mendekode, memodifikasi, atau memutar ulang (replay) sebuah kursor? Hal tersebut menentukan penandatanganan (signing), kedaluwarsa, dan pembuatan versi (versioning).

Kerangka jawaban 30 detik

“Pertama-tama saya memperjelas pengurutan, lompatan halaman, dan semantik konsistensi. Untuk daftar besar yang dinamis, saya akan menggunakan keyset pagination dengan pemecah seri yang unik, seperti created_at DESC, id DESC, yang didukung oleh composite index yang cocok; permintaan berikutnya membawa sort key baris terakhir alih-alih OFFSET yang dalam. Kursor mengodekan filter, versi pengurutan, dan batas, lalu ditandatangani dan diberi waktu kedaluwarsa. Jika penelusuran membutuhkan snapshot tetap, saya akan mempertimbangkan timestamp atau database cursor transaksional dan menjelaskan biaya sumber dayanya. Saya akan memvalidasi kontrak tersebut terhadap concurrent insert, delete, timestamp duplikat, dan token yang dimanipulasi.”

Jawaban langkah demi langkah

Langkah 1: Tentukan kontrak pagination

Tentukan batas limit, urutan default, next_cursor, has_more, dan filter. Pengurutan yang stabil memerlukan total order; mengurutkan hanya berdasarkan created_at bersifat ambigu ketika beberapa baris memiliki timestamp yang sama, jadi tambahkan ID unik yang immutable. Jika klien memerlukan halaman sebelumnya, rancang perbandingan terbalik dan penanganan batas secara eksplisit daripada hanya membalikkan kueri halaman berikutnya.

Langkah 2: Jelaskan performa dan pergeseran OFFSET

OFFSET k LIMIT n biasanya mengharuskan database menemukan dan melewati k baris sebelumnya, sehingga beban kerja deep-page meningkat seiring bertambahnya k. Jika operasi insert atau delete terjadi sebelum halaman yang sudah dibaca, offset berikutnya akan bergeser dan dapat menghasilkan data duplikat atau celah (gap). Daftar admin yang kecil dan sebagian besar statis yang memerlukan lompatan halaman dapat menerima OFFSET, tetapi batasi kedalamannya dan verifikasi biayanya dengan execution plan.

Langkah 3: Terapkan keyset pada total order yang stabil

Untuk (created_at, id) menurun (descending), halaman pertama tidak memiliki batas dan halaman berikutnya menggunakan key dari baris terakhir:

sql
-- first page
SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC, id DESC
LIMIT 50;

-- next page: boundary comes from the last returned row
SELECT id, title, created_at
FROM posts
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 50;

Kueri melakukan seek dari batas indeks dan memindai sejumlah baris yang terbatas alih-alih melewati setiap halaman sebelumnya. Urutan kolom composite index, filter, dan arah pengurutan harus cocok dengan kueri; jika tidak, kueri keyset yang secara teoritis baik tetap dapat memindai rentang data yang besar.

Langkah 4: Rancang token kursor yang dapat diverifikasi

Jangan perlakukan offset internal sebagai kursor. Sertakan boundary key, ringkasan (digest) filter, versi pengurutan, dan waktu kedaluwarsa; tandatangani token atau gunakan enkripsi terotentikasi agar klien tidak dapat memodifikasinya. Saat diterima, validasi versi, penyewa (tenant), dan filter terhadap permintaan. Jika pengurutan berubah, tolak versi lama atau mulai ulang secara eksplisit alih-alih menafsirkan token yang sama di bawah pengurutan yang berbeda.

Langkah 5: Pilih semantik live atau snapshot

Keyset pagination secara live memungkinkan baris baru muncul di bagian atas; baris yang sudah berada di luar batas biasanya tidak berulang. Menghapus baris batas tidak membuatnya muncul kembali, meskipun jumlah totalnya berubah. Untuk ekspor atau audit yang membutuhkan set data tetap, sertakan read timestamp, versi snapshot, atau database cursor dalam kontrak. Database cursor dapat menahan transaksi dan sumber daya, sehingga tidak otomatis cocok untuk pagination HTTP yang berdurasi panjang.

Langkah 6: Tangani replika, shard, dan baris batas

Lag pada replika dapat menyembunyikan sementara baris yang telah dikembalikan oleh database utama (primary). Kunci rute (pin the route), bawa batas waktu yang telah dikonfirmasi, atau dokumentasikan konsistensi akhir (eventual consistency). Di berbagai shard, setiap shard dapat menghasilkan keyset lokal dan layanan dapat melakukan k-way merge yang sadar kursor. Uji penghapusan baris batas akhir, timestamp yang bernilai sama, dan perubahan filter.

Langkah 7: Verifikasi performa dan kebenaran

Pemeriksaan performa mencakup pemindaian baris pada deep-page, latensi P95, dan penggunaan indeks. Pengujian kebenaran (correctness) melakukan insert, delete, dan update baris di antara permintaan, lalu memastikan perilaku duplikat dan celah sesuai dengan yang dijanjikan. Catat filter setiap halaman, versi pengurutan, key pertama dan terakhir, serta versi token sehingga pergeseran pada produksi dapat dipetakan ke batas yang konkret.

Contoh jawaban yang kuat

“Pertama-tama saya akan mengklarifikasi apakah pengguna memerlukan lompatan halaman, jumlah total, atau snapshot tetap, dan apakah field pengurutan bersifat unik. Untuk daftar yang terus ditulis dan sebagian besar ditelusuri ke arah depan, saya akan memilih keyset pagination: gunakan created_at yang immutable ditambah id yang unik untuk total order dan composite index, lalu gunakan dua key dari baris terakhir sebagai batas rentang berikutnya. Halaman-halaman dalam tidak memindai dan membuang semua baris sebelumnya, dan operasi insert sebelum batas tidak akan menggeser setiap halaman berikutnya.

Saya akan mengodekan filter, versi pengurutan, boundary key, dan kedaluwarsa ke dalam token yang ditandatangani, lalu mewajibkan tenant dan filter token cocok dengan permintaan. Di bawah semantik live, baris baru muncul di bagian atas; untuk hasil tetap setingkat audit, saya akan mempertimbangkan timestamp atau kursor transaksional yang terkontrol dan menjelaskan biaya transaksi jangka panjang. Pada sistem dengan replika, saya akan mengunci routing atau mendokumentasikan batas visibilitas.

Terakhir, saya akan memverifikasi execution plan dan uji konkurensi yang mencakup deep scan, timestamp duplikat, insert, delete, update pada baris batas, lag replika, dan pemalsuan token. Daftar admin kecil yang harus melompat halaman dapat tetap menggunakan OFFSET, tetapi saya akan membatasi kedalaman dan memantau latensinya.”

Kesalahan umum

  • Mengklaim bahwa kursor selalu lebih cepat → mengabaikan kebutuhan lompatan halaman dan biaya transaksi panjang → bandingkan OFFSET, keyset, dan database cursor berdasarkan batasan.
  • Mengurutkan hanya berdasarkan waktu → nilai yang sama menghasilkan urutan yang tidak stabil → tambahkan ID unik yang immutable.
  • Memasukkan angka offset ke dalam token → halaman dalam tetap mengalami pergeseran dan memakan biaya lebih → bawa batas pengurutan dan digest filter.
  • Membiarkan status live/snapshot tidak terdefinisi → klien tidak dapat menginterpretasikan operasi insert atau delete → nyatakan semantik visibilitas dalam kontrak API.
  • Mengabaikan arah indeks dan filter → keyset tetap memindai banyak baris → verifikasi composite index dengan execution plan.
  • Menerima kursor klien apa pun → tenant atau batas yang basi dapat dipalsukan → tandatangani, validasi versi, dan beri batas kedaluwarsa pada token.

Pertanyaan lanjutan dan jawaban

Pertanyaan lanjutan 1: Pengguna harus melompat ke halaman 500. Apa yang Anda lakukan?

Untuk daftar admin yang kecil atau sebagian besar statis, OFFSET dengan batas maksimal masih masuk akal. Untuk daftar yang besar, gunakan batas halaman yang telah dihitung sebelumnya atau filter pencarian dan pengurutan sehingga pengguna dapat menemukan wilayah yang diinginkan alih-alih menjanjikan latensi rendah pada kedalaman sembarang. Dokumentasikan biaya dan batasan konsistensi dari setiap pilihan.

Pertanyaan lanjutan 2: created_at dapat diedit. Apakah kursor tetap stabil?

Tidak. Gunakan waktu pembuatan yang immutable ditambah ID unik, atau bekukan field pengurutan yang dapat berubah tersebut dalam versi snapshot. Jika kebutuhan bisnis mengharuskan pengurutan berdasarkan field yang dapat berubah, terimalah pergeseran posisi dalam live view atau gunakan versi tetap dengan biaya penyimpanan dan pembacaan tambahan.

Pertanyaan lanjutan 3: Replika yang mengalami lag membuat halaman berikutnya menjadi pendek. Bagaimana Anda menanganinya?

Kunci pembacaan ke database primary atau ke replika yang sama sesuai dengan persyaratan konsistensi, atau sertakan batas 'tidak lebih awal dari waktu/LSN' dan kembalikan status eksplisit setelah batas waktu tunggu (wait timeout). Jangan menganggap baris yang hilang sebagai akhir dari daftar secara diam-diam; bedakan ketidaktampakan sementara dari has_more=false.

Pertanyaan lanjutan 4: Bagaimana Anda membuktikan bahwa tidak ada duplikat atau celah data (gap)?

Bangun pengujian konkurensi yang terkontrol: baca halaman pertama, lakukan insert sebelum batas, hapus baris batas, masukkan sort key yang sama, dan perbarui baris; kemudian periksa set ID unik, pengurutan, dan visibilitas yang dijanjikan. Catat batas halaman, versi token, dan pengidentifikasi snapshot sehingga kegagalan dapat direproduksi tepat pada batas yang bersangkutan.

Sumber publik

Pertanyaan terkait