ประสิทธิภาพ: N+1 และ DataLoader
รวมกลุ่มและแคชการแก้ค่าฟิลด์เพื่อให้ทำงานได้รวดเร็ว
ประสิทธิภาพ: N+1 และ DataLoader เป็นบทเรียน PHP Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน PHP Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส PHP Academy มีบทเรียนทั้งหมด 4 บทเรียน
ตัวการเงียบที่ร้ายแรง: N+1
กับดักด้านประสิทธิภาพที่ใหญ่ที่สุดของ GraphQL คือ ปัญหาคิวรี N+1 ปัญหานี้ไม่ส่งสัญญาณเตือน เพราะตัวแก้ไขแต่ละตัวดูไม่มีปัญหาเมื่อพิจารณาแยกกัน — แต่เมื่อซ้อนรายการเข้ากับช่องข้อมูลต่อรายการ คุณจะส่งคิวรีหนึ่งครั้งสำหรับรายการ และอีกหนึ่งครั้งต่อรายการ เมื่อมี 100 รายการ นั่นเท่ากับการเดินทางไปกลับ 101 ครั้ง บทเรียนนี้แสดงวิธีรวมให้เหลือคิวรีแบบชุดเพียงไม่กี่ครั้งด้วย DataLoader
เห็น N+1 อย่างเป็นรูปธรรม
ลองพิจารณา { posts { author { name } } } ตัวแก้ไข posts จะทำงานด้วยคิวรีหนึ่งครั้ง จากนั้นสำหรับโพสต์แต่ละรายการ ตัวแก้ไข author จะทำคิวรีของตัวเอง โค้ดแบบตรงไปตรงมาด้านล่างทำให้เห็นต้นทุนอย่างชัดเจน
<?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
ข้อสังเกตสำคัญ: รวมเป็นชุดตามระดับ
GraphQL ประมวลผลทีละระดับ ตัวแก้ไข author ทั้งหมดสำหรับรายการโพสต์จะทำงานในจังหวะการประมวลผลเดียวกัน หากเราเลื่อนการค้นหาผู้เขียนแต่ละรายการ รวบรวมรหัสที่ร้องขอ แล้วส่งคิวรี WHERE id IN (...) เพียงครั้งเดียวได้ เราจะเปลี่ยนจาก N คิวรีให้เหลือคิวรีเดียว การเลื่อนการทำงานนี้คือสิ่งที่ 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)
บัฟเฟอร์/ตัวโหลดแบบพื้นฐาน
หัวใจของ DataLoader มีดังนี้: บัฟเฟอร์ที่สะสมรหัส โหลดรหัสเหล่านั้นเพียงครั้งเดียวผ่านฟังก์ชันแบบชุด และส่งผลลัพธ์จากแคช คีย์เดียวกันที่ถูกร้องขอสองครั้งจะถูกโหลดเพียงครั้งเดียว — ระบบกำจัดรายการซ้ำโดยอัตโนมัติ
<?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; }
}
เชื่อมต่อเข้ากับตัวแก้ไข
ตัวแก้ไข author จะเข้าคิวรหัสและส่งคืน Deferred graphql-php จะเรียกใช้งานที่เลื่อนไว้ทั้งหมดหลังจากระดับปัจจุบัน ดังนั้นเมื่อฟังก์ชันปิดทำงาน รหัสผู้เขียนทั้งหมดของรายการทั้งชุดก็จะถูกเข้าคิวแล้ว การเรียกใช้ loadOnce() หนึ่งครั้งจะเรียกคิวรีแบบชุดเพียงครั้งเดียว
<?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']);
});
},
];
ใช้ไลบรารี overblog/dataloader
คุณแทบไม่จำเป็นต้องเขียนสิ่งนี้เอง overblog/dataloader-php เป็นพอร์ตที่ใช้งานกันอย่างแพร่หลายของ DataLoader จาก Facebook คุณเพียงส่งฟังก์ชันแบบชุดที่รับอาร์เรย์ของคีย์และต้องส่งคืนพรอมิสของค่าตามลำดับเดียวกัน ไลบรารีจะจัดการแคช การกำจัดรายการซ้ำ และการแก้ค่าพรอมิสให้
composer require overblog/dataloader-phpสร้าง DataLoader
สัญญาของฟังก์ชันแบบชุดเข้มงวดมาก: เมื่อได้รับ [k1, k2, k3] ฟังก์ชันต้องแก้ค่าเป็น [v1, v2, v3] ตามตำแหน่ง จัดทำดัชนีแถวในฐานข้อมูลด้วยคีย์ แล้วจัดเรียงกลับตามลำดับข้อมูลเข้า เพื่อให้คีย์ที่ไม่พบกลายเป็น 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);
แก้ค่าผ่านตัวโหลด
ในตัวแก้ไข คุณเพียงเรียก $loader->load($id) ซึ่งส่งคืนพรอมิส graphql-php (ผ่านอะแดปเตอร์) จะรวบรวมพรอมิสเหล่านี้และเรียกใช้ฟังก์ชันแบบชุดโดยอัตโนมัติเมื่อจังหวะการประมวลผลจบลง ไม่ต้องจัดการบัฟเฟอร์เอง
<?php
$authorField = [
'type' => $userType,
'resolve' => fn($post, $args, $context) =>
$context['userLoader']->load($post['author_id']),
];
อายุการใช้งานต่อคำขอสำคัญอย่างยิ่ง
DataLoaders แคชตามคีย์ ดังนั้นจึงต้องสร้างใหม่สำหรับทุกคำขอ ตัวโหลดที่ใช้ร่วมกันข้ามคำขอจะส่งข้อมูลเก่าและทำให้หน่วยความจำรั่วไหล สร้างตัวโหลดเมื่อประกอบบริบทคำขอ $context และทิ้งตัวโหลดเมื่อคำขอสิ้นสุดลง
<?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),
];
}
กลไกป้องกันด้านประสิทธิภาพอื่น ๆ
DataLoader ช่วยแก้ปัญหาการอ่านแบบ N+1 แต่คิวรีที่เป็นอันตรายหรือเขียนอย่างไม่ระมัดระวังก็ยังสร้างความเสียหายได้ ควรเพิ่มชั้นป้องกันดังนี้:
- จำกัดความลึกของคิวรี — กฎ
QueryDepthปฏิเสธคิวรีที่ซ้อนกันผิดปกติ - ความซับซ้อนของคิวรี —
QueryComplexityกำหนดงบประมาณต้นทุนต่อช่องข้อมูล - คิวรีที่บันทึกไว้ — อนุญาตเฉพาะการดำเนินการที่อยู่ในรายการอนุญาตที่ทราบเท่านั้น
- การแบ่งหน้า — อย่าประมวลผลรายการที่ไม่มีขอบเขต ใช้การเชื่อมต่อแบบเคอร์เซอร์
<?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));
วัดผลลัพธ์ที่ได้
จงวัดการปรับปรุงเป็นตัวเลขเสมอ ห่อชั้นฐานข้อมูลเพื่อให้นับจำนวนคิวรีในการทดสอบ จากนั้นตรวจสอบว่ารุ่นที่รวมเป็นชุดส่งคิวรีไม่เกินจำนวนที่กำหนด ไม่ว่ารายการจะมีขนาดเท่าใด วิธีนี้ช่วยป้องกันการถดถอยเมื่อมีผู้เพิ่มตัวแก้ไขแบบซ้อนที่ทำงานตรงไปตรงมา และทำให้ปัญหา 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
ตรวจสอบอย่างรวดเร็ว
เหตุใดจึงต้องสร้าง DataLoaders แยกสำหรับแต่ละคำขอ
สรุป
คุณกำจัดกับดักด้านประสิทธิภาพที่เลวร้ายที่สุดของ GraphQL ได้แล้ว:
- ช่องข้อมูลรายการแบบซ้อนทำให้เกิด N+1: มีคิวรีหนึ่งครั้งต่อรายการ
- graphql-php ประมวลผลทีละระดับ ดังนั้นการเลื่อนการทำงานจึงทำให้รวมคีย์ทั้งหมดเป็นคิวรี
IN (...)เดียวได้ GraphQL\Deferredเป็นพื้นฐาน ส่วนoverblog/dataloader-phpรวมการทำงานเป็นชุด + การแคชแยกตามคีย์ + การกำจัดรายการซ้ำไว้ให้- ฟังก์ชันแบบชุดต้องส่งคืนค่าตามลำดับเดียวกับคีย์ข้อมูลเข้า
- ตัวโหลดมีอายุการใช้งานต่อคำขอ ควรเพิ่มข้อจำกัดด้านความลึก/ความซับซ้อนและการแบ่งหน้าเป็นกลไกป้องกันเพิ่มเติม
คำถามที่พบบ่อย
บทเรียน “ประสิทธิภาพ: N+1 และ DataLoader” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ประสิทธิภาพ: N+1 และ DataLoader” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส PHP Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส PHP Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ประสิทธิภาพ: N+1 และ DataLoader”
รวมกลุ่มและแคชการแก้ค่าฟิลด์เพื่อให้ทำงานได้รวดเร็ว คุณปฏิบัติ PHP Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน PHP Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน PHP Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “ประสิทธิภาพ: N+1 และ DataLoader” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน PHP Academy นี้ได้ไหม
ได้ บทเรียน PHP Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- GraphQL เทียบกับ REST
- การสร้างสคีมาด้วย graphql-php
- ตัวแก้ไข การกลายพันธุ์ และการสมัครรับข้อมูล
- ประสิทธิภาพ: N+1 และ DataLoader