تحسين قاعدة البيانات والأداء
مقدمة إلى تحسين قاعدة البيانات
أداء قاعدة البيانات أمر حاسم لسرعة التطبيق وقابليته للتوسع. مع نمو تطبيقك، يمكن أن تصبح استعلامات قاعدة البيانات غير الفعالة عائقاً رئيسياً. يغطي هذا الدرس تقنيات التحسين الأساسية بما في ذلك حل مشكلة N+1، التحميل المسبق، الفهرسة، تحسين الاستعلامات، التخزين المؤقت، والاستخدام الصحيح لمعاملات قاعدة البيانات.
لماذا يهم تحسين قاعدة البيانات:
- يقلل من وقت تحميل الصفحة ويحسن تجربة المستخدم
- يخفض استهلاك موارد الخادم (CPU، الذاكرة، الإدخال/الإخراج)
- يسمح للتطبيق بالتعامل مع المزيد من المستخدمين المتزامنين
- يقلل من تكاليف خادم قاعدة البيانات واحتياجات البنية التحتية
- يحسن تحسين محركات البحث من خلال سرعات صفحة أسرع
- يمنع المهلات والأخطاء تحت حمل ثقيل
مشكلة استعلام N+1
تحدث مشكلة N+1 عندما تنفذ استعلاماً واحداً لجلب مجموعة، ثم تنفذ N استعلام إضافي (واحد لكل عنصر) لجلب البيانات المرتبطة. هذه واحدة من أكثر مشاكل الأداء شيوعاً في تطبيقات Laravel.
مثال على مشكلة N+1:
<?php
// سيء: مشكلة N+1 - 101 استعلام لـ 100 منشور
$posts = Post::all(); // استعلام واحد
foreach ($posts as $post) {
echo $post->user->name; // 100 استعلام إضافي (واحد لكل منشور)
}
تأثير الأداء: إذا كان لديك 100 منشور، ينفذ هذا الكود 101 استعلام قاعدة بيانات (1 للمنشورات + 100 للمستخدمين). مع 1000 منشور، يصبح 1001 استعلام!
الحل: التحميل المسبق
<?php
// جيد: التحميل المسبق - استعلامان فقط
$posts = Post::with('user')->get(); // استعلامان إجمالياً
foreach ($posts as $post) {
echo $post->user->name; // بدون استعلامات إضافية
}
// علاقات متعددة
$posts = Post::with(['user', 'comments', 'tags'])->get();
// علاقات متداخلة
$posts = Post::with(['comments.user', 'user.profile'])->get();
// تحميل مسبق شرطي
$posts = Post::with([
'comments' => function ($query) {
$query->where('approved', true)->orderBy('created_at', 'desc');
}
])->get();
التحميل المسبق الكسول
تحميل العلاقات بعد استرجاع النموذج الأصلي بالفعل:
<?php
$posts = Post::all();
// أدركت أنك تحتاج المستخدمين لاحقاً
$posts->load('user');
// تحميل علاقات متعددة
$posts->load(['user', 'comments']);
// تحميل مسبق كسول شرطي
if ($includeComments) {
$posts->load('comments');
}
كشف مشاكل N+1
<?php
// تثبيت Laravel Debugbar
composer require barryvdh/laravel-debugbar --dev
// أو استخدام تسجيل الاستعلامات
\Illuminate\Support\Facades\DB::enableQueryLog();
// الكود الخاص بك هنا
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name;
}
// التحقق من الاستعلامات المنفذة
$queries = \Illuminate\Support\Facades\DB::getQueryLog();
dd($queries); // رؤية جميع الاستعلامات المنفذة
الوقاية: قم بتمكين preventLazyLoading() في التطوير للقبض على مشاكل N+1 مبكراً:
// app/Providers/AppServiceProvider.php
public function boot()
{
Model::preventLazyLoading(!app()->isProduction());
}
فهرسة قاعدة البيانات
تسرع فهارس قاعدة البيانات بشكل كبير استرجاع البيانات ولكنها تبطئ الكتابة. فكر في الفهارس مثل فهرس الكتاب - تساعدك في العثور على المعلومات بسرعة دون قراءة كل صفحة.
متى تضيف الفهارس:
- المفاتيح الأجنبية (user_id، post_id، إلخ)
- الأعمدة المستخدمة في بنود WHERE بشكل متكرر
- الأعمدة المستخدمة في بنود ORDER BY
- الأعمدة المستخدمة في عمليات JOIN
- قيود الفريدة (البريد الإلكتروني، اسم المستخدم)
<?php
// إنشاء ترحيل مع الفهارس
php artisan make:migration add_indexes_to_posts_table
// في ملف الترحيل
public function up()
{
Schema::table('posts', function (Blueprint $table) {
// فهرسة عمود واحد
$table->index('user_id');
$table->index('status');
$table->index('published_at');
// فهرس مركب (أعمدة متعددة)
$table->index(['user_id', 'status']);
// فهرس فريد
$table->unique('slug');
// فهرس نص كامل (للبحث)
$table->fullText(['title', 'content']);
});
}
public function down()
{
Schema::table('posts', function (Blueprint $table) {
$table->dropIndex(['user_id']);
$table->dropIndex(['status']);
$table->dropIndex(['published_at']);
$table->dropIndex(['user_id', 'status']);
$table->dropUnique(['slug']);
$table->dropFullText(['title', 'content']);
});
}
<?php
// استخدام البحث عن النص الكامل مع الفهرس
$posts = Post::whereFullText(['title', 'content'], 'Laravel tutorial')
->get();
// بدون فهرس النص الكامل (أبطأ بكثير)
$posts = Post::where('title', 'like', '%Laravel%')
->orWhere('content', 'like', '%Laravel%')
->get();
مقايضات الفهرس: بينما تسرع الفهارس القراءات، فإنها تبطئ عمليات INSERT/UPDATE/DELETE لأن الفهرس يجب تحديثه أيضاً. لا تفرط في الفهرسة - أضف فهارس فقط للأعمدة التي تستعلم عنها فعلياً بشكل متكرر.
تحسين الاستعلامات
حدد فقط الأعمدة المطلوبة
<?php
// سيء: جلب جميع الأعمدة
$users = User::all();
// جيد: جلب الأعمدة المطلوبة فقط
$users = User::select('id', 'name', 'email')->get();
// في العلاقات
$posts = Post::with(['user:id,name,email'])->get();
استخدام التجزئة لمجموعات البيانات الكبيرة
<?php
// سيء: تحميل جميع السجلات في الذاكرة دفعة واحدة
$users = User::all(); // قد يسبب مشاكل في الذاكرة مع 100 ألف+ سجل
foreach ($users as $user) {
// معالجة المستخدم
}
// جيد: معالجة في قطع
User::chunk(1000, function ($users) {
foreach ($users as $user) {
// معالجة المستخدم
}
});
// أفضل: استخدام المجموعات الكسولة لكفاءة الذاكرة
User::lazy()->each(function ($user) {
// معالجة مستخدم واحد في كل مرة
});
العد بكفاءة
<?php
// سيء: تحميل جميع السجلات فقط للعد
$count = Post::all()->count();
// جيد: عد قاعدة البيانات (أسرع بكثير)
$count = Post::count();
// التحقق من الوجود
if (Post::where('user_id', $userId)->exists()) {
// المستخدم لديه منشورات
}
// بدلاً من
if (Post::where('user_id', $userId)->count() > 0) {
// المستخدم لديه منشورات
}
تجنب SELECT N+1 مع التجميعات
<?php
// سيء: N+1 للعد
$users = User::all();
foreach ($users as $user) {
echo $user->posts()->count(); // N استعلام
}
// جيد: استخدام withCount()
$users = User::withCount('posts')->get();
foreach ($users as $user) {
echo $user->posts_count; // بدون استعلامات إضافية
}
// عدة عدادات
$users = User::withCount(['posts', 'comments', 'likes'])->get();
// عد شرطي
$users = User::withCount([
'posts',
'posts as published_posts_count' => function ($query) {
$query->where('status', 'published');
}
])->get();
التخزين المؤقت للاستعلامات
قم بتخزين نتائج الاستعلامات باهظة الثمن مؤقتاً لتجنب الوصول إلى قاعدة البيانات بشكل متكرر:
<?php
use Illuminate\Support\Facades\Cache;
// تخزين نتائج الاستعلام مؤقتاً لمدة ساعة واحدة
$posts = Cache::remember('popular_posts', 3600, function () {
return Post::where('views', '>', 1000)
->with(['user', 'comments'])
->orderBy('views', 'desc')
->limit(10)
->get();
});
// التخزين المؤقت مع الوسوم (لمسح أسهل)
$posts = Cache::tags(['posts'])->remember('popular_posts', 3600, function () {
return Post::popular()->get();
});
// مسح ذاكرة التخزين المؤقت الموسومة
Cache::tags(['posts'])->flush();
// التخزين المؤقت للأبد (حتى المسح يدوياً)
$stats = Cache::rememberForever('site_stats', function () {
return [
'total_users' => User::count(),
'total_posts' => Post::count(),
];
});
// مسح ذاكرة التخزين المؤقت المحددة
Cache::forget('popular_posts');
نمط التخزين المؤقت للنموذج
<?php
// app/Models/Post.php
class Post extends Model
{
public static function popular()
{
return Cache::remember('posts:popular', 3600, function () {
return static::where('views', '>', 1000)
->orderBy('views', 'desc')
->limit(10)
->get();
});
}
// مسح ذاكرة التخزين المؤقت عند حفظ النموذج
protected static function booted()
{
static::saved(function () {
Cache::forget('posts:popular');
});
static::deleted(function () {
Cache::forget('posts:popular');
});
}
}
معاملات قاعدة البيانات
تضمن المعاملات سلامة البيانات من خلال تجميع عمليات قاعدة بيانات متعددة في وحدة ذرية واحدة:
<?php
use Illuminate\Support\Facades\DB;
// معاملة تلقائية (موصى به)
DB::transaction(function () {
$user = User::create(['name' => 'John', 'email' => 'john@example.com']);
$user->profile()->create(['bio' => 'Developer']);
$user->roles()->attach(1);
// إذا فشلت أي عملية، سيتم التراجع عن الجميع
});
// معاملة يدوية
DB::beginTransaction();
try {
$order = Order::create([
'user_id' => $userId,
'total' => $total,
]);
foreach ($items as $item) {
$order->items()->create($item);
}
Inventory::decrement($item['product_id'], $item['quantity']);
DB::commit(); // تثبيت إذا نجح كل شيء
} catch (\Exception $e) {
DB::rollBack(); // التراجع عند الخطأ
throw $e;
}
أفضل ممارسات المعاملات:
- حافظ على المعاملات قصيرة - لا تتضمن استدعاءات API خارجية
- لا تتداخل المعاملات دون داع
- استخدم المعاملات للعمليات التي يجب أن تنجح أو تفشل معاً
- تقفل المعاملات صفوف قاعدة البيانات - قلل من وقت القفل
أفضل ممارسات الترقيم
<?php
// ترقيم قياسي
$posts = Post::paginate(20); // 20 لكل صفحة
// ترقيم بسيط (أسرع، بدون عدد إجمالي)
$posts = Post::simplePaginate(20);
// ترقيم المؤشر (الأفضل لمجموعات البيانات الكبيرة)
$posts = Post::cursorPaginate(20);
// في وحدة التحكم
public function index()
{
$posts = Post::with('user')
->latest()
->paginate(20);
return view('posts.index', compact('posts'));
}
// في العرض
{{ $posts->links() }}
// عرض ترقيم مخصص
{{ $posts->links('pagination.custom') }}
نطاقات الاستعلام لإعادة الاستخدام
<?php
// app/Models/Post.php
class Post extends Model
{
// نطاق محلي
public function scopePublished($query)
{
return $query->where('status', 'published')
->where('published_at', '<=', now());
}
public function scopePopular($query, $threshold = 1000)
{
return $query->where('views', '>=', $threshold);
}
public function scopeByAuthor($query, $userId)
{
return $query->where('user_id', $userId);
}
// نطاق عالمي (يطبق على جميع الاستعلامات)
protected static function booted()
{
static::addGlobalScope('published', function ($query) {
$query->where('status', 'published');
});
}
}
// الاستخدام
$posts = Post::published()->popular()->get();
$myPosts = Post::published()->byAuthor(auth()->id())->get();
// إزالة النطاق العالمي عند الحاجة
$allPosts = Post::withoutGlobalScope('published')->get();
تمرين تطبيقي 1: تحسين قائمة منشورات المدونة
بالنظر إلى هذا الكود غير الفعال، قم بتحسينه:
<?php
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name;
echo $post->category->name;
echo $post->comments->count();
echo $post->tags->pluck('name')->implode(', ');
}
المتطلبات:
- إصلاح مشاكل N+1 بالتحميل المسبق
- استخدام withCount() لعدادات التعليقات
- إضافة ترقيم (20 لكل صفحة)
- تخزين النتائج مؤقتاً لمدة 5 دقائق
- تحديد الأعمدة المطلوبة فقط
تمرين تطبيقي 2: إضافة فهارس استراتيجية
أنشئ ترحيلاً لإضافة فهارس لجدول المنشورات هذا:
- أضف فهرساً لـ user_id (مفتاح أجنبي)
- أضف فهرساً لعمود الحالة
- أضف فهرساً مركباً لـ (status، published_at)
- أضف فهرساً فريداً للـ slug
- أضف فهرس نص كامل للعنوان والمحتوى
الاختبار: اكتب استعلامات تستفيد من هذه الفهارس واستخدم EXPLAIN للتحقق من استخدام الفهرس.
تمرين تطبيقي 3: معالجة الطلبات القائمة على المعاملات
نفذ عملية الخروج باستخدام معاملات قاعدة البيانات:
- إنشاء سجل الطلب
- إنشاء عناصر الطلب
- تقليل مخزون المنتج
- تسجيل معاملة الدفع
- إرسال تأكيد الطلب (خارج المعاملة)
المتطلبات:
- يجب أن تنجح جميع عمليات قاعدة البيانات أو تفشل جميعها
- التحقق من توفر المخزون قبل المعالجة
- التعامل مع الأخطاء بشكل صحيح مع التراجع
- تسجيل أي فشل في المعاملات
الخلاصة
في هذا الدرس، تعلمت تقنيات تحسين قاعدة البيانات الأساسية:
- تحديد وحل مشاكل استعلام N+1 بالتحميل المسبق
- استخدام فهارس قاعدة البيانات لتسريع الاستعلامات
- تحسين الاستعلامات من خلال تحديد الأعمدة المطلوبة فقط
- التجزئة والمجموعات الكسولة لمعالجة مجموعات البيانات الكبيرة
- استخدام withCount() لتجنب N+1 مع التجميعات
- تنفيذ التخزين المؤقت لنتائج الاستعلامات مع واجهة Cache في Laravel
- استخدام معاملات قاعدة البيانات لسلامة البيانات
- تنفيذ استراتيجيات ترقيم فعالة
- إنشاء نطاقات استعلام قابلة لإعادة الاستخدام
- أفضل الممارسات لأداء قاعدة البيانات
تحسين قاعدة البيانات أمر حاسم لبناء تطبيقات قابلة للتوسع. ستحسن هذه التقنيات بشكل كبير أداء تطبيقك وتجربة المستخدم. في الدرس التالي، سنبني تطبيق CRUD متكامل يجمع كل هذه المفاهيم معاً.