إتقان أداء SQLite: ما وراء الأساسيات
تُعد SQLite قاعدة البيانات الأكثر انتشارًا في العالم. إنها موجودة في هاتفك (سواء iOS أو Android)، متصفحك، جهاز Mac الخاص بك، وربما حتى الميكروويف (لا أمزح). بشكل افتراضي، تم تكوين SQLite لتكون آمنة و متوافقة، وليس بالضرورة فائقة السرعة. إذا كنت تستخدم SQLite لتطبيق سطح مكتب جاد أو عملية كثيفة البيانات، فأنت بحاجة للنظر تحت الغطاء.
في هذا المنشور، سنغطي التقنيات التي استخدمناها في HarborDB للاستعلام عن ملايين الصفوف في أجزاء من الثانية.
1. أولاً وقبل كل شيء: تفعيل WAL
إذا لم تفعل شيئًا آخر، فافعل هذا. وضع تسجيل الكتابة المسبقة (Write-Ahead Logging - WAL) يغير بشكل جذري كيفية تعامل SQLite مع التزامن. بشكل افتراضي، تستخدم SQLite دفتر يومية للتراجع (journal rollback). هذا يعني أنه عندما يكتب عملية ما، لا يمكن لأي شخص آخر القراءة (أو العكس، اعتمادًا على وضع القفل).
يسمح وضع WAL بـ:
- القراء والكتاب في وقت واحد.
- كتابة أسرع (لأن "نقاط التفتيش" متسلسلة).
كيفية تفعيله:
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
ملاحظة:
PRAGMA synchronous = NORMALآمن طالما أن القرص الصلب لا يتعرض للتلف المادي أو انقطاع التيار الكهربائي. بالنسبة لتطبيقات سطح المكتب، إنها المقايضة المثالية.
2. الإدخالات المجمعة والمعاملات
الخطأ الأكثر شيوعًا للمبتدئين هو إدراج الصفوف في حلقة تكرار بدون معاملة (transaction). الطريقة البطيئة (مثال Python):
# لا تقم بذلك. سيستغرق الأمر وقتًا طويلاً.
for row in data:
cursor.execute("INSERT INTO users VALUES (?)", row)
conn.commit() # التزام تلقائي لكل صف!
تضطر SQLite لانتظار دوران القرص المادي (أو أمر المزامنة في SSD) لكل عملية إدراج.
الطريقة السريعة:
conn.execute("BEGIN TRANSACTION")
for row in data:
cursor.execute("INSERT INTO users VALUES (?)", row)
conn.commit()
فرق السرعة: غالبًا 100x إلى 1000x أسرع.
3. الفهارس: استخدمها بحكمة
الفهرس هو نسخة مرتبة من بياناتك. إنه يجعل القراءة سريعة للغاية، لكن الكتابة أبطأ قليلاً (لأنك تحتاج إلى تحديث كل من الجدول والفهرس).
نصيحة الفهرس الذهبية
استخدم EXPLAIN QUERY PLAN قبل التخمين.
EXPLAIN QUERY PLAN SELECT * FROM logs WHERE severity = 'ERROR';
إذا رأيت SCAN TABLE، فأنت تفعل ذلك بشكل خاطئ (فهو يقرأ كل صف).
إذا رأيت SEARCH TABLE USING INDEX، فأنت على الطريق الصحيح.
إنشاء فهرس:
CREATE INDEX idx_logs_severity ON logs(severity);
4. تحسينات PRAGMA للمستخدمين المتقدمين
فيما يلي بعض الأوامر الأقل شهرة التي نستخدمها في HarborDB:
-
PRAGMA temp_store = MEMORY;يجبر هذا SQLite على الاحتفاظ بالجداول والفهارس المؤقتة في ذاكرة الوصول العشوائي (RAM) بدلاً من الكتابة على القرص. إذا كان لديك الكثير من الذاكرة، فهذا مكسب كبير. -
PRAGMA mmap_size = 30000000000;يستخدم هذا الإدخال/الإخراج المعين للذاكرة للوصول إلى ملف قاعدة البيانات. يتعامل نظام التشغيل مع التخزين المؤقت. بالنسبة للقراءات الكبيرة، يمكن أن يقضي هذا تقريبًا على زمن انتقال الإدخال/الإخراج. -
PRAGMA cache_size = -2000;يحدد كمية الصفحات التي تحتفظ بها SQLite في الذاكرة. الأرقام السالبة تعني الكيلوبايت (هنا ~2MB). الأرقام الموجبة تعني عدد الصفحات. قم بزيادة هذا للاستعلامات الكبيرة.
5. البنى الموجهة للتطبيقات
أحيانًا لا تكون المشكلة في SQL، ولكن في الكود الخاص بك. في HarborDB، نقوم بنقل عمليات القراءة الثقيلة إلى خيط خلفي (background thread)، مع الحفاظ على استجابة واجهة المستخدم. نظرًا لأننا قمنا بتفعيل وضع WAL، فإن الخيط الخلفي لا يблоки إذا قام المستخدم بإجراء تغييرات صغيرة على واجهة المستخدم (الكتابة).
ملخص
SQLite ليست قاعدة بيانات "لعبة". إنها أداة قوية قادرة على التعامل مع أعباء العمل الهائلة إذا تعاملت معها بشكل صحيح.
- قم بتفعيل WAL.
- قم دائمًا بتغليف عمليات الكتابة في معاملات.
- استخدم الفهارس لجمل
WHERE. - قم بتحسين الذاكرة باستخدام PRAGMAs.
حظًا سعيدًا في التحسين!