Topik temu duga representatif

Temu Duga Backend: Bagaimana Anda Mendiagnosis dan Membetulkan Masalah N+1 Query?

BackendSukar
Pasukan Editorial Offer.ccDiterbitkan Dikemas kini

Soalan

Satu titik akhir senarai pesanan mengambil 50 pesanan dan kemudian membaca pelanggan bagi setiap pesanan semasa mensiri respons, menghasilkan 51 pernyataan SQL. Setiap pernyataan adalah pantas, tetapi kependaman permintaan meningkat seiring dengan saiz halaman. Bagaimanakah anda akan membuktikan punca N+1, memilih pembetulan, dan menghalangnya daripada berulang?

Masalah dan Konteks yang Berkenaan

Satu titik akhir senarai pesanan mula-mula mengambil satu halaman yang mengandungi 50 pesanan. Semasa penyirian respons, ORM memuatkan order.customer secara berasingan untuk setiap pesanan. Surihan pengeluaran mengandungi satu pertanyaan pesanan dan 50 pertanyaan pelanggan. Tiada pernyataan individu yang kelihatan perlahan, namun satu permintaan melakukan 51 pusingan pergi balik (roundtrip) pangkalan data. Apabila saiz halaman digandakan, bilangan pernyataan dan kependaman permintaan turut meningkat bersama-samanya.

Andaikan setiap pesanan kepunyaan satu pelanggan, respons hanya memerlukan nama paparan pelanggan, dan titik akhir mesti mengekalkan susunan semasa, penapis pengesahan, dan semantik penomboran halaman (pagination). Angka-angka ini merupakan andaian temu duga yang menjadikan diagnosis boleh diuji. Soalan terasnya adalah bagaimana untuk mengesan masalah corak capaian aplikasi, bukan cara menala satu pelan SQL yang perlahan.

Peranan sasaran ialah jurutera backend yang bekerja dengan pangkalan data hubungan dan ORM. Jawapan yang lengkap harus membandingkan pertanyaan cantuman tunggal (single joined query), kelompok select-in dua pertanyaan, dan perubahan pada pemuatan lalai. Ia juga mesti merangkumi hubungan satu-ke-banyak, ketekalan transaksi, kebolehcerapan, dan semakan regresi yang bilangan pertanyaannya tidak bergantung pada pemasaan.

Perkara yang Dinilai oleh Penemu Duga

Isyarat pertama ialah sama ada calon mengukur kerja pada sempadan permintaan. Masalah N+1 boleh terdiri daripada banyak pertanyaan berindeks yang cekap secara individu. Hanya melihat pada log pertanyaan perlahan atau menjalankan EXPLAIN pada satu carian pelanggan boleh terlepas pandang faktor pengganda tersebut. Bukti yang berguna ialah surihan atau log pertanyaan yang mengelompokkan pernyataan mengikut permintaan dan menunjukkan carian ternormal yang sama berulang daripada tapak panggilan yang sama.

Isyarat kedua ialah model pertumbuhan yang betul. Untuk N baris induk, laluan naif melaksanakan satu pertanyaan induk ditambah satu pertanyaan baris berkaitan bagi setiap induk:

text
Q(N) = 1 + N
Q(50) = 51

Kelompok select-in biasanya menukar perkara itu kepada satu pertanyaan induk ditambah satu pertanyaan baris berkaitan, jadi bilangannya kekal dua untuk saiz halaman yang diuji. Jika senarai ID mesti dipecahkan kepada B kelompok, bilangannya menjadi 1 + B; ia masih tidak berkembang sekali bagi setiap induk.

Isyarat ketiga ialah memilih bentuk pemuatan daripada kardinaliti hubungan dan keperluan respons. Carian pelanggan banyak-ke-satu dengan beberapa lajur sempit selalunya sesuai untuk JOIN. Koleksi satu-ke-banyak yang besar boleh menggandakan baris hasil dan menduplikasi lajur induk, menjadikan pertanyaan halaman induk diikuti dengan pertanyaan anak berkelompok lebih selamat. "Dayakan eager loading di mana-mana" bukanlah satu reka bentuk; sesetengah ORM masih boleh mengeluarkan select sekunder untuk perkaitan eager, dan pemuatan eager global boleh mengambil data yang tidak pernah dikembalikan oleh titik akhir ini.

Akhir sekali, penemu duga menjangkakan bukti bahawa tingkah laku dikekalkan. Penambahbaikan bilangan pertanyaan tidak menghalalkan penapis penyewa yang hilang, sempadan halaman yang berubah, susunan yang tidak stabil, bacaan yang tidak konsisten, atau baris tambahan yang disebabkan oleh cantuman. Jawapan yang kukuh mengesahkan kerja pangkalan data dan kesetaraan respons.

Soalan Penjelasan Sebelum Menjawab

  • Di manakah capaian hubungan berlaku? Jika penyirian, templat, pengelogan, atau pemeta menyentuh sifat tersebut selepas repositori mengembalikannya, punca pertanyaan berada di luar gelung yang ketara. Pembetulan mesti meliputi laluan capaian sebenar.
  • Apakah kardinaliti hubungan tersebut? Data banyak-ke-satu boleh dicantum tanpa menggandakan satu pesanan kepada beberapa baris. Koleksi anak satu-ke-banyak mengubah risiko penomboran halaman dan saiz muatan.
  • Medan berkaitan manakah yang diperlukan? Nama paparan menyokong unjuran yang sempit. Memuatkan keseluruhan entiti pelanggan dan setiap perkaitan mewujudkan overfetching walaupun bilangan pertanyaan berkurangan.
  • Adakah ID berkaitan berulang? Peta identiti berskop permintaan boleh mengurangkan carian pendua, tetapi ia tidak mengehadkan bilangan apabila kebanyakan ID adalah unik. Ukur dan bukannya menganggap bahawa cache membetulkannya.
  • Bagaimanakah penomboran halaman digunakan? Baris induk mesti dipilih dengan susunan deterministik sebelum cantuman satu-ke-banyak atau pemuatan anak; jika tidak, penggandaan baris boleh mengubah induk yang muncul.
  • Adakah kedua-dua bacaan mesti berkongsi satu snapshot? JOIN ialah satu pernyataan. Pertanyaan induk yang diikuti oleh pertanyaan anak boleh melihat perubahan serentak di bawah tingkah laku pengasingan lalai. Jika ketekalan titik-dalam-masa penting, gunakan snapshot transaksi yang sesuai atau bentuk pernyataan tunggal.
  • Apakah yang sebenarnya dijana oleh ORM? Nama seperti eager, include, prefetch, atau split query tidak menjamin bilangan pernyataan tertentu. Periksa SQL yang dikeluarkan untuk versi yang digunakan.

Rangka Kerja Jawapan 30 Saat

"Saya akan mengelompokkan span pangkalan data mengikut permintaan dan mengesahkan satu pertanyaan halaman diikuti dengan carian pelanggan ternormal yang sama sebanyak 50 kali. Kemudian saya akan mengubah saiz halaman; kiraan 11, 21, dan 41 untuk halaman 10, 20, dan 40 membuktikan amplifikasi pertanyaan linear walaupun setiap carian adalah pantas. Untuk medan nama paparan banyak-ke-satu ini, saya akan membandingkan JOIN sempit dengan kelompok select-in dua pertanyaan. Saya akan mengelakkan pemuatan eager lalai secara global kerana ia boleh menyebabkan overfetching dan tidak menjamin satu pernyataan. Untuk hubungan satu-ke-banyak yang besar, saya akan menomborkan halaman induk dahulu dan mengelompokkan anak untuk mengelakkan penggandaan baris. Akhir sekali, saya akan menetapkan bajet pertanyaan yang malar, membandingkan ID respons dan susunan, serta memantau kiraan pertanyaan tahap permintaan dan kependaman selepas pelancaran."

Perbincangan Mendalam Langkah demi Langkah

Langkah 1: Buktikan amplifikasi pada sempadan permintaan

Lampirkan ID permintaan atau surihan pada span pangkalan data, normalkan SQL dengan menggantikan nilai parameter, dan kelompokkan mengikut tapak panggilan. Surihan yang mencurigakan secara strukturnya sepatutnya kelihatan seperti ini:

text
1 × SELECT id, customer_id, created_at, total_cents FROM orders ... LIMIT ?
50 × SELECT id, display_name FROM customers WHERE id = ?

Cap jari yang berulang dan penskalaan linear membezakan N+1 daripada satu pernyataan tunggal yang mahal, penantian kunci, giliran kolam sambungan, atau penyiri yang perlahan. Rekodkan jumlah tempoh pangkalan data dan kiraan pusingan pergi balik serta tempoh pernyataan. Lima puluh pertanyaan satu milisaat tidak bersamaan dengan satu pertanyaan lima puluh satu milisaat kerana setiap pusingan pergi balik turut menggunakan sambungan, kerja protokol, dan masa penjadual.

Ulangi permintaan dengan saiz halaman terkawal sebanyak 10, 20, dan 40. Kiraan 11, 21, dan 41 ialah tanda sebab-akibat yang kukuh. Mengalih keluar medan hubungan buat sementara waktu sepatutnya meruntuhkan pertanyaan tambahan; ini mengesahkan capaian sifat mana yang mencetuskan pemuatan. Eksperimen ini lebih berguna daripada menambah indeks pada carian kunci utama yang sudah pun berindeks.

Langkah 2: Tentukan hasil yang diperlukan sebelum menukar pemuatan

Tulis kontrak respons: ID pesanan yang disusun, kursor atau sempadan halaman, penyewa yang dibenarkan, dan medan pelanggan yang tepat. Tentukan juga cara pelanggan yang hilang atau dipadam dipaparkan. Ini menghalang pengoptimuman pertanyaan daripada secara senyap menjadi perubahan kontrak data.

Kekalkan predikat pengesahan dan pemadaman lembut (soft-delete) dalam laluan berkelompok. Jika pemuat hubungan asal menguatkuasakan skop penyewa, pertanyaan WHERE id = ANY(...) yang ditulis secara manual yang mengetepikan skop penyewa boleh menjadi kebocoran data. Bilangan pertanyaan hanyalah salah satu kriteria penerimaan.

Langkah 3: Pilih antara JOIN sempit dan kelompok select-in

Untuk hubungan banyak-ke-satu mandatori dan respons yang sempit, satu pernyataan bercantum adalah mudah:

sql
SELECT
  o.id,
  o.created_at,
  o.total_cents,
  c.id AS customer_id,
  c.display_name
FROM orders AS o
JOIN customers AS c
  ON c.id = o.customer_id
 AND c.tenant_id = o.tenant_id
WHERE o.tenant_id = $1
ORDER BY o.created_at DESC, o.id DESC
LIMIT $2;

Penentu seri pada o.id menjadikan susunan bersifat deterministik. Gunakan left join sebaliknya jika pesanan mungkin wujud lebih lama daripada rekod pelanggannya secara sah dan kontrak sedia ada mengembalikan pesanan tersebut.

Kelompok dua pertanyaan mengekalkan penomboran halaman induk berasingan dan berfungsi dengan baik apabila lebar cantuman atau kardinaliti koleksi akan melambung hasil. TypeScript ilustrasi berikut menyahduplikasi ID, memuatkan lajur yang diperlukan sahaja, dan memetakannya dalam memori:

ts
interface OrderRow {
  id: string
  customerId: string
  createdAt: Date
  totalCents: number
}

interface CustomerRow {
  id: string
  displayName: string
}

const orders = await loadOrderPage(tenantId, limit)
const customerIds = [...new Set(orders.map((order) => order.customerId))]
const customers = await loadCustomersByIds(tenantId, customerIds)
const customerById = new Map(customers.map((customer) => [customer.id, customer]))

return orders.map((order) => ({
  ...order,
  customer: customerById.get(order.customerId) ?? null,
}))

loadCustomersByIds repositori harus menggunakan satu predikat berasaskan set untuk halaman biasa dan membahagikan senarai ID yang luar biasa besar kepada kelompok terikat. Penyahduplikasian mengurangkan parameter yang dipindahkan; ia bukan pembetulan utama. Pembetulan utama ialah memindahkan pemuatan berkaitan ke luar laluan capaian setiap baris.

Langkah 4: Kendalikan hubungan satu-ke-banyak tanpa merosakkan penomboran halaman

Katakan setiap pesanan juga mengembalikan banyak item baris. Mencantumkan pesanan, pelanggan, dan item boleh mengeluarkan satu baris bagi setiap item dan mengulangi lajur pesanan. Menggunakan LIMIT 50 selepas cantuman itu mungkin mengehadkan baris yang dicantum, bukan 50 pesanan yang berbeza. Memuatkan beberapa koleksi dalam satu cantuman boleh menggandakannya antara satu sama lain.

Pilih 50 pesanan induk dahulu dengan susunan yang stabil, kemudian ambil semua item yang order_id berada dalam set ID induk tersebut. Kelompokkan item mengikut order_id dan lampirkannya dalam susunan induk asal. Ini adalah sebab praktikal dokumentasi rasmi ORM menawarkan strategi joined, subquery, select-in, dan split-query dan bukannya satu suis pemuatan eager sejagat.

Langkah 5: Tolak perubahan pemuatan global sebagai jalan pintas

Menukar setiap hubungan daripada lazy kepada eager boleh memindahkan masalah dan bukannya menyelesaikannya. Titik akhir yang tidak memerlukan pelanggan kini melakukan overfetching. Pertanyaan yang tidak melakukan join-fetch pada perkaitan eager mungkin masih mencetuskan select sekunder dalam sesetengah tingkah laku ORM. Graf objek yang luas juga boleh menghasilkan cantuman besar atau kitaran yang sukar diramal.

Utamakan unjuran khusus titik akhir atau pelan pemuatan eksplisit. Dalam pembangunan dan ujian, gunakan pilihan ORM yang membangkitkan ralat pada SQL lazy yang tidak dijangka jika tersedia. Ini mengubah capaian pangkalan data tersembunyi kepada kegagalan yang kelihatan pada sempadan tempat respons dipasang.

Langkah 6: Sahkan bentuk pertanyaan, semantik, dan kesan pengeluaran

Bina matriks regresi dengan hasil kosong, satu baris, ID pelanggan berulang, semua ID pelanggan unik, pelanggan pilihan yang hilang, dan saiz halaman maksimum yang dibenarkan. Sahkan susunan respons, ID, tingkah laku null, pengasingan penyewa, dan bajet pertanyaan yang malar. Untuk pelan dua pertanyaan, halaman 10 dan 40 kedua-duanya harus menggunakan dua pernyataan di bawah had kelompok yang dipilih.

Kemudian bandingkan data seakan-pengeluaran yang mewakili untuk jumlah kependaman permintaan, span pangkalan data bagi setiap permintaan, baris dan bait yang dikembalikan, penghunian kolam sambungan, dan beban pangkalan data. JOIN yang mengurangkan 51 pernyataan kepada satu tetapi mengembalikan muatan berulang yang besar mungkin merupakan regresi di bawah metrik yang berbeza. Lancarkan mengikut titik akhir, perhatikan taburan kiraan pertanyaan, dan simpan sampel surihan yang boleh mengenal pasti tapak panggilan jika pemuatan lazy muncul semula.

Contoh Jawapan Berkualiti Tinggi

"Bukti menunjukkan amplifikasi pertanyaan dan bukannya satu pelan perlahan. Saya akan mulakan dengan satu surihan permintaan dan mengelompokkan SQL ternormal mengikut tapak panggilan. Jika halaman 50 menunjukkan satu pertanyaan pesanan dan 50 carian kunci utama pelanggan, kemudian halaman 10, 20, dan 40 menghasilkan 11, 21, dan 41 pernyataan, saya boleh membuktikan bahawa kerja pangkalan data bertambah sekali bagi setiap baris induk.

Sebelum membetulkannya, saya akan mengekalkan kontrak: predikat penyewa, ID pesanan, susunan deterministik, sempadan halaman, medan pelanggan yang diperlukan, dan tingkah laku kehilangan pelanggan. Oleh kerana ini ialah carian banyak-ke-satu yang sempit, JOIN ialah calon pertama yang baik. Pemuatan select-in dua pertanyaan juga sah: ambil halaman pesanan, nyahduplikasi ID pelanggan, muatkan pelanggan tersebut dalam satu pertanyaan set, dan petakan mengikut ID. Saya akan memilih antara kedua-duanya daripada SQL yang dijana, lebar muatan, dan keperluan ketekalan.

Jika hubungan tersebut ialah koleksi satu-ke-banyak yang besar, saya akan menomborkan halaman pesanan dahulu dan mengelompokkan item dalam pertanyaan kedua. Ini mengelakkan penggandaan baris yang dicantum daripada mengubah penomboran halaman. Saya tidak akan menjadikan setiap perkaitan eager secara global; ia boleh menyebabkan overfetching dan sesetengah bentuk pertanyaan ORM masih mengeluarkan select sekunder.

Ujian regresi akan menjalankan kes kosong, ID berulang, ID unik, hubungan hilang, dan halaman maksimum. Ia akan mengesahkan ID, susunan, pengesahan, dan tingkah laku null yang serupa, ditambah bajet pernyataan yang malar. Selepas pelancaran saya akan memantau span pangkalan data bagi setiap permintaan dan jumlah kependaman, bukan hanya pernyataan individu yang perlahan. Ini membuktikan kedua-dua pembetulan prestasi dan hasil yang tidak berubah."

Kesilapan Lazim

  • Menambah indeks pada carian pelanggan yang berulang → Setiap carian mungkin sudah menggunakan indeks kunci utama, sementara permintaan masih melakukan satu pusingan pergi balik bagi setiap pesanan → Ukur dan tukar corak capaian.
  • Mendayakan pemuatan eager secara global → Titik akhir yang tidak berkaitan melakukan overfetching, dan tingkah laku eager khusus ORM mungkin masih mengeluarkan pernyataan sekunder → Gunakan unjuran khusus titik akhir atau pelan pemuatan.
  • Mencantumkan setiap hubungan → Koleksi satu-ke-banyak menggandakan baris, mengulangi data induk, dan boleh merosakkan sempadan halaman → Nomborkan halaman induk dahulu dan kelompokkan koleksi yang besar.
  • Menggunakan cache seluruh proses sebagai pembetulan → ID sejuk atau unik masih menghasilkan pertanyaan linear, dan data lapuk atau silang penyewa menjadi risiko baharu → Hadkan bilangan pertanyaan secara bebas daripada capaian cache (cache hits).
  • Hanya mengira pernyataan yang perlahan → Puluhan pertanyaan pantas terlepas daripada log pertanyaan perlahan berasaskan ambang semasa menggunakan pusingan pergi balik dan sambungan → Agregatkan span mengikut permintaan dan cap jari.
  • Menggugurkan predikat keselamatan dalam pertanyaan kelompok → Pengoptimuman mungkin memuatkan baris berkaitan daripada penyewa lain → Kekalkan penapis pengesahan dan pemadaman lembut secara eksplisit.
  • Hanya mengesahkan kependaman yang lebih rendah → Ujian pemasaan adalah bising dan boleh lulus dengan cache yang panas → Sahkan bajet pertanyaan yang malar dan kesetaraan respons, kemudian ukur kependaman secara berasingan.
  • Menganggap dua pertanyaan bersamaan dengan satu snapshot → Kemas kini serentak boleh muncul antara pernyataan di bawah tingkah laku pengasingan biasa → Pilih snapshot transaksi atau satu pernyataan apabila kontrak memerlukan ketekalan titik-dalam-masa.

Soalan Susulan dan Maklum Balas

Soalan susulan 1: Bilakah JOIN lebih baik daripada kelompok dua pertanyaan?

JOIN menarik untuk hubungan banyak-ke-satu atau satu-ke-satu yang sempit, apabila semantik snapshot satu pernyataan penting dan penggandaan baris adalah terikat. Kelompok menarik apabila penomboran halaman induk mesti kekal terasing, data berkaitan ialah koleksi, atau cantuman akan mengulangi lajur induk yang lebar. Periksa SQL yang dikeluarkan dan bait yang dikembalikan; bilangan pernyataan sahaja tidak menentukan.

Soalan susulan 2: Bagaimana jika 50 pesanan hanya merujuk kepada tiga pelanggan?

Peta identiti berskop permintaan mungkin mengurangkan laluan naif kepada empat pernyataan, tetapi itu tetap bergantung pada data. Nyahduplikasi tiga ID tersebut dan keluarkan satu pertanyaan set supaya kiraan yang dirancang ialah dua. Jangan bergantung pada cache silang permintaan untuk ketepatan atau pengasingan penyewa.

Soalan susulan 3: Bagaimanakah anda mengesan N+1 dalam penyelesai bersarang gaya GraphQL?

Kumpulkan kunci hubungan semasa satu pelaksanaan permintaan dan hantarkan kelompok berskop permintaan sebelum menyelesaikan medan. Kekalkan susunan hasil dengan memetakan baris kembali ke jujukan kunci asal dan wakili kunci yang hilang secara eksplisit. Ujian regresi harus meminta medan bersarang untuk beberapa induk dan mengesahkan kiraan pernyataan yang terikat; meninggalkan medan tersebut harus mengelakkan pertanyaan yang berkaitan.

Soalan susulan 4: Bagaimana jika kelompok mengandungi lebih banyak ID daripada yang sepatutnya dibawa oleh satu pertanyaan?

Bahagikan ID yang telah dinyahduplikasi kepada ketulan terikat yang dipilih daripada kekangan pangkalan data dan pemacu. Model ini menjadi 1 + B pernyataan untuk B ketulan, jadi ujian harus mengesahkan had yang dijangkakan dan bukannya dua secara tanpa syarat. Jika halaman titik akhir biasa memerlukan banyak ketulan, kurangkan halaman atau pertimbangkan semula bentuk data.

Soalan susulan 5: Bilangan pertanyaan telah dibetulkan, tetapi kependaman hampir tidak bertambah baik. Apakah langkah seterusnya?

Bandingkan masa pangkalan data, masa rangkaian, penyirian, baris dan bait yang dikembalikan, penantian kunci, dan giliran kolam sebelum mencadangkan pembetulan lain. Pernyataan bercantum atau berkelompok yang baharu mungkin memerlukan indeks tersendiri, mungkin mengembalikan terlalu banyak data, atau mungkin tidak menguasai kependaman hujung-ke-hujung. Kekalkan pembetulan N+1 jika ia menghapuskan amplifikasi linear, tetapi diagnosis kesesakan yang tinggal dengan bukti baharu.

Sumber awam

Soalan berkaitan