Kinerja: N+1 dan DataLoader
Kelompokkan dan simpan hasil penyelesaian bidang dalam cache agar tetap cepat.
Kinerja: N+1 dan DataLoader adalah pelajaran PHP Academy gratis di CoddyKit. Ini adalah pelajaran 4 dari 4. Kamu bisa membaca pelajaran lengkapnya di bawah secara gratis — lalu praktikkan langsung di browser dengan editor kode bawaan dan tutor AI 24/7. Ini adalah bagian dari jalur belajar PHP Academy, dan progresmu tersinkronisasi di web dan aplikasi CoddyKit. Kursus PHP Academy mencakup 4 pelajaran total.
Pembunuh Senyap: N+1
Jebakan performa terbesar GraphQL adalah masalah kueri N+1. Masalah ini senyap karena setiap penyelesai tampak tidak berbahaya jika dilihat secara terpisah — tetapi ketika Anda menggabungkan sebuah daftar dan bidang per item, Anda menjalankan satu kueri untuk daftar tersebut ditambah satu kueri untuk setiap item. Dengan 100 item, itu berarti 101 perjalanan pulang-pergi. Pelajaran ini menunjukkan cara menggabungkannya menjadi beberapa kueri berkelompok dengan DataLoader.
Melihat N+1 Secara Konkret
Perhatikan { posts { author { name } } }. Penyelesai posts menjalankan satu kueri. Kemudian, untuk setiap post, penyelesai author menjalankan kuerinya sendiri. Kode naif di bawah ini memperjelas biayanya.
<?php
// 1 query for posts...
$posts = [['id'=>1,'author_id'=>7],['id'=>2,'author_id'=>7],['id'=>3,'author_id'=>9]];
$queries = 1;
foreach ($posts as $post) {
// ...then 1 query PER post to fetch its author
$queries++;
// SELECT * FROM users WHERE id = $post['author_id']
}
echo "Total DB queries: {$queries}\n"; // Total DB queries: 4
Wawasan: Mengelompokkan berdasarkan Tingkat
GraphQL menyelesaikan bidang tingkat demi tingkat. Semua penyelesai author untuk daftar post berjalan pada putaran eksekusi yang sama. Jika kita dapat menunda setiap pencarian author, mengumpulkan id yang diminta, lalu menjalankan satu WHERE id IN (...), kita mengubah N kueri menjadi satu. Penundaan itulah yang disediakan oleh GraphQL\Deferred.
<?php
$ids = [7, 7, 9];
$unique = array_values(array_unique($ids));
// One query instead of three:
echo 'SELECT * FROM users WHERE id IN (' . implode(',', $unique) . ")\n";
// SELECT * FROM users WHERE id IN (7,9)
Penyangga/Pemuatan Minimal
Inilah inti DataLoader: penyangga yang mengumpulkan id, memuatnya sekali melalui fungsi batch, lalu menyajikan hasil dari tembolok. Kunci yang sama yang diminta dua kali hanya dimuat sekali — penghapusan duplikasi otomatis.
<?php
class UserLoader {
private array $queue = [];
private array $cache = [];
public function __construct(private \Closure $batchFn) {}
public function add(int $id): void { $this->queue[$id] = true; }
public function loadOnce(): void {
$missing = array_diff(array_keys($this->queue), array_keys($this->cache));
if ($missing) {
foreach (($this->batchFn)(array_values($missing)) as $id => $row) {
$this->cache[$id] = $row;
}
}
$this->queue = [];
}
public function get(int $id): mixed { return $this->cache[$id] ?? null; }
}
Menyambungkannya ke Penyelesai
Penyelesai author memasukkan id ke antrean dan mengembalikan Deferred. graphql-php menjalankan semua penundaan setelah tingkat saat ini, jadi ketika closure dijalankan, setiap id author untuk seluruh daftar sudah masuk antrean. Satu loadOnce() memicu satu kueri berkelompok.
<?php
use GraphQL\Deferred;
$authorField = [
'type' => $userType,
'resolve' => function ($post, $args, $context) {
/** @var UserLoader $loader */
$loader = $context['userLoader'];
$loader->add($post['author_id']);
return new Deferred(function () use ($loader, $post) {
$loader->loadOnce(); // batches across all posts
return $loader->get($post['author_id']);
});
},
];
Menggunakan Pustaka overblog/dataloader
Anda jarang perlu membuatnya sendiri. overblog/dataloader-php adalah port mapan dari DataLoader milik Facebook. Anda memberinya fungsi batch yang menerima array kunci dan harus mengembalikan janji berisi nilai dalam urutan yang sama. Pustaka ini menangani penyimpanan dalam tembolok, penghapusan duplikasi, dan penyelesaian janji.
composer require overblog/dataloader-phpMembangun DataLoader
Kontrak fungsi batch sangat ketat: jika diberi [k1, k2, k3], fungsi tersebut harus menyelesaikan dengan [v1, v2, v3] berdasarkan posisi. Indekskan baris basis data berdasarkan kunci, lalu petakan kembali berdasarkan urutan masukan agar kunci yang tidak ditemukan menjadi null.
<?php
use Overblog\DataLoader\DataLoader;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use Overblog\PromiseAdapter\Adapter\WebonyxGraphQLSyncPromiseAdapter;
$adapter = new WebonyxGraphQLSyncPromiseAdapter(new SyncPromiseAdapter());
$userLoader = new DataLoader(function (array $ids) use ($adapter, $db) {
$rows = $db->usersByIds($ids); // SELECT ... WHERE id IN (...)
$byId = [];
foreach ($rows as $r) { $byId[$r['id']] = $r; }
// MUST return values in the SAME ORDER as $ids
$ordered = array_map(fn($id) => $byId[$id] ?? null, $ids);
return $adapter->createFulfilled($ordered);
}, $adapter);
Menyelesaikan Melalui Loader
Di dalam penyelesai, Anda cukup memanggil $loader->load($id), yang mengembalikan sebuah janji. graphql-php (melalui adaptor) mengumpulkan semua janji ini dan memicu batch secara otomatis di akhir putaran. Tidak perlu melakukan penyanggaan secara manual.
<?php
$authorField = [
'type' => $userType,
'resolve' => fn($post, $args, $context) =>
$context['userLoader']->load($post['author_id']),
];
Masa Hidup per Permintaan Sangat Penting
DataLoaders menyimpan data dalam tembolok berdasarkan kunci, jadi DataLoaders harus dibuat baru untuk setiap permintaan. Loader yang digunakan bersama oleh beberapa permintaan dapat menyajikan data usang dan membocorkan memori. Bangun loader saat Anda menyusun $context permintaan, lalu buang loader ketika permintaan berakhir.
<?php
// Per request: brand new loaders, attached to context
function buildContext($db, $currentUser): array {
return [
'db' => $db,
'user' => $currentUser,
'userLoader' => makeUserLoader($db), // fresh, not a singleton
'postLoader' => makePostLoader($db),
];
}
Perlindungan Performa Lainnya
DataLoader memperbaiki pembacaan N+1, tetapi kueri yang berbahaya atau ceroboh tetap dapat merugikan Anda. Tambahkan beberapa lapisan berikut:
- Pembatasan kedalaman kueri — aturan
QueryDepthmenolak kueri dengan tingkat penyarangan yang tidak wajar. - Kompleksitas kueri —
QueryComplexitymenetapkan anggaran biaya untuk setiap bidang. - Kueri tersimpan — hanya izinkan daftar operasi yang sudah diketahui.
- Paginasi — jangan pernah menyelesaikan daftar tanpa batas; gunakan koneksi kursor.
<?php
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\DocumentValidator;
DocumentValidator::addRule(new QueryDepth(10));
DocumentValidator::addRule(new QueryComplexity(200));
Mengukur Peningkatannya
Selalu ukur peningkatannya. Bungkus lapisan basis data Anda untuk menghitung kueri dalam sebuah pengujian, lalu tegaskan bahwa versi berkelompok menjalankan jumlah kueri yang terbatas, berapa pun ukuran daftarnya. Ini mencegah kemunduran ketika seseorang menambahkan penyelesai bertingkat yang naif dan secara diam-diam memasukkan kembali N+1.
<?php
class CountingDb {
public int $queries = 0;
public function usersByIds(array $ids): array {
$this->queries++; // one batched call
return array_map(fn($id) => ['id' => $id], $ids);
}
}
$db = new CountingDb();
$db->usersByIds([7, 9, 11, 13]); // 4 authors
echo "Queries for 4 authors: {$db->queries}\n"; // Queries for 4 authors: 1
Pemeriksaan Singkat
Mengapa DataLoaders harus dibuat untuk setiap permintaan?
Ringkasan
Anda telah menghilangkan jebakan performa terburuk GraphQL:
- Bidang daftar bertingkat menyebabkan N+1: satu kueri untuk setiap item.
- graphql-php menyelesaikan bidang tingkat demi tingkat, sehingga penundaan memungkinkan Anda mengelompokkan semua kunci ke dalam satu kueri
IN (...). GraphQL\Deferredadalah primitifnya;overblog/dataloader-phpmengemas pengelompokan + tembolok per kunci + penghapusan duplikasi.- Fungsi batch harus mengembalikan nilai dalam urutan yang sama dengan kunci masukan.
- Loader berlaku per permintaan; tambahkan batas kedalaman/kompleksitas dan paginasi sebagai perlindungan tambahan.
Belajar PHP dengan tutor AI — gratis
Tulis dan jalankan kode asli di browser kamu, dapatkan bantuan instan dari tutor AI 24/7, dan lanjutkan di mana kamu tinggalkan di web atau aplikasi.
- Kursus
- 49
- Pelajaran
- 195
Pertanyaan yang Sering Diajukan
Apakah pelajaran “Kinerja: N+1 dan DataLoader” gratis?
Ya — teks lengkap “Kinerja: N+1 dan DataLoader” gratis dibaca di sini di web. Untuk praktiknya secara interaktif (editor kode bawaan dan tutor AI 24/7) dan buka sisa kursus PHP Academy, upgrade ke CoddyKit PRO. Kursus PHP Academy mencakup 4 pelajaran total.
Apa yang akan aku pelajari di “Kinerja: N+1 dan DataLoader”?
Kelompokkan dan simpan hasil penyelesaian bidang dalam cache agar tetap cepat. Kamu berlatih PHP Academy dengan kode praktik yang langsung kamu jalankan di browser, dan tutor AI 24/7 menjawab pertanyaanmu saat kamu mengerjakan pelajaran ini.
Apakah aku perlu pengalaman untuk memulai PHP Academy?
Tidak diperlukan pengalaman sebelumnya. PHP Academy di CoddyKit dirancang untuk pemula hingga pelajar tingkat lanjut, jadi kamu bisa memulai di sini atau dari awal dan belajar sesuai kecepatan kamu sendiri. Ini adalah pelajaran 4 dari 4.
Berapa lama pelajaran “Kinerja: N+1 dan DataLoader” memakan waktu?
Sebagian besar pelajaran CoddyKit memakan waktu sekitar 5–10 menit. Setiap pelajaran ringkas dan interaktif, jadi kamu membuat kemajuan stabil dan melanjutkan dari tempat kamu tinggalkan di web dan aplikasi.
Bisakah aku menulis dan menjalankan kode dalam pelajaran PHP Academy ini?
Ya. Setiap pelajaran PHP Academy menyertakan editor kode bawaan, jadi kamu menulis dan menjalankan kode nyata langsung di browser dan mendapatkan umpan balik AI instan — tidak diperlukan penyiapan lokal.
Semua pelajaran dalam kursus ini
- GraphQL vs REST
- Membangun Skema dengan graphql-php
- Resolver, Mutasi, dan Langganan
- Kinerja: N+1 dan DataLoader