عند تطوير تطبيق Laravel صغير يحتوي على بضع مئات من السجلات، قد لا تلاحظ مشكلة في كتابة استعلام يقوم بجلب جميع البيانات من قاعدة البيانات باستخدام get(). لكن عندما ينتقل التطبيق إلى بيئة Production ويصبح جدول واحد يحتوي على عشرات أو مئات الآلاف من السجلات، قد يتحول استعلام بسيط ظاهريًا إلى سبب مباشر في بطء التطبيق وارتفاع استهلاك الذاكرة، بل وقد يؤدي إلى ظهور خطأ:
Allowed memory size exhaustedالمشكلة هنا ليست Laravel بحد ذاته، وإنما طريقة جلب البيانات ومعالجتها.
في هذا الدليل سنبدأ بمثال واقعي، ثم ننتقل تدريجيًا من الحل البسيط باستخدام select() إلى تقنيات أكثر ملاءمة لتطبيقات Production مثل paginate() وchunkById() وlazy()، مع توضيح متى يجب تنفيذ عملية التجميع داخل قاعدة البيانات بدلًا من PHP.
القاعدة الأساسية ليست: "اجلب البيانات ثم قرر ماذا ستستخدم منها"، وإنما: اجلب أقل كمية من البيانات التي يحتاجها الطلب فعلًا.
المشكلة الواقعية: صفحة أرشيف الكتب
لنفترض أن لدينا منصة كتب تحتوي على جدول books، وكل كتاب مرتبط بمؤلف داخل جدول authors.
نريد إنشاء صفحة تعرض الكتب مرتبة ومجمعة حسب سنة النشر:
2026
├── Clean Architecture with Laravel
├── Advanced PHP
└── Building APIs with Laravel
2025
├── Database Design
└── Modern Backend Development
2024
└── PHP Performance Guideقد يحتوي جدول books في البداية على 500 كتاب فقط، لكن بعد عدة سنوات قد يصل العدد إلى 100,000 أو حتى مليون كتاب. وهنا تبدأ القرارات الصغيرة في الاستعلام بالتأثير بشكل واضح على استهلاك الذاكرة والأداء.
الحل الأول: الطريقة التقليدية
قد نكتب Controller بالشكل التالي:
use App\Models\Book;
use Carbon\Carbon;
public function index()
{
$years = Book::query()
->with('author')
->latest('published_at')
->get()
->groupBy(function ($book) {
return Carbon::parse($book->published_at)->format('Y');
});
return view('books', compact('years'));
}وفي Blade:
<div class="row">
@foreach($years as $year => $books)
<div class="col-4">
<div class="card mt-2">
<div class="card-header">
{{ $year }}
</div>
<ul class="list-group list-group-flush">
@foreach($books as $book)
<li class="list-group-item">
{{ \Illuminate\Support\Str::limit($book->title, 40) }}
</li>
@endforeach
</ul>
</div>
</div>
@endforeach
</div>الكود يعمل، والنتيجة صحيحة، ولذلك قد يبدو أنه لا توجد مشكلة.
لكن علينا النظر إلى ما يحدث فعليًا داخل التطبيق.
تدفق البيانات في هذا الاستعلام
HTTP Request
│
▼
Laravel Controller
│
▼
SELECT books.*
│
▼
جلب جميع الكتب
│
├──────────────► SELECT authors.*
│
▼
إنشاء Book Models
│
▼
إنشاء Author Models
│
▼
Collection في ذاكرة PHP
│
▼
groupBy() داخل PHP
│
▼
Collection جديدة/مجمعة
│
▼
Blade
│
▼
HTML Responseلاحظ أن قاعدة البيانات لا تقوم هنا بتجميع الكتب حسب السنة. Laravel يقوم أولًا بجلب النتائج، ثم يتم تنفيذ groupBy() الخاص بالـ Collection داخل PHP.
أين يحدث استهلاك الذاكرة؟
عند تنفيذ:
Book::get();لا تحصل فقط على البيانات الخام القادمة من MySQL، بل يقوم Eloquent بتحويل النتائج إلى Models يمكن التعامل معها داخل التطبيق.
وعند استخدام:
->with('author')سيتم أيضًا تحميل بيانات العلاقة وإنشاء Models للمؤلفين.
بالتالي فإن حجم البيانات الموجودة داخل ذاكرة PHP قد يكون أكبر بكثير من حجم الصفوف الخام التي تتخيل أنك قمت بجلبها من قاعدة البيانات.
المشكلة الأولى: SELECT *
الاستعلام الافتراضي سيجلب جميع أعمدة الكتاب تقريبًا:
SELECT * FROM books;لنفترض أن الجدول يحتوي على:
id
title
description
content
cover
isbn
published_at
author_id
metadata
created_at
updated_atلكن الصفحة تحتاج فعليًا إلى:
id
title
published_at
author_idإذا كان description أو content أو metadata يحتوي على بيانات كبيرة، فإن تحميلها لكل سجل بدون استخدامها يعتبر هدرًا غير ضروري للذاكرة ونقل البيانات.
المشكلة الثانية: تحميل جميع أعمدة العلاقة
الأمر نفسه يحدث عند كتابة:
->with('author')إذا كان جدول المؤلف يحتوي على عدة أعمدة، بينما تحتاج الصفحة إلى اسم المؤلف فقط، فلا يوجد سبب لتحميل بقية البيانات.
المشكلة الثالثة والأهم: get()
حتى بعد تحديد الأعمدة المطلوبة، تبقى هناك مشكلة إذا كان عدد السجلات كبيرًا:
->get()لأنك تطلب من التطبيق تحميل كامل مجموعة النتائج التي يعيدها الاستعلام إلى الذاكرة.
إذا كانت لديك 100 نتيجة، فهذا طبيعي. أما إذا كانت لديك 500,000 نتيجة، فقد يصبح الأمر مكلفًا جدًا.
التحسين الأول: جلب الأعمدة المطلوبة فقط
بدلًا من:
$books = Book::query()
->with('author')
->get();يمكن كتابة:
$books = Book::query()
->select([
'id',
'title',
'published_at',
'author_id',
])
->with('author:id,name')
->get();بهذا أصبح الاستعلام أقرب إلى:
SELECT
id,
title,
published_at,
author_id
FROM books;واستعلام المؤلفين سيطلب فقط الحقول التي حددناها للعلاقة.
عند تقييد أعمدة العلاقات في Eloquent، احرص على تضمين المفاتيح اللازمة لربط العلاقة، مثل المفتاح الأساسي والمفتاح الأجنبي عند الحاجة.
لماذا نحتاج author_id؟
من الأخطاء الشائعة كتابة:
Book::select('id', 'title', 'published_at')
->with('author:id,name')
->get();مع نسيان:
author_idEloquent يحتاج المفتاح الأجنبي الموجود في الكتاب لمعرفة المؤلف المرتبط به.
لذلك يجب أن يكون ضمن الأعمدة المحددة:
->select([
'id',
'title',
'published_at',
'author_id',
])نسخة محسنة من المثال الأصلي
use App\Models\Book;
public function index()
{
$years = Book::query()
->select([
'id',
'title',
'published_at',
'author_id',
])
->with('author:id,name')
->latest('published_at')
->get()
->groupBy(fn ($book) => $book->published_at->format('Y'));
return view('books', compact('years'));
}إذا كان published_at معرفًا كـ cast في Model:
protected function casts(): array
{
return [
'published_at' => 'datetime',
];
}فلا نحتاج إلى تنفيذ:
Carbon::parse($book->published_at)في كل دورة. يمكن استخدام:
$book->published_at->format('Y')مباشرة، وهو أيضًا أوضح من ناحية تصميم الكود.
هل select() يجعل التطبيق أسرع دائمًا؟
ليس بالضرورة.
استخدام select() يقلل كمية البيانات المطلوبة، وقد يقلل استهلاك الذاكرة وحجم البيانات المنقولة من قاعدة البيانات، خصوصًا عندما يحتوي الجدول على أعمدة كبيرة.
لكن إذا كان الجدول يحتوي فقط على:
id
title
published_atفقد يكون الفرق محدودًا.
أما إذا كان يحتوي على:
LONGTEXT
JSON
TEXT
BLOB
large metadataفقد يصبح الفرق كبيرًا.
لا تعتمد على أرقام ثابتة مثل "انخفض الاستهلاك من 31MB إلى 4MB" كقاعدة عامة. النتيجة تختلف حسب عدد السجلات، حجم الأعمدة، إصدار PHP وLaravel، العلاقات المحملة وإعدادات البيئة.
كيف نقيس استهلاك الذاكرة بشكل صحيح؟
يمكن استخدام أدوات مثل Laravel Debugbar أثناء التطوير، لكن يمكن أيضًا إجراء قياسات مباشرة باستخدام PHP.
$startMemory = memory_get_usage(true);
$startTime = microtime(true);
$books = Book::query()
->select([
'id',
'title',
'published_at',
'author_id',
])
->with('author:id,name')
->get();
$endMemory = memory_get_usage(true);
$endTime = microtime(true);
logger()->info('Books benchmark', [
'memory_mb' => round(
($endMemory - $startMemory) / 1024 / 1024,
2
),
'peak_memory_mb' => round(
memory_get_peak_usage(true) / 1024 / 1024,
2
),
'execution_ms' => round(
($endTime - $startTime) * 1000,
2
),
'records' => $books->count(),
]);هذا يسمح لنا بمقارنة أكثر من تنفيذ بدل الاعتماد على الانطباع.
ما الذي يجب قياسه؟
Memory Usage
Peak Memory
Execution Time
Database Query Time
Number of Queries
Number of Records
Response Sizeلأن انخفاض الذاكرة وحده لا يعني بالضرورة أن الحل أصبح أسرع في جميع الحالات.
لكن ماذا لو كان لدينا مليون كتاب؟
هنا نصل إلى النقطة الأهم.
حتى لو استخدمت:
select('id', 'title')ثم نفذت:
->get()على مليون سجل، فأنت ما زلت تحاول تحميل عدد ضخم من النتائج إلى ذاكرة التطبيق.
وهذا يعني أن select() تحسين مهم، لكنه ليس استراتيجية لمعالجة البيانات الضخمة بمفرده.
الحل في صفحات العرض: Pagination
إذا كان المستخدم لا يحتاج إلى مشاهدة جميع الكتب دفعة واحدة، فلا تجلبها جميعًا.
$books = Book::query()
->select([
'id',
'title',
'published_at',
'author_id',
])
->with('author:id,name')
->latest('published_at')
->paginate(50);الآن أصبح التدفق:
Database
│
▼
50 Books
│
▼
Laravel
│
▼
Blade
│
▼
User
Next Page
│
▼
50 Books أخرىبدلًا من تحميل عشرات الآلاف من الكتب في Request واحد.
متى نستخدم Pagination؟
تعتبر مناسبة جدًا في:
- لوحات التحكم.
- قوائم المستخدمين.
- المنتجات.
- الطلبات.
- المقالات.
- سجلات العمليات.
- نتائج البحث.
ماذا عن معالجة البيانات في الخلفية؟
لنفترض أن لدينا 2,000,000 كتاب ونريد تنفيذ عملية عليهم، مثل تحديث بيانات أو تصدير سجلات.
هذه الطريقة خطرة:
$books = Book::all();
foreach ($books as $book) {
// processing
}لأن جميع السجلات يتم تحميلها إلى الذاكرة.
استخدام chunkById()
في عمليات المعالجة الكبيرة يمكن معالجة البيانات على دفعات:
Book::query()
->select([
'id',
'title',
'published_at',
])
->chunkById(1000, function ($books) {
foreach ($books as $book) {
// Process book...
}
});بدلًا من:
2,000,000 records
│
▼
Memoryنصبح أقرب إلى:
Database
│
├── 1000 records ──► Process
│
├── 1000 records ──► Process
│
├── 1000 records ──► Process
│
├── 1000 records ──► Process
│
└── ...وبالتالي لا نحتاج إلى الاحتفاظ بكامل dataset في الذاكرة في اللحظة نفسها.
متى نستخدم lazy() أو lazyById()؟
إذا أردنا التعامل مع النتائج بأسلوب قريب من Collection مع الحفاظ على استهلاك أكثر انضباطًا للذاكرة، يمكن استخدام Lazy Collections.
Book::query()
->select([
'id',
'title',
'published_at',
])
->lazyById(1000)
->each(function (Book $book) {
// Process one book...
});هذا الأسلوب مفيد في عمليات مثل:
- تصدير البيانات.
- معالجة السجلات القديمة.
- Jobs طويلة.
- عمليات المزامنة.
- إعادة بناء بيانات مشتقة.
هل يجب أن نقوم بالتجميع داخل PHP أصلًا؟
في المثال:
->get()
->groupBy(...)قاعدة البيانات تعيد الكتب، ثم PHP يقوم بالتجميع.
لكن إذا كان المطلوب إحصائية فقط، مثل عدد الكتب المنشورة في كل سنة، فلا يوجد سبب لتحميل جميع Models إلى PHP.
مثال غير فعال للإحصائيات
$years = Book::all()
->groupBy(fn ($book) => $book->published_at->format('Y'))
->map
->count();إذا كان لدينا مليون كتاب، فهذا يعني تحميل مليون Model فقط لحساب عدد الكتب في كل سنة.
دع قاعدة البيانات تنفذ التجميع
في MySQL مثلًا:
use Illuminate\Support\Facades\DB;
$years = Book::query()
->selectRaw('YEAR(published_at) as year, COUNT(*) as total')
->whereNotNull('published_at')
->groupByRaw('YEAR(published_at)')
->orderByDesc('year')
->get();فتصبح النتيجة:
[
{ year: 2026, total: 18432 },
{ year: 2025, total: 15721 },
{ year: 2024, total: 12987 }
]بدل نقل عشرات الآلاف من الصفوف إلى PHP.
مقارنة تدفق البيانات
التجميع في PHP:
Database
│
▼
100,000 rows
│
▼
Network / DB Driver
│
▼
100,000 Eloquent Models
│
▼
PHP Memory
│
▼
groupBy()
│
▼
3 أو 10 مجموعاتالتجميع في قاعدة البيانات:
Database
│
▼
GROUP BY
│
▼
10 rows
│
▼
Laravel
│
▼
Responseإذا كانت قاعدة البيانات تستطيع تقليل dataset قبل إرساله إلى التطبيق، فغالبًا من الأفضل أن تقوم بذلك هناك، خصوصًا في عمليات العد والتجميع والتصفية.
مشكلة N+1 التي يجب الانتباه إليها
لنفترض أننا كتبنا:
$books = Book::query()
->select('id', 'title', 'author_id')
->get();
foreach ($books as $book) {
echo $book->author->name;
}إذا لم تكن العلاقة محملة مسبقًا، فقد ينتج عن ذلك استعلام إضافي أثناء الوصول إلى علاقة كل كتاب.
الحل:
$books = Book::query()
->select([
'id',
'title',
'author_id',
])
->with('author:id,name')
->get();بهذا نجمع بين تحسينين مختلفين:
Selective Columns
+
Eager Loading
│
▼
بيانات أقل + استعلامات أكثر انضباطًاخطأ Production شائع: تحسين الذاكرة على حساب عدد الاستعلامات
قد يقرر مطور إزالة:
->with('author')لأنها تستهلك ذاكرة إضافية.
لكن إذا كانت الصفحة تحتاج إلى بيانات المؤلف، فقد يؤدي ذلك إلى مشكلة N+1.
لذلك لا يمكن تقييم الأداء باستخدام مؤشر واحد فقط.
يجب مراقبة:
Memory
Queries
Query Time
CPU
Response Time
Transferred Dataماذا نستخدم في كل سيناريو؟
سيناريو 1: صفحة تحتوي على 30 كتابًا
select()
+
with()
+
get()حل طبيعي ولا داعي لتعقيد الكود.
سيناريو 2: صفحة تحتوي على آلاف الكتب
select()
+
with()
+
paginate()لا يوجد سبب عملي غالبًا لعرض آلاف العناصر في HTML دفعة واحدة.
سيناريو 3: معالجة مليون سجل
select()
+
chunkById()أو:
select()
+
lazyById()سيناريو 4: حساب إحصائيات
استخدم SQL قدر الإمكان:
COUNT()
SUM()
AVG()
MIN()
MAX()
GROUP BYبدل تحميل كامل البيانات ثم حساب النتيجة في PHP.
سيناريو 5: API
إذا كانت API تحتاج:
{
"id": 18,
"title": "Laravel Performance"
}فلا يوجد سبب منطقي لجلب:
content
description
internal_notes
metadata
created_at
updated_at
...من قاعدة البيانات إذا لم تكن مطلوبة أصلًا.
استخدام API Resources لا يغني عن select()
هذه نقطة مهمة.
إذا كتبت:
return [
'id' => $this->id,
'title' => $this->title,
];داخل Resource، فهذا يحدد البيانات التي سيتم إرسالها للمستخدم، لكنه لا يعني بالضرورة أن قاعدة البيانات لم تجلب بقية الأعمدة.
هناك فرق بين:
Database Selection
│
▼
select()
Response Transformation
│
▼
API Resourceالأول يتحكم فيما تطلبه من قاعدة البيانات، والثاني يتحكم فيما ترسله إلى العميل.
ماذا عن Indexes؟
تقليل الأعمدة لا يحل جميع مشاكل الأداء.
في مثالنا نستخدم:
ORDER BY published_at DESCومع نمو الجدول، يجب أيضًا دراسة الفهارس المناسبة للاستعلامات المتكررة.
مثلًا قد يكون وجود index على:
published_atمفيدًا لبعض أنماط الاستعلام.
لكن قرار إضافة Index يجب أن يعتمد على الاستعلامات الحقيقية وخطة التنفيذ وليس على قاعدة عامة.
يمكن تحليل الاستعلام باستخدام:
EXPLAIN SELECT ...أو الأدوات التي توفرها قاعدة البيانات المستخدمة.
مثال Production أكثر واقعية
لنفترض أن لدينا متجرًا يحتوي على 500,000 طلب. لوحة الإدارة تريد عرض آخر الطلبات مع اسم العميل.
تنفيذ مثل:
$orders = Order::with('customer')
->latest()
->get();تصميم سيئ لصفحة ويب عادية، لأنك لا تحتاج إلى نصف مليون طلب في Request واحد.
تنفيذ أكثر منطقية:
$orders = Order::query()
->select([
'id',
'customer_id',
'status',
'total',
'created_at',
])
->with([
'customer:id,name,email'
])
->latest('id')
->paginate(50);تدفق البيانات:
500,000 Orders in Database
│
▼
SQL Filtering
│
▼
Required Columns
│
▼
50 Records
│
▼
Eloquent
│
▼
Blade
│
▼
Browserهذه هي الفكرة التي يجب أن نبني عليها استعلامات Production:
ليس المهم كم عدد السجلات الموجودة في قاعدة البيانات، بل كم سجلًا وكم بايتًا يحتاج هذا الـ Request تحديدًا.
أخطاء شائعة عند تحسين استعلامات Laravel
1. استخدام get() في كل مكان
لا تستخدم get() تلقائيًا قبل التفكير في حجم النتائج.
2. استخدام SELECT * بدون حاجة
خصوصًا في الجداول التي تحتوي على أعمدة كبيرة.
3. نسيان المفاتيح عند استخدام select()
حذف author_id مثلًا قد يمنع Eloquent من ربط العلاقة بصورة صحيحة.
4. حل N+1 بتحميل علاقات ضخمة بلا داعٍ
Eager Loading مهم، لكن حمّل العلاقات والأعمدة التي يحتاجها السيناريو فقط.
5. تنفيذ Aggregation في PHP
لا تحمل مليون سجل لتنفيذ count() أو sum() إذا كان SQL يستطيع إعطاءك النتيجة مباشرة.
6. افتراض أن select() يحل مشكلة البيانات الضخمة
هو يقلل حجم كل سجل، لكنه لا يمنعك من تحميل مليون سجل إذا استخدمت get().
7. الاعتماد على أرقام Benchmark من جهاز آخر
رقم مثل:
31 MB → 4 MBقد يكون صحيحًا في اختبار معين، لكنه ليس نتيجة يمكن ضمانها في كل تطبيق.
قم بالقياس داخل بيئتك وببيانات قريبة من Production.
قائمة مراجعة قبل إطلاق الاستعلام إلى Production
[ ] هل أحتاج فعلًا إلى جميع السجلات؟
[ ] هل أحتاج جميع الأعمدة؟
[ ] هل العلاقات المطلوبة محددة؟
[ ] هل حددت أعمدة العلاقات؟
[ ] هل أحتاج Pagination؟
[ ] هل العملية الكبيرة تحتاج chunkById أو lazyById؟
[ ] هل يمكن تنفيذ aggregation داخل SQL؟
[ ] هل يوجد N+1؟
[ ] هل الاستعلام يحتاج Index مناسبًا؟
[ ] هل قمت بقياس Peak Memory؟
[ ] هل قمت بقياس Query Time؟
[ ] هل اختبرت الاستعلام بكمية بيانات قريبة من Production؟الخلاصة
تحسين استهلاك الذاكرة في Laravel لا يعني إضافة select() إلى الاستعلام وانتهاء المشكلة. الأداء الجيد يبدأ من فهم دورة حياة البيانات منذ لحظة طلبها من قاعدة البيانات وحتى إرسال النتيجة إلى المستخدم.
ابدأ دائمًا بالسؤال:
ما أقل كمية من البيانات التي يحتاجها هذا الـ Request لتنفيذ مهمته؟
إذا كنت تحتاج أربعة أعمدة، فلا تجلب عشرين. إذا كنت تحتاج 50 سجلًا، فلا تجلب 50 ألفًا. إذا كنت تحتاج عدد السجلات، فلا تحمل السجلات نفسها. وإذا كنت تعالج ملايين الصفوف، فلا تضعها جميعًا في الذاكرة في الوقت نفسه.
يمكن تلخيص الاستراتيجية كالتالي:
Small Dataset
│
└── select() + with() + get()
Large UI Dataset
│
└── select() + with() + paginate()
Large Processing Job
│
├── chunkById()
│
└── lazyById()
Statistics / Reports
│
└── SQL Aggregation
Related Data
│
└── Eager Loading + Required Columns
Performance Problem
│
└── Measure → Analyze → Optimize → Measure Againبهذه الطريقة يصبح تحسين الذاكرة جزءًا من تصميم التطبيق نفسه، وليس محاولة إصلاح المشكلة بعد وصول السيرفر إلى حد memory_limit.
مراجع تقنية
للمزيد من التفاصيل، يُنصح بالرجوع إلى التوثيق الرسمي لـ Laravel حول Eloquent وQuery Builder وطرق التعامل مع مجموعات البيانات الكبيرة، بالإضافة إلى توثيق PHP الرسمي الخاص بقياس استهلاك الذاكرة.
بيانات SEO
SEO Title
تقليل استهلاك الذاكرة وتحسين أداء Laravel عند جلب البيانات من قاعدة البياناتSEO Description
دليل عملي متقدم لتقليل استهلاك الذاكرة في Laravel وتحسين استعلامات Eloquent باستخدام select وEager Loading وPagination وchunkById وlazyById وSQL Aggregation مع أمثلة Production عملية.SEO Keywords
Laravel Performance, Laravel Memory Optimization, Laravel Eloquent, تحسين أداء Laravel, تقليل استهلاك الذاكرة Laravel, Laravel select, Laravel chunkById, Laravel lazyById, Laravel Pagination, Eloquent Performance, Laravel Database Optimization, Laravel Query Optimization, تحسين استعلامات Laravel, PHP Memory Optimization, Laravel ProductionSlug
laravel-memory-database-query-optimization
عندي سؤال بخصوص ال grouping؟ ليه سويت ال group by على مستوى ال php وما سويته على مستوى قاعدة البيانات؟
مشكووووووووووووووووور يا طيب