اتصال واحد، ملفات متعددة
اتصال SQLite ليس مقيّداً بملف واحد فقط. عبر الأمر ATTACH DATABASE يمكنك فتح ملفات .db إضافية إلى جانب الملف الذي بدأت به، ثم الاستعلام عنها جميعاً وكأنها مخططات (schemas) داخل قاعدة بيانات واحدة. هذه أقرب ميزة يقدّمها SQLite لمفهوم "عدة قواعد بيانات على خادم واحد".
الصيغة الأساسية:
يُنشَأ ملف archive.db تلقائيًا إن لم يكن موجودًا، تمامًا كما يحصل مع قاعدة البيانات الرئيسية. ومن هذه اللحظة وحتى نهاية الجلسة، كل ما يبدأ بالبادئة archive. يُخزَّن في هذا الملف الثاني، بينما يبقى كل ما يبدأ بـ main. (أو بدون بادئة أصلًا) في الملف الأصلي.
أي اتصال بـ SQLite يحمل بشكل ضمني مخططَين: main (وهو الملف الذي فتحته أول مرة) وtemp (مساحة مؤقتة للجداول العابرة). وعملية الربط (ATTACH) تضيف مخططات أخرى فوق هذين.
صيغة ATTACH DATABASE وما الذي يفعله الاسم المستعار
ATTACH DATABASE 'path/to/file.db' AS alias_name;
الاسم المستعار هنا هو اسم المخطط (schema) الذي ستستخدمه لتأهيل أسماء الجداول. وهو محصور في نطاق الاتصال الحالي فقط، أي أن اتصالاً آخر يربط نفس الملف يمكنه اختيار اسم مستعار مختلف. اختر اسماً قصيراً ومعبّراً مثل archive أو analytics أو cache، لأنك ستكتبه كثيراً.
بعض النقاط المهمة التي يجدر معرفتها:
- المسار يكون نسبياً إلى مجلد العمل الخاص بالعملية، ما لم يكن مساراً مطلقاً.
- النص
':memory:'يربط قاعدة بيانات جديدة في الذاكرة (in-memory database) تحت ذلك الاسم المستعار. - لا يمكن أن يتعارض الاسم المستعار مع
mainأوtemp، ولا يمكن تكراره بين عمليات الربط.
الاستعلام عبر قواعد بيانات SQLite متعددة
هذه هي الميزة التي يلجأ معظم الناس إلى ATTACH من أجلها. بمجرد أن يصبح الملفان ضمن نفس الاتصال، يمكنك تنفيذ join بين جداولهما في استعلام واحد:
مُخطِّط الاستعلامات يتعامل مع كلا الـ schema بنفس الطريقة التي يتعامل بها مع الجداول في main. الفهارس على الجداول المرفقة تُستخدم بشكل طبيعي، وEXPLAIN QUERY PLAN يعمل عبرها دون مشاكل. لا توجد أي رحلات عبر الشبكة — كلا الملفين مفتوحان داخل نفس العملية.
هذا الأسلوب مفيد فعلاً عند الحاجة إلى فصل البيانات النشطة عن الأرشيف البارد، أو تقسيم الملفات حسب كل مستأجر (tenant)، أو استخراج البيانات المرجعية إلى قاعدة بيانات منفصلة للقراءة فقط.
ربط قاعدة بيانات للقراءة فقط أو في الذاكرة
إذا كانت قاعدة البيانات الثانية مجرد بيانات تريد قراءتها دون تعديلها — مثل مجموعة بيانات مرجعية تأتي مع التطبيق — فاربطها بوضع القراءة فقط عبر URI:
صيغة URI تتطلب أن تكون مكتبة SQLite مفعَّلة فيها SQLITE_OPEN_URI (وهي مفعَّلة في واجهة سطر الأوامر ومعظم رِبط اللغات). بعد ذلك، أي عملية INSERT أو UPDATE أو DELETE على ref.* ستُطلق خطأً قبل أن تمس الملف أصلاً.
وكذلك يُعدّ ربط قواعد البيانات في الذاكرة (in-memory) مفيداً جداً لتجهيز البيانات مؤقتاً:
scratch تختفي بمجرد إغلاق الاتصال. تشبه temp لكن مع إعطائك التحكم الكامل بعمرها.
المعاملات تشمل كل قواعد البيانات المرتبطة
أي BEGIN/COMMIT واحد يغطي عمليات الكتابة على main وعلى كل المخططات المرتبطة. فإمّا أن يُحفظ كل شيء أو يُتراجع عنه بالكامل، وبهذا تبقى الذرّية (atomicity) مصونة عبر الملفات المختلفة:
نقل الصفوف من جدول حيّ إلى ملف أرشيف هو بالضبط نوع العمليات التي تحتاج فيها إلى هذا الضمان. فبدون الذرّية (atomicity) عبر الملفات، أي تعطّل في المنتصف سيتركك أمام صفوف مكرّرة، أو الأسوأ من ذلك: صفوف مفقودة.
لكن انتبه لنقطة مهمة: عند الكتابة في أكثر من قاعدة بيانات مرفقة ضمن المعاملة الواحدة، يلجأ SQLite إلى بروتوكول إيداع (commit) أكثر تحفّظاً يتطلّب ملف journal مؤقّتاً. هذا الأسلوب أبطأ من الإيداع على ملف واحد، لكنه يبقى آمناً.
فصل قاعدة البيانات باستخدام DETACH DATABASE
عند انتهائك من قاعدة البيانات المرفقة، افصلها:
DETACH DATABASE archive;
الملف يبقى على القرص كما هو دون أي تغيير، فالأمر DETACH يقتصر على إغلاق الـ handle داخل الاتصال الحالي. هناك قيدان ينبغي تذكرهما:
- لا يمكنك فصل قاعدة البيانات
mainأوtemp. - لا يمكنك فصل قاعدة بيانات مرتبطة حاليًا بعملية (transaction) جارية أو لها استعلامات مفتوحة عليها.
وإن نسيت تنفيذ DETACH، فالأمر ليس كارثة؛ مجرد إغلاق الاتصال يكفي لتنظيف كل شيء تلقائيًا.
الحدود والأخطاء الشائعة
إليك بعض الحدود العملية التي يُستحسن معرفتها:
- الحد الافتراضي هو 10 قواعد بيانات مرفقة لكل اتصال (إضافة إلى
mainوtemp). أما الحد الأقصى وقت التصريف فهو 125. إن تجاوزت الحد ستظهر لك الرسالةtoo many attached databases - max 10. - كل ملف مرفق يستهلك ذاكرة تخزين مؤقت للصفحات (page cache). إرفاق عشرات قواعد البيانات الضخمة ليس مجانيًا، فاستهلاك الذاكرة سيرتفع.
- لا يمكن تنفيذ
ATTACHنفسه داخل transaction. شغّله قبلBEGINأو بعدCOMMIT.
وإليك بعض الأخطاء التي يُرجَّح أن تصادفها:
-- الملف غير موجود والمجلد غير قابل للكتابة:
Error: unable to open database: 'missing/path.db'
-- حاولت الكتابة إلى مرفق للقراءة فقط:
Error: attempt to write a readonly database
-- استخدمت نفس الاسم المستعار مرتين:
Error: database archive is already in use
معظم هذه الأخطاء واضحة بمجرد قراءتها. لكن خطأ "already in use" هو الذي يُربك الناس عادةً — فأمر ATTACH لا يستبدل اسماً مستعاراً موجوداً مسبقاً، بل عليك تنفيذ DETACH أولاً.
نمط عملي: الفصل بين البيانات الساخنة والباردة
لنجمع كل ما سبق في سيناريو واقعي — سير عمل أرشفة بسيط ينقل الطلبات الأقدم من سنة من قاعدة البيانات الرئيسية إلى أخرى مخصصة للأرشيف:
تنتقل الصفوف القديمة إلى archive.orders، بينما تبقى الصفوف الحديثة في main. التقارير التي تحتاج إلى السجل التاريخي يمكنها عمل join بين القاعدتين، أما الاستعلامات اليومية على main.orders فتظل سريعة لأن الجدول أصبح أصغر. اتصال واحد، ملفان، ومعاملة واحدة.
الخطوة التالية: Prepared Statements
الهدف من ATTACH هو منح اتصال واحد صلاحية الوصول إلى بيانات أكثر. أما المواضيع القادمة فتدور حول الطريقة التي تتواصل بها التطبيقات مع SQLite بأمان وكفاءة، وسنبدأها بـ prepared statements، فهي الأساس الذي يقوم عليه ربط المعاملات (parameter binding) وكتابة استعلامات محصّنة ضد هجمات الحقن.
الأسئلة الشائعة
ما وظيفة ATTACH DATABASE في SQLite؟
الأمر ATTACH DATABASE 'file.db' AS alias يفتح ملف قاعدة بيانات SQLite ثانٍ داخل نفس الاتصال الحالي ويعطيه اسماً للمخطط (schema). بعد ذلك يمكنك الإشارة إلى جداوله بالصيغة alias.table_name ودمجها مع جداول قاعدة البيانات الرئيسية في استعلام واحد.
ما الحد الأقصى لعدد قواعد البيانات التي يمكن إرفاقها في SQLite؟
افتراضياً يسمح SQLite بإرفاق حتى 10 قواعد بيانات لكل اتصال، إضافةً إلى المخططين main وtemp. الحد الأعلى المطلق هو 125، ويمكن تعديله عند الترجمة (compile time) عبر الثابت SQLITE_MAX_ATTACHED. وعند تجاوز الحد ستظهر لك رسالة الخطأ too many attached databases.
هل يمكنني تنفيذ استعلام واحد عبر عدة قواعد بيانات مرفقة؟
نعم بالتأكيد. بعد إرفاق القاعدة، اكتب اسم الجدول مسبوقاً باسم المخطط، مثلاً: SELECT * FROM main.users JOIN archive.orders ON .... عمليات الـ JOIN والاستعلامات الفرعية وINSERT ... SELECT كلها تعمل بين المخططات المختلفة. كما أن المعاملات (transactions) تمتد لتشمل جميع القواعد المرفقة، لذا فإن أي COMMIT يكون ذرّياً (atomic) عبر كل الملفات.
كيف أفصل قاعدة بيانات مرفقة في SQLite؟
ببساطة نفّذ الأمر DETACH DATABASE alias. الملف نفسه يبقى على القرص دون أي تغيير، فالأمر DETACH يكتفي بإغلاق المرجع داخل الاتصال الحالي. لا يمكنك فصل main أو temp، كما لا يمكنك فصل قاعدة بيانات أثناء وجود معاملة مفتوحة عليها.