هل تساءلت يوماً لماذا يستغرق استعلامك ٤٥ ثانية بينما يمكن تنفيذه في ٢٠٠ ميلي ثانية؟ إليك التقنيات الحقيقية التي تستخدمها الشركات الكبرى لتحويل قواعد البيانات البطيئة إلى آلات عالية الأداء، مع قياسات فعلية من قواعد بيانات حقيقية.
في أحد المشاريع التي عملت عليها مع فريق في شركة ناشئة في مجال التجارة الإلكترونية، كان لدينا جدول products يحتوي على ١٢ مليون صف. استعلام بسيط مثل SELECT * FROM products WHERE category_id = 5 كان يستغرق ٤٥ ثانية تقريباً. بعد جلسة تحسين واحدة، انخفض الوقت إلى ١٨٠ ميلي ثانية فقط. الفرق ليس مجرد أرقام على الشاشة، بل هو فرق بين تجربة مستخدم سلسة وتطبيق يبدو وكأنه معلق. الحقيقة هي أن معظم المطورين لا يعرفون كيف تعمل قواعد البيانات خلف الكواليس، ويعتمدون على الفهارس الافتراضية دون فهم كيف تُستخدم أو متى تُهمل.
المشكلة الأكبر ليست في كتابة SQL نفسها، بل في فهم كيف يقرأ محرك قاعدة البيانات الاستعلام، وكيف يخزن البيانات في الذاكرة، وكيف يتعامل مع الأقراص الصلبة. مثلاً، عندما تطلب SELECT * بدون تحديد الأعمدة، فإن قاعدة البيانات تضطر لقراءة كل بايت من كل صف في الجدول، حتى لو كنت تحتاج إلى عمودين فقط. هذا يشبه الذهاب إلى مكتبة وطلب كل الكتب الموجودة بدلاً من تحديد العنوان الذي تريده. في هذا المقال، سنفكك استعلامات SQL من الداخل، ونرى كيف يمكن تحسينها باستخدام تقنيات مثبتة، مع قياسات حقيقية من قواعد بيانات حقيقية.
الفهارس هي أول شيء يفكر فيه المطورون عند الحديث عن تحسين SQL، لكنها أيضاً أكثر شيء يُساء فهمه. الفهرس ليس مجرد أداة سحرية تجعل كل شيء أسرع. في الواقع، الفهارس الزائدة يمكن أن تبطئ عمليات INSERT و UPDATE بشكل كبير. لكن متى يجب استخدام الفهارس؟ وكيف تعمل بالضبط؟
لنأخذ مثالاً عملياً. لدينا جدول users يحتوي على ٥ ملايين صف، ونريد البحث عن مستخدمين بناءً على email. بدون فهرس، قاعدة البيانات تقوم بعملية scan كاملة للجدول، تقرأ كل صف وتتحقق من تطابق البريد الإلكتروني. هذا يشبه البحث عن كتاب في مكتبة غير مرتبة، عليك فحص كل كتاب على الرف. مع فهرس على عمود email، قاعدة البيانات تستخدم بنية بيانات شبيهة بالشجرة الثنائية (B-tree) للوصول المباشر إلى الصف المطلوب. الفرق في الأداء يمكن أن يكون هائلاً.
-- قبل الفهرس
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'user@example.com';
-- الوقت: 2.45 ثانية (Seq Scan على 5 مليون صف)
-- بعد إضافة الفهرس
CREATE INDEX idx_users_email ON users(email);
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'user@example.com';
-- الوقت: 0.045 ثانية (Index Scan)لكن الفهارس ليست دائماً الحل الأمثل. إذا كان الجدول صغيراً، قد لا يكون الفهرس ضرورياً، بل قد يبطئ الأمور. مثلاً، إذا كان الجدول يحتوي على ١٠٠ صف فقط، فإن قاعدة البيانات قد تختار القيام بعملية scan كاملة بدلاً من استخدام الفهرس، لأن تكلفة قراءة الفهرس نفسه قد تكون أعلى من قراءة الجدول بالكامل. أيضاً، الفهارس على الأعمدة التي تتغير كثيراً (مثل timestamps أو status) يمكن أن تبطئ عمليات الكتابة بشكل ملحوظ.
أحد الأخطاء الشائعة التي أراها في المشاريع هو كتابة استعلامات SQL معقدة جداً في محاولة لتجنب معالجة البيانات في التطبيق. مثلاً، استخدام JOINات متعددة مع GROUP BY و HAVING في استعلام واحد. المشكلة هنا أن قاعدة البيانات تضطر لتنفيذ كل هذه العمليات في الذاكرة، وقد تضطر لإنشاء جداول مؤقتة على القرص إذا لم تكن البيانات مناسبة لذاكرة الوصول العشوائي (RAM).
لنأخذ مثالاً من مشروع حقيقي. كان لدينا استعلام يجمع بيانات المبيعات الشهرية مع تفاصيل المنتجات والعملاء. الاستعلام الأصلي كان يحتوي على ٧ JOINات و GROUP BY على ٤ أعمدة، وكان يستغرق ١٢ ثانية تقريباً. بعد تحليل خطة التنفيذ (execution plan)، اكتشفنا أن قاعدة البيانات كانت تنشئ جدولاً مؤقتاً بحجم ١.٢ جيجابايت على القرص الصلب. الحل؟ تقسيم الاستعلام إلى جزأين: الأول يجمع بيانات المبيعات الأساسية، والثاني يضيف تفاصيل المنتجات والعملاء باستخدام استعلامات فرعية.
-- الاستعلام الأصلي (بطيء جداً)
SELECT
c.customer_name,
p.product_name,
SUM(o.quantity) as total_quantity,
SUM(o.amount) as total_amount
FROM
orders o
JOIN customers c ON o.customer_id = c.id
JOIN products p ON o.product_id = p.id
JOIN order_statuses os ON o.status_id = os.id
JOIN payment_methods pm ON o.payment_method_id = pm.id
JOIN shipping_methods sm ON o.shipping_method_id = sm.id
JOIN warehouses w ON o.warehouse_id = w.id
WHERE
o.created_at BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY
c.customer_name, p.product_name, os.status_name, pm.method_name
HAVING
SUM(o.amount) > 1000;
-- الوقت: 12.3 ثانية
-- الاستعلام المحسن (مقسم إلى جزأين)
-- الجزء الأول: جمع بيانات المبيعات الأساسية
WITH sales_data AS (
SELECT
customer_id,
product_id,
SUM(quantity) as total_quantity,
SUM(amount) as total_amount
FROM
orders
WHERE
created_at BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY
customer_id, product_id
HAVING
SUM(amount) > 1000
)
-- الجزء الثاني: إضافة التفاصيل
SELECT
c.customer_name,
p.product_name,
sd.total_quantity,
sd.total_amount
FROM
sales_data sd
JOIN customers c ON sd.customer_id = c.id
JOIN products p ON sd.product_id = p.id;
-- الوقت: 1.8 ثانيةالفرق هنا ليس فقط في الوقت، بل في استهلاك الموارد أيضاً. الاستعلام الأصلي كان يستخدم ١.٢ جيجابايت من القرص الصلب للجدول المؤقت، بينما الاستعلام المحسن استخدم ذاكرة الوصول العشوائي فقط. هذا يعني أن السيرفر يمكنه التعامل مع عدد أكبر من الاستعلامات المتزامنة دون أن يعلق.
هناك عدة علامات تدل على أن استعلامك معقد جداً ويجب تقسيمه:
معظم المطورين يركزون على تحسين الاستعلامات نفسها، لكنهم ينسون أن قاعدة البيانات تستخدم ذاكرة مؤقتة (buffer pool) لتخزين البيانات التي يتم الوصول إليها بشكل متكرر. إذا كانت ذاكرة الوصول العشوائي لديك صغيرة جداً، فإن قاعدة البيانات تضطر لقراءة البيانات من القرص الصلب في كل مرة، وهذا بطيء جداً. مثلاً، قراءة صفحة بيانات (عادة ٨ كيلوبايت) من القرص الصلب قد تستغرق ١٠ ميلي ثانية، بينما القراءة من الذاكرة المؤقتة تستغرق ٠.١ ميلي ثانية فقط.
في أحد المشاريع مع شركة كبيرة في مجال الخدمات المالية، كان لدينا قاعدة بيانات تحتوي على ٥٠٠ جيجابايت من البيانات، لكن السيرفر كان يحتوي على ١٦ جيجابايت فقط من ذاكرة الوصول العشوائي. الاستعلامات كانت بطيئة جداً لأن قاعدة البيانات كانت تضطر لقراءة البيانات من القرص الصلب في كل مرة. بعد زيادة ذاكرة الوصول العشوائي إلى ١٢٨ جيجابايت، تحسنت أوقات الاستجابة بنسبة ٩٠٪ تقريباً. لماذا؟ لأن قاعدة البيانات استطاعت تخزين معظم البيانات المستخدمة بشكل متكرر في الذاكرة المؤقتة.
-- كيف تعرف حجم الذاكرة المؤقتة المستخدمة في PostgreSQL
SHOW shared_buffers;
-- القيمة الافتراضية عادة 128MB، يمكن زيادتها إلى 25% من ذاكرة الوصول العشوائي الكلية
-- كيف تعرف نسبة استخدام الذاكرة المؤقتة
SELECT
pg_size_pretty(pg_table_size('orders')) as table_size,
pg_size_pretty(pg_total_relation_size('orders')) as total_size,
pg_size_pretty(pg_indexes_size('orders')) as indexes_size;
-- في MySQL
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
-- القيمة الافتراضية عادة 128MB، يمكن زيادتها إلى 70-80% من ذاكرة الوصول العشوائي الكليةلكن زيادة الذاكرة المؤقتة ليست دائماً الحل. إذا كانت قاعدة البيانات لديك تحتوي على الكثير من البيانات التي لا تُستخدم بشكل متكرر، فإن زيادة الذاكرة المؤقتة قد لا يكون لها تأثير كبير. في هذه الحالة، قد تحتاج إلى تقسيم البيانات إلى جداول أصغر أو استخدام تقنيات مثل table partitioning.
أحد الأخطاء التي أراها كثيراً هو استخدام الاستعلامات الفرعية (subqueries) أو التكرارات (loops) في SQL بطريقة غير فعالة. مثلاً، استخدام SELECT داخل SELECT داخل SELECT، أو استخدام وظائف مثل CURSOR في SQL Server لعمل تكرار على النتائج. هذه التقنيات يمكن أن تكون مدمرة للأداء، خاصة عندما تعمل على مجموعات بيانات كبيرة.
لنأخذ مثالاً من مشروع حقيقي. كان لدينا استعلام يجمع بيانات عن المنتجات الأكثر مبيعاً لكل فئة. المطور الأصلي كتب الاستعلام باستخدام تكرار في التطبيق (PHP)، حيث كان يقوم أولاً بجلب كل الفئات، ثم لكل فئة يقوم بجلب المنتجات الأكثر مبيعاً. هذا يعني أن التطبيق كان يرسل عشرات الاستعلامات إلى قاعدة البيانات بدلاً من استعلام واحد. الحل؟ استخدام window functions في SQL للحصول على نفس النتيجة في استعلام واحد.
-- الطريقة السيئة: تكرار في التطبيق
-- (مثال بلغة PHP)
$categories = $db->query("SELECT id, name FROM categories");
foreach ($categories as $category) {
$topProducts = $db->query(
"SELECT p.id, p.name, SUM(o.quantity) as total_quantity
FROM products p
JOIN orders o ON p.id = o.product_id
WHERE p.category_id = {$category['id']}
GROUP BY p.id, p.name
ORDER BY total_quantity DESC
LIMIT 5"
);
// معالجة النتائج
}
-- عدد الاستعلامات: عدد الفئات + 1
-- الطريقة الجيدة: استخدام window functions
SELECT
c.id as category_id,
c.name as category_name,
p.id as product_id,
p.name as product_name,
SUM(o.quantity) as total_quantity,
RANK() OVER (PARTITION BY c.id ORDER BY SUM(o.quantity) DESC) as rank
FROM
categories c
JOIN products p ON c.id = p.category_id
JOIN orders o ON p.id = o.product_id
GROUP BY
c.id, c.name, p.id, p.name
HAVING
RANK() OVER (PARTITION BY c.id ORDER BY SUM(o.quantity) DESC) <= 5;
-- عدد الاستعلامات: 1 فقطالفرق هنا ليس فقط في عدد الاستعلامات، بل في الأداء أيضاً. الطريقة الأولى ترسل عشرات الاستعلامات إلى قاعدة البيانات، وكل استعلام يحتاج إلى وقت للتخطيط والتنفيذ. الطريقة الثانية ترسل استعلاماً واحداً فقط، وتستخدم قدرات قاعدة البيانات للحصول على النتيجة المطلوبة بكفاءة.
الاستعلامات الفرعية يمكن أن تكون مفيدة في بعض الحالات، لكنها يجب أن تُستخدم بحذر:
كل ما تحدثنا عنه حتى الآن لا قيمة له إذا لم تتمكن من قياس تأثير التحسينات. معظم المطورين يعتمدون على الشعور أو على أوقات التنفيذ الظاهرة، لكنهم لا يستخدمون أدوات القياس الحقيقية. في عالم قواعد البيانات، هناك عدة أدوات وطرق لقياس الأداء بدقة.
أولاً، يجب دائماً استخدام EXPLAIN ANALYZE بدلاً من EXPLAIN فقط. الفرق هو أن EXPLAIN ANALYZE ينفذ الاستعلام بالفعل ويقيس الوقت الفعلي لكل خطوة، بينما EXPLAIN يعطي خطة تنفيذ تقديرية فقط. مثلاً، في PostgreSQL:
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'user@example.com';
-- النتيجة تحتوي على الوقت الفعلي لكل خطوة، وعدد الصفوف التي تم معالجتها، وغيرها من التفاصيل الهامةثانياً، يجب قياس تأثير التحسينات على قاعدة بيانات مشابهة لقاعدة البيانات الحقيقية. لا تعتمد على قاعدة بيانات اختبار تحتوي على ١٠٠ صف فقط. استخدم أدوات مثل pgbench في PostgreSQL أو sysbench في MySQL لتحميل قاعدة البيانات ببيانات حقيقية وقياس الأداء تحت ضغط مشابه للواقع.
ثالثاً، لا تنسَ قياس تأثير التحسينات على عمليات الكتابة أيضاً. مثلاً، إضافة فهرس قد يحسن أداء SELECT لكنه يبطئ INSERT و UPDATE. لذلك، يجب قياس الأداء الكلي للنظام، وليس فقط الاستعلامات الفردية.
بعد أكثر من عشر سنوات في العمل مع قواعد البيانات، هناك بعض القواعد الذهبية التي أتبعها دائماً عند تحسين استعلامات SQL:
في النهاية، تحسين استعلامات SQL هو مزيج من الفن والعلم. الفن يأتي من الخبرة وفهم كيف تعمل قواعد البيانات خلف الكواليس، والعلم يأتي من القياس الدقيق والتحليل المنطقي. لا تخف من تجربة أشياء جديدة، لكن قِس تأثيرها دائماً. أحياناً، الحل البسيط هو الأفضل، وأحياناً تحتاج إلى حلول معقدة. المهم هو أن تفهم لماذا يعمل الحل الذي اخترته، وليس فقط أن تعرف أنه يعمل.
إذا كان هناك شيء واحد تأخذه من هذا المقال، فليكن هذا: قاعدة البيانات ليست صندوقاً أسود. افتحها، افهم كيف تعمل، وقِس كل شيء. عندها فقط ستتمكن من تحويل قاعدة البيانات البطيئة إلى آلة عالية الأداء.