الوضع الافتراضي وحدوده
افتراضيًا، يستخدم SQLite ما يُعرف بـ سجل التراجع (rollback journal). عند الكتابة، ينسخ SQLite الصفحات الأصلية إلى ملف بامتداد -journal، ثم يُعدّل قاعدة البيانات الرئيسية، ويحذف هذا الملف بعد إتمام الـ commit. وإذا تعطّلت العملية في منتصف الكتابة، تُعاد قراءة السجل بشكل عكسي لإلغاء التغيير الجزئي.
الفكرة بسيطة وآمنة، لكنها تحمل عيبًا مزعجًا: الكاتبون والقارئون يتصارعون على الملف نفسه. فطالما يحتفظ كاتب بقفل قاعدة البيانات، لا يستطيع أي قارئ بدء معاملة جديدة. وطالما هناك قرّاء نشطون، يبقى الكاتب في حالة انتظار. وفي تطبيق مزدحم — لنقل خادم ويب يستقبل عدة طلبات متزامنة — سترى أخطاء SQLITE_BUSY تنهال عليك أسرع مما تتخيل.
هنا يأتي دور وضع WAL ليقلب المعادلة.
ما الذي يفعله وضع WAL في sqlite فعليًا؟
تقنية write-ahead logging تعكس النموذج تمامًا. فبدلًا من تعديل ملف قاعدة البيانات الرئيسي مباشرةً، يقوم الكاتب بـ إلحاق الصفحات المُثبَّتة (committed) بملف منفصل ينتهي باللاحقة -wal. أما القرّاء فيواصلون القراءة من الملف الرئيسي، مع إلقاء نظرة سريعة على ملف WAL لمعرفة ما إذا كانت هناك نسخ أحدث من الصفحات التي يحتاجونها.
النتيجة؟ كاتب واحد وعدد غير محدود من القرّاء يعملون في اللحظة ذاتها. كل قارئ يرى لقطة (snapshot) متّسقة تعود إلى لحظة بدء معاملته، بينما يواصل الكاتب الإضافة إلى ملف WAL دون أن يمسّ ما يطّلع عليه القرّاء.
هذا الـ pragma وحده يكفي لتحويل قاعدة البيانات إلى الوضع الجديد. والإعداد يبقى محفوظًا داخل ترويسة الملف نفسه، لذلك أي اتصال لاحق سيلتقط وضع WAL تلقائيًا. لست بحاجة لتنفيذ هذا الأمر مع كل اتصال، يكفي مرة واحدة عند تهيئة قاعدة البيانات (أو ضمن سكربت الـ migration عندك).
يُرجِع الـ pragma اسم الوضع الجديد بعد التطبيق. إذا حصلت على wal، فأنت جاهز. أما إذا أعاد قيمة أخرى، فالغالب أن نظام الملفات لا يدعم الذاكرة المشتركة (سنعود لهذه النقطة بعد قليل).
تفعيل وضع WAL في SQLite والتحقق منه
يمكنك التأكد من الوضع الحالي في أي وقت:
المكالمة الأولى تُفعّل وضع WAL وتُرجع الوضع الجديد، أما المكالمة الثانية (بدون علامة =) فهي مجرد استعلام عن الوضع الحالي. بعد هذه الخطوة، سيحتوي مجلد messages.db على ثلاثة ملفات عند وجود نشاط على قاعدة البيانات: messages.db وmessages.db-wal وmessages.db-shm. الملفان الأخيران يظهران ويختفيان بحسب وجود اتصالات مفتوحة أم لا.
ملفا -wal و -shm في SQLite
يصاحب وضع WAL ملفان إضافيان، ومن المهم أن تفهم وظيفة كلٍّ منهما:
-walيحتفظ بالمعاملات المُثبَّتة (committed) التي لم تُدمج بعد في قاعدة البيانات الأساسية. يكبر حجمه مع كل عملية كتابة، ويصغر (أو يُعاد ضبطه) عند تنفيذ عملية الـ checkpoint.-shmهو ملف ذاكرة مشتركة، ويعمل كفهرس لملف WAL حتى تتفق جميع الاتصالات على مواقع الصفحات دون الحاجة لمسح ملف WAL مع كل استعلام.
النتيجة العملية لذلك: لا تنسخ قاعدة بيانات تعمل بوضع WAL عن طريق نسخ ملف .db وحده فقط. آخر البيانات موجودة داخل ملف -wal، وبدونه ستحصل على نسخة قديمة أو تالفة. إما أن تنسخ الملفات الثلاثة معًا في وقت لا توجد فيه عمليات كتابة نشطة، أو — وهو الخيار الأفضل بكثير — تستخدم واجهة النسخ الاحتياطي المدمجة في SQLite (سنتناولها في الفصل التالي).
التزامن في SQLite: كاتب واحد وقُرّاء كُثُر
وضع WAL لا يمنحك كتابات متزامنة (concurrent writes)؛ فـ SQLite ما زال يُنفّذ عمليات الكتابة بالتسلسل: في أي لحظة، هناك معاملة واحدة فقط تملك قفل الكتابة. ما تغيّر فعليًا هو أن الكتابة لم تعد تُعطّل القراءة، والقراءة لم تعد تُعطّل الكتابة.
ولذلك يتصرف تطبيق ويب نموذجي يعمل بوضع WAL على النحو التالي:
- نقاط النهاية التي يغلب عليها طابع القراءة تعمل بالتوازي دون تنازع.
- نقاط الكتابة تنتظر بعضها لفترة قصيرة في طابور، لكنها لا تُعطّل القراءة.
- القُرّاء طويلو الأمد (استعلامات التحليلات، عمليات التصدير) لا يُجبرون الكُتّاب على الانتظار.
إذا حاول اتصالان الكتابة في الوقت نفسه، سيحصل الثاني على الخطأ SQLITE_BUSY. الحل في الغالب هو ضبط مهلة انتظار معقولة (busy timeout) — أي إخبار SQLite بأن ينتظر قليلًا قبل أن يستسلم:
busy_timeout=5000 معناها: «إذا كان هناك قفل قائم، انتظر حتى 5 ثوانٍ قبل أن ترمي خطأ». هذا الإعداد مع وضع WAL يكفي لمعالجة معظم حالات التزاحم التي تواجهها التطبيقات فعلياً. أما الصيغة BEGIN IMMEDIATE فهي تأخذ قفل الكتابة فور بداية المعاملة بدل أن تنتظر أول عملية كتابة، وهذا يجنّبك صنفاً كاملاً من حالات الـ deadlock التي تحدث حين تحاول عدة اتصالات الترقية إلى الكتابة في نفس الوقت.
نقاط التحقق (Checkpoints): إعادة دمج ملف WAL
لا يمكن لملف WAL أن يكبر بلا حدود. عملية الـ checkpoint هي ببساطة أخذ الصفحات المُثبَّتة (committed) من ملف WAL وكتابتها داخل قاعدة البيانات الأصلية، ثم تفريغ ملف WAL وإعادة ضبطه.
تُجري SQLite عملية checkpoint تلقائياً حين يتجاوز حجم WAL حوالي 1000 صفحة (وهي القيمة الافتراضية لـ wal_autocheckpoint). في معظم التطبيقات لا داعي للعبث بهذه القيمة. لكن إن أردت ضبطها أو تشغيل checkpoint يدوياً:
يقبل الأمر wal_checkpoint أحد الأوضاع التالية:
PASSIVE— ينفّذ نقطة التحقق (checkpoint) بأقصى قدر ممكن دون التأثير على القرّاء أو الكتّاب. وهو الوضع الافتراضي.FULL— ينتظر حتى ينتهي الكتّاب النشطون، ثم ينفّذ نقطة تحقق لكل ما تم إيداعه (committed).RESTART— مثلFULL، مع منع القرّاء الجدد من استخدام ملف WAL القديم.TRUNCATE— مثلRESTART، مع تقليص حجم ملف WAL إلى صفر بايت.
في الغالب، خوادم الويب لا تحتاج لاستدعاء هذا الأمر يدويًا. لكن إن كنت تطوّر تطبيق سطح مكتب وتحبّ أن تبقى أحجام الملفات مرتّبة عند الإغلاق، فمن المعقول تنفيذ نقطة تحقق من نوع TRUNCATE قبل إغلاق آخر اتصال بقاعدة البيانات.
إعدادات PRAGMA تتكامل بشكل ممتاز مع وضع WAL
تفعيل وضع WAL وحده شيء جيد، لكن استخدامه مع بعض الإعدادات الأخرى هو ما تعتمد عليه تطبيقات الإنتاج عادةً:
جولة سريعة:
synchronous=NORMALهو الإعداد المُوصى به مع وضع WAL. فهو آمن في حالة انهيار التطبيق أو نظام التشغيل؛ والشيء الوحيد الذي قد يُفقد آخر المعاملات هو انقطاع التيار في لحظة سيئة جدًا، ومع ذلك تبقى قاعدة البيانات في حالة متّسقة. أما القيمة الافتراضيةFULLفأكثر أمانًا لكنها أبطأ بفارق ملحوظ.busy_timeoutتحدثنا عنه سابقًا.foreign_keys=ONلا علاقة له بـ WAL، لكن يستحق التفعيل في كل اتصال — لأن SQLite يُعطّل فرض المفاتيح الأجنبية افتراضيًا حفاظًا على التوافق مع الإصدارات القديمة.
هذه الإعدادات تُطبَّق على مستوى الاتصال (ما عدا journal_mode فهو يبقى ثابتًا). شغّلها مباشرة بعد فتح الاتصال داخل كود تطبيقك.
متى لا يكون وضع WAL هو الخيار المناسب
رغم أن تفعيل WAL mode في SQLite هو التوصية الافتراضية، إلا أن هناك حالات تستدعي التفكير مرتين:
- أنظمة الملفات الشبكية. يعتمد وضع WAL على الذاكرة المشتركة (
mmap) بين العمليات التي تصل إلى قاعدة البيانات. وأنظمة مثل NFS وSMB وما شابهها لا تدعم ذلك بشكل موثوق. فإذا كانت قاعدة بياناتك موجودة على مشاركة شبكية، فالتزم بـ rollback journal — أو الأفضل من ذلك، لا تضع SQLite على مشاركة شبكية أصلًا. - الوسائط للقراءة فقط. يحتاج WAL إلى كتابة ملفي
-walو-shm. لذا قاعدة البيانات الموجودة على قرص CD-ROM أو ما شابهه يجب أن تستخدم وضع journal لا يقوم بالكتابة (أو تُفتح للقراءة فقط عبرmode=ro). - مهام دفعية بكاتب وحيد دون قرّاء متزامنين. لن يضرّك WAL هنا، لكنك أيضًا لن تستفيد منه. وضع rollback journal الافتراضي يفي بالغرض.
أما في 95% من التطبيقات — خوادم الويب، تطبيقات سطح المكتب، تطبيقات الموبايل، الأجهزة المدمجة ذات التخزين المحلي — فإن WAL هو الخيار الصحيح.
إعداد واقعي للإنتاج
هذه هي الصيغة التي تتبعها معظم بيئات SQLite في الإنتاج، مُلخّصة في أوامر pragma جاهزة للتشغيل:
الخيار temp_store=MEMORY يحتفظ بالجداول والفهارس المؤقتة في الذاكرة بدل القرص — مكسب صغير ومجاني طالما عندك ذاكرة فاضية.
اضبط هذه الإعدادات مرة واحدة عند فتح الاتصال في كود تهيئة قاعدة البيانات في تطبيقك، وبكذا تكون غطّيت معظم ما يحتاجه أي تطبيق يعتمد على SQLite ليعمل بكفاءة تحت الحِمل المتزامن.
التالي: النسخ الاحتياطي والاستعادة
بما أن قاعدة بياناتك صار معها ملفان مرافقان -wal و -shm، فإن نسخ الملف الأساسي لم يعد طريقة آمنة للنسخ الاحتياطي. الفصل القادم يشرح الطريقة الصحيحة لأخذ نسخة احتياطية من قاعدة بيانات SQLite وهي شغّالة — أمر .backup، وواجهة Online Backup API، وكيف تحصل على لقطة متّسقة (snapshot) دون إيقاف التطبيق.
الأسئلة الشائعة
ما هو وضع WAL في SQLite؟
WAL اختصار لـ write-ahead logging. بدلًا من كتابة التغييرات مباشرة في ملف قاعدة البيانات الرئيسي والاعتماد على rollback journal للتراجع عند الفشل، يقوم SQLite بإلحاق التغييرات في ملف منفصل اسمه -wal، ثم يدمجها لاحقًا في الملف الأصلي. الفائدة الكبرى هنا هي التزامن: القرّاء والكاتب يشتغلون في نفس الوقت دون أن يعطّل أحدهما الآخر.
كيف أُفعّل وضع WAL في SQLite؟
نفّذ الأمر PRAGMA journal_mode=WAL; مرة واحدة فقط. الإعداد دائم لأنه يُخزَّن في ترويسة ملف قاعدة البيانات نفسه، وبالتالي أي اتصال جديد سيستخدم WAL تلقائيًا. لا حاجة لضبطه مع كل اتصال. ويُرجِع البراغما القيمة wal عند نجاح العملية.
هل يدعم وضع WAL الكتابة المتزامنة؟
لا — SQLite ما زال يُسلسل عمليات الكتابة، فلا يوجد أكثر من كاتب واحد يحمل قفل الكتابة في نفس اللحظة. الذي تغيّر مع WAL أن القرّاء لم يعودوا يعطّلون الكاتب، والكاتب لم يعد يعطّل القرّاء. وهذا في الغالب هو الاختناق الحقيقي في معظم التطبيقات.
ما وظيفة ملفي -wal و -shm؟
ملف -wal يحتوي على التغييرات المُلتَزَم بها (committed) التي لم تُدمج بعد في قاعدة البيانات الأصلية. أما -shm فهو فهرس صغير في الذاكرة المشتركة يساعد الاتصالات على إيجاد الصفحات داخل ملف WAL بسرعة. الاثنان يُنشآن تلقائيًا، لكن عند نسخ قاعدة البيانات يجب نسخهما معًا، أو استخدام backup API.