Menu
flag Ar iconالعربيةdown icon
جرّب في Playground

النسخ الاحتياطي والاستعادة في SQLite

كيف تأخذ نسخة احتياطية لقاعدة بيانات SQLite وتستعيدها بأمان: أمر ‎.backup، و VACUUM INTO، وواجهة النسخ المباشر، ولماذا نسخ الملف يدوياً فكرة سيئة.

تحتوي هذه الصفحة على محررات قابلة للتشغيل — حرّر، شغّل، وشاهد النتيجة فوراً.

لماذا لا يكفي استخدام cp لنسخ ملف قاعدة البيانات؟

قاعدة بيانات SQLite عبارة عن ملف واحد فقط، وهذا ما يغري الكثيرين بأخذ نسخة احتياطية عبر نسخ الملف مباشرةً. أحياناً تنجح هذه الطريقة، لكنها في الغالب تفشل.

هناك مشكلتان رئيسيتان قد تحدثان:

  • وجود اتصال آخر في منتصف عملية كتابة لحظة النسخ. النتيجة؟ ملف وجهة يحتوي على معاملة (transaction) مطبَّقة جزئياً — أي ملف تالف عند محاولة فتحه.
  • إذا كانت قاعدة البيانات تعمل بوضع WAL (وهو الوضع الافتراضي في معظم التطبيقات الحديثة)، فإن آخر التعديلات تكون مخزَّنة في ملف منفصل اسمه database.db-wal. لو نسخت الملف الأساسي فقط، ستفقد بيانات دون أن تشعر.

لحسن الحظ، يوفر SQLite أدوات مخصصة لهذه المهمة، تتعامل مع القفل (locking) ومحتويات WAL والاتصالات المتزامنة دون أي مفاجآت. اعتمد عليها بدل cp.

أمر ‎.backup‎ في sqlite

أسرع طريقة لعمل نسخ احتياطي sqlite من سطر الأوامر هي استخدام أمر النقطة .backup:

sqlite3 app.db
sqlite> .backup backup.db
sqlite> .quit

هذه العملية تكتب نسخة كاملة من app.db إلى backup.db. وتعمل حتى لو كانت هناك عمليات أخرى تقرأ من قاعدة البيانات أو تكتب فيها، لأن واجهة النسخ الاحتياطي تأخذ سلسلة من الأقفال الصغيرة بدلاً من قفل واحد كبير، فتنسخ الصفحات تدريجيًا وتعيد محاولة نسخ أي صفحة تتعدّل أثناء العملية.

الناتج هو ملف قاعدة بيانات SQLite صالح للاستخدام بالكامل. افتحه كأي ملف آخر:

sqlite3 backup.db
sqlite> .tables

يمكنك أيضًا تنفيذ العملية كاملة بأمر واحد في الـ shell، وهذا في الغالب هو الشكل الذي تنتهي إليه معظم مهام cron:

sqlite3 app.db ".backup '/var/backups/app-$(date +%Y%m%d).db'"

ملف واحد يدخل، وملف واحد يخرج. لا حاجة لدورة dump/restore، ولا تحليل لأوامر SQL — مجرد نسخ للصفحات على مستوى التخزين مباشرةً.

استخدام VACUUM INTO لإنشاء نسخة مضغوطة

يُعدّ VACUUM INTO أداة قريبة من النسخ الاحتياطي لكنها تختلف عنه في الجوهر؛ فهي تكتب نسخة مُعاد بناؤها بالكامل من قاعدة البيانات إلى ملف جديد:

النتيجة هي نفس قاعدة البيانات منطقيًا، لكنها مُعاد كتابتها من الصفر — كل صفحة مرصوصة بإحكام، بدون تجزئة، وبدون صفحات حرة متبقية من صفوف محذوفة. وهذا ما يجعل ملف النسخة الاحتياطية بأصغر حجم ممكن.

متى تختار أيهما:

  • .backup — للنسخ الاحتياطي الروتيني والمتكرر. أسرع، ويتعايش بسلاسة مع عمليات الكتابة المتزامنة، ومطابق للبايتات تمامًا.
  • VACUUM INTO — للقطات دورية تريد فيها أيضًا ملفًا مرتبًا بأقل حجم ممكن. أبطأ لأنه يعيد كتابة كل شيء، كما أنه يأخذ قفل كتابة على قاعدة البيانات المصدر طوال المدة.

كلاهما يُنتج ملف .db صالحًا يمكنك فتحه فورًا.

استخدام واجهة النسخ الاحتياطي المباشر (Online Backup API) من داخل التطبيق

داخل التطبيق، لن تستدعي أداة sqlite3 من سطر الأوامر. بدلًا من ذلك تستخدم واجهة النسخ الاحتياطي المباشر التي يوفرها مشغّل قاعدة البيانات (driver) لديك. في مكتبة sqlite3 القياسية في بايثون، هذه الواجهة هي Connection.backup:

import sqlite3

source = sqlite3.connect("app.db")
dest = sqlite3.connect("backup.db")

with dest:
    source.backup(dest)

source.close()
dest.close()

تنسخ الدالة backup الصفحات من source إلى dest بينما تواصل الاتصالات الأخرى عملها بشكل طبيعي. يمكنك أيضًا تمرير pages= للنسخ على دفعات، وprogress= لاستقبال دالة استدعاء (callback) — وهذا مفيد جدًا في قواعد البيانات الكبيرة عندما تريد التحكم في سرعة النسخ أو عرض شريط تقدم.

معظم المكتبات (drivers) في اللغات الأخرى تكشف نفس واجهة C البرمجية (sqlite3_backup_init و_step و_finish) باسم مشابه. النمط واحد دائمًا: افتح المصدر، افتح الوجهة، اعبر الصفحات خطوة بخطوة، ثم أنهِ العملية.

النسخ الاحتياطي المباشر في sqlite أثناء تشغيل قاعدة البيانات

هنا يتألق SQLite بهدوء. كل من الأمر .backup وواجهة النسخ الاحتياطي المباشر (online backup API) مصممان للنسخ الاحتياطي الساخن (hot backup) — أي أن قاعدة البيانات المصدر يمكن أن تبقى مفتوحة ونشطة طوال الوقت.

ما يحدث فعليًا:

  1. تأخذ عملية النسخ قفلًا مشتركًا (shared lock) وتبدأ بنسخ الصفحات.
  2. إذا عدّل أحد الكتّاب صفحة لم تُنسخ بعد، تلاحظ عملية النسخ ذلك وتعيد قراءتها.
  3. تكتمل عملية النسخ بمجرد أن تصبح كل الصفحات متسقة.

لست مضطرًا لإيقاف تطبيقك، أو طرد الاتصالات، أو جدولة فترة توقف. في قاعدة بيانات مزدحمة قد تحتاج العملية إلى دورات إضافية حتى تستقر، لكنها ستستقر في النهاية. الملف الذي ستحصل عليه في الوجهة يمثل لقطة (snapshot) متسقة من قاعدة البيانات في لحظة زمنية محددة.

نقطة جديرة بالملاحظة: إذا كنت تستخدم وضع WAL، شغّل PRAGMA wal_checkpoint(TRUNCATE); بين الحين والآخر حتى لا يتضخم ملف WAL بلا حدود. عملية النسخ نفسها تتعامل مع WAL بشكل صحيح — لكن هذه مجرد ممارسة جيدة للحفاظ على نظافة ملفات WAL.

استعادة قاعدة بيانات sqlite من نسخة احتياطية

استعادة قاعدة بيانات SQLite عملية مملة بشكل غير معتاد، وهذا هو بيت القصيد. ملف النسخة الاحتياطية هو قاعدة بيانات بحد ذاته. كل ما عليك لاستخدامه هو فتحه مباشرة:

sqlite3 backup.db
sqlite> SELECT COUNT(*) FROM notes;

لاستعادة قاعدة بيانات حية فوق نسخة قائمة — مثلاً بعد فقدان بيانات — اتّبع هذا الترتيب الآمن:

  1. أوقف كل عملية تفتح قاعدة البيانات.
  2. احذف الملفات الموجودة: app.db وapp.db-wal وapp.db-shm. أيّ بقايا من ملفات WAL/SHM تخص قاعدة البيانات القديمة ستربك SQLite حين تُقرَن بالملف الرئيسي المُستعاد.
  3. انسخ النسخة الاحتياطية إلى مكانها: cp backup.db app.db.
  4. أعد تشغيل تطبيقك.

ملفا -wal و-shm مهمّان جدًا. إذا تجاوزت الخطوة الثانية، فقد يحاول SQLite تطبيق WAL قديم فوق الملف الرئيسي المُستعاد، والنتيجة إمّا تلف في البيانات أو خليط غريب لا معنى له.

ومن داخل واجهة CLI، يتوفّر أيضًا الأمر .restore، وهو النظير المقابل لـ .backup:

sqlite3 app.db
sqlite> .restore backup.db
sqlite> .quit

هذا يستبدل محتويات قاعدة البيانات المتصلة بمحتويات backup.db، مستخدمًا نفس الـ online backup API لكن بالاتجاه المعاكس.

أمر .dump أداة مختلفة تمامًا

ستصادف ذكرًا لأمر .dump في بعض الشروحات القديمة، لكنه ليس نسخًا احتياطيًا بالمعنى الذي نتحدث عنه هنا، إذ ينتج ملفًا نصيًا بصيغة SQL يحتوي على عبارات CREATE و INSERT:

sqlite3 app.db .dump > app.sql

للاستعادة، تقوم بتشغيل أوامر SQL مرة أخرى:

sqlite3 new.db < app.sql

هذا مفيد عند الترقية بين إصدارات SQLite، أو لمقارنة المخططات (schemas) في git، أو لنقل البيانات إلى محرك قواعد بيانات مختلف. لكنه أبطأ وأكبر حجمًا وأقل دقة من .backup (فبعض الأمور مثل collations المخصصة والأعمدة المولّدة (generated columns) وبعض pragmas تحتاج عناية إضافية). أما إذا كنت تريد نسخًا احتياطيًا فعليًا لقاعدة بيانات قيد التشغيل، فاعتمد على .backup أو VACUUM INTO.

روتين نسخ احتياطي عملي لقاعدة بيانات SQLite

بالنسبة لمعظم التطبيقات، هذه التوليفة تؤدي الغرض جيدًا:

  • تشغيل دوري لأمر .backup — كل ساعة أو كل يوم، حسب حجم البيانات التي تتحمّل فقدها. خفيف وسريع ويعمل أثناء التشغيل (hot).
  • تشغيل أسبوعي لـ VACUUM INTO إلى مسار منفصل. يلتقط أي انحراف في البيانات، ويمنحك لقطة مضغوطة، ويُجرّب مسارًا مختلفًا في الكود.
  • سياسة احتفاظ واضحة: احتفظ بآخر N من النسخ اليومية، وآخر M من النسخ الأسبوعية. قواعد بيانات SQLite تستجيب جيدًا للضغط، لذا تشغيل gzip backup.db بعد عملية النسخ يستحق العناء.
  • بين الحين والآخر، استعِد إحدى النسخ ونفّذ عليها بعض الاستعلامات للتأكد. النسخة الاحتياطية غير المُختبَرة مجرد أمنية، لا نسخة احتياطية حقيقية.
# يوميًا، في cron:
sqlite3 /var/lib/app/app.db ".backup '/var/backups/app-$(date +%F).db'"
gzip "/var/backups/app-$(date +%F).db"

# أسبوعيًا:
sqlite3 /var/lib/app/app.db "VACUUM INTO '/var/backups/app-weekly-$(date +%F).db'"

كلا الأمرين آمن للتشغيل أثناء استقبال التطبيق للطلبات.

التالي: إعدادات PRAGMA

النسخ الاحتياطي جانب تشغيلي واحد فقط، أما ضبط سلوك قاعدة البيانات أثناء التشغيل فهو جانب آخر. تتيح SQLite التحكم في إعداداتها عبر تعليمات PRAGMA — وضع journal، ومستوى synchronous، وحجم الكاش، وتفعيل المفاتيح الأجنبية. الصفحة التالية تستعرض أبرز الإعدادات التي تستحق المعرفة.

الأسئلة الشائعة

كيف آخذ نسخة احتياطية من قاعدة بيانات SQLite؟

من سطر الأوامر، نفّذ ‎.backup path/to/backup.db وأنت متصل بقاعدة البيانات الأصلية. أما من داخل التطبيق، فاستخدم واجهة النسخ المباشرة (sqlite3_backup_init في لغة C، أو ما يقابلها في درايفر لغتك). كلا الطريقتين تنتج نسخة متناسقة حتى لو كانت هناك اتصالات أخرى تكتب على القاعدة في نفس اللحظة.

هل يكفي أن أنسخ ملف ‎.db مباشرة كنسخة احتياطية؟

لا تفعل ذلك إلا إذا كنت متأكداً تماماً أنه لا توجد أي عملية تفتح القاعدة للكتابة. وإلا فقد تنسخ الملف في منتصف معاملة وينتهي بك الأمر بنسخة تالفة، أو تفقد بيانات لا تزال داخل ملف WAL. الأفضل استخدام ‎.backup أو VACUUM INTO، فهما يتعاملان مع الأقفال ومحتوى WAL بالشكل الصحيح.

ما الفرق بين ‎.backup و VACUUM INTO؟

أمر ‎.backup يستخدم واجهة النسخ المباشرة وينتج نسخة مطابقة بايت ببايت، بما في ذلك الصفحات غير المستخدمة. أما VACUUM INTO 'file.db' فيكتب نسخة مضغوطة ومرتّبة من جديد — أصغر حجماً وبدون تجزئة، لكنه يعيد كتابة كل صفحة. استخدم ‎.backup للنسخ الاحتياطي الدوري، و VACUUM INTO عندما تريد كذلك استرجاع المساحة الضائعة.

كيف أستعيد قاعدة بيانات SQLite من ملف نسخة احتياطية؟

إذا كانت النسخة عبارة عن ملف ‎.db، فيكفي أن تفتحه — قواعد بيانات SQLite ملف واحد فقط. وإذا أردت استعادة النسخة فوق قاعدة موجودة، أوقف التطبيق أولاً، ثم استبدل الملف (واحذف أي ملفات ‎-wal أو ‎-shm متبقية)، ثم أعد فتحه. ومن سطر الأوامر تستطيع أيضاً تنفيذ ‎.restore path/to/backup.db وأنت متصل بقاعدة جديدة فارغة.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن