الـ Affinity مجرد تفضيل وليس قاعدة صارمة
من المعروف أن SQLite يعتمد على الكتابة الديناميكية (dynamic typing). فكل قيمة تحمل معها فئة التخزين الخاصة بها (NULL, INTEGER, REAL, TEXT, BLOB)، والنوع المُعلَن للعمود لا يفرض قيوداً صارمة على ما يمكن تخزينه فيه. كل ما يفعله النوع المُعلَن هو منح العمود ما يُسمى بـ type affinity — أي فئة تخزين مُفضَّلة يحاول SQLite تحويل القيم الواردة إليها.
شاهد ما يحدث عندما لا يكفي الـ affinity لمنع عدم تطابق الأنواع:
الصف الثاني يخزّن النص 'two' داخل عمود من نوع INTEGER. حاول SQLite تحويل 'two' إلى رقم، لكنه فشل (لأنه ليس رقمًا أصلًا)، فخزّنه كـ TEXT كما هو. الدالة typeof() تكشف لك فئة التخزين الفعلية لكل قيمة — وهي لا تطابق دائمًا ما أعلنته في تعريف العمود.
هذا السلوك يفاجئ القادمين من Postgres أو MySQL، لكنه مقصود في تصميم SQLite.
أنواع الـ Affinity الخمسة في SQLite
كل عمود في جدول غير STRICT يأخذ واحدًا فقط من هذه الأنواع:
TEXT— يُفضّل تخزين النصوص.NUMERIC— يُفضّل الأرقام، لكنه يقبل النصوص إذا تعذّر التحويل.INTEGER— مثلNUMERIC، لكنه يخزّن القيم التي لا تحتوي على جزء عشري كأعداد صحيحة.REAL— يُفضّل الأرقام ذات الفاصلة العائمة.BLOB— بلا تفضيل، يخزّن ما تعطيه له كما هو.
يُطلق على الـ BLOB affinity أيضًا اسم "بدون affinity" — وهو ما تحصل عليه عندما لا تحدّد نوعًا للعمود إطلاقًا.
نفس المُدخَل — السلسلة '42' — لكنه يُخزَّن بخمسة أنواع مختلفة. كل عمود حوّل القيمة (أو لم يحوّلها) حسب الـ affinity الخاص به.
كيف يختار SQLite الـ affinity من تعريف العمود
هنا تكمن النقطة التي تُربك الكثيرين: SQLite لا يملك قائمة ثابتة بالأنواع "المعتمدة". تستطيع أن تكتب أي شيء تقريبًا بعد اسم العمود، وسيستنتج SQLite الـ affinity بالبحث عن سلاسل فرعية داخل النص الذي كتبته، وفق هذا الترتيب:
- يحتوي على
INT←INTEGER - يحتوي على
CHARأوCLOBأوTEXT←TEXT - يحتوي على
BLOB، أو لا يوجد نوع أصلًا ←BLOB - يحتوي على
REALأوFLOAأوDOUB←REAL - أي شيء آخر ←
NUMERIC
هذه هي الخوارزمية كاملةً. وهي تفسّر الكثير من السلوك الغريب الذي قد تصادفه:
FLOATING_POINTS يتحول إلى INTEGER لأن السلسلة الفرعية INT موجودة داخل كلمة POINTS. القاعدة الأولى المطابِقة هي التي تفوز، بالترتيب من الأعلى إلى الأسفل. ولهذا السبب، فإن نسخ الأنواع من قاعدة بيانات أخرى دون تدقيق قد يعطيك نتيجة مختلفة عمّا تتوقع.
آلية عمل type affinity عند الإدراج
تظهر أهمية الـ type affinity بوضوح عندما يقرر SQLite ما إذا كان سيحوّل القيمة التي أدخلتها أم سيخزّنها كما هي. والقواعد كالتالي:
- الـ affinity من نوع
TEXT: الأرقام وقيمBLOBتُحوَّل إلى نص. - الـ affinities من نوع
NUMERICو**INTEGER** و**REAL**: النص الذي يبدو كرقم يُحوَّل إلى رقم، أما النص الذي لا يمثّل رقمًا فيبقى نصًا كما هو. - الـ affinity من نوع
BLOB: لا يحدث أي تحويل على الإطلاق.
صفًّا بصف:
- القيمة
'123'في عمود من نوعNUMERICتتحوّل إلى العدد الصحيح123، لأن التحويل من نص إلى رقم نجح دون أي فقد. - القيمة
'12.5'تتحوّل إلى العدد العشري12.5. - القيمة
'hello'في عمودNUMERICتبقى نصًّا كما هي، إذ لا يوجد رقم يمكن تحويلها إليه. - عمود
TEXTيُحوّل الأرقام إلى صيغتها النصية. - أما عمود
BLOBفيخزّن كل قيمة تمامًا كما أُدخلت، مع نوعها الأصلي.
الفرق الدقيق بين INTEGER و REAL
تتصرّف خاصية INTEGER affinity بشكل شبه مطابق لـ NUMERIC، مع فارق واحد: أي قيمة مثل 3.0 لا تحتوي على جزء عشري حقيقي يتم تخزينها كعدد صحيح 3 توفيرًا للمساحة.
3.0 يُخزَّن كـ INTEGER في العمودين — هذا التحسين يحصل مع NUMERIC أيضاً. أما 3.5 فيحتفظ بجزئه العشري ويبقى REAL. الخلاصة: لا تعتمد على typeof() لتعرف هل العمود مُعرَّف كـ INTEGER أو REAL، فهو يخبرك بما هو مخزَّن فعلياً، وقد يختلف من صفّ لآخر.
متى تسبّب لك الـ type affinity مشاكل؟
هذه المرونة مريحة... إلى أن تنقلب عليك. في الكود الحقيقي تظهر مشكلتان شائعتان:
1. تسرّب بيانات فاسدة. لو كان في تطبيقك خطأ يُرسل 'N/A' إلى عمود من نوع INTEGER، فإن SQLite سيخزّنها دون اعتراض. وبعدها أي استعلام يُجري عمليات حسابية على العمود سيرجع نتائج غريبة أو NULL. بدون أي خطأ ولا تحذير، فقط فساد صامت في البيانات.
2. المقارنات تتصرّف بشكل غير متوقَّع. عمليات الترتيب وفحص المساواة تعامل القيم باختلاف فئة التخزين الخاصة بها معاملةً مختلفة:
الأعداد الصحيحة تُرتَّب رقمياً، ثم تأتي القيم النصية مرتَّبة أبجدياً — وتظهر بعد كل الأرقام. فتحصل على 2, 3, 10 (الأعداد بالترتيب الرقمي)، ثم '20', '100' (النصوص بالترتيب الأبجدي). وهذا غالباً ليس ما تتوقّعه.
إذا كنت تتحكّم في عمليات الإدخال وتتحقّق من القيم جيداً، فالجداول العادية تفي بالغرض. أما إذا لم تكن تتحكّم بها — أو كنت ببساطة تريد من قاعدة البيانات أن تفرض الأنواع نيابةً عنك — فهناك خيار أفضل.
الخطوة التالية: جداول STRICT
أضافت SQLite في الإصدار 3.37 جداول STRICT، التي تُعطّل سلوك type affinity وترفض أي قيمة لا تطابق النوع المُعلَن. هذه الجداول تمنحك سلوك dynamic typing الافتراضي حين تريده، وتفرض الأنواع بأسلوب Postgres حين لا تريده. وهذا موضوع الصفحة التالية.
الأسئلة الشائعة
ما هو Type Affinity في SQLite؟
هو ببساطة النوع المُفضَّل لتخزين القيم في عمود معيّن. عندك في SQLite خمسة أنواع: TEXT وNUMERIC وINTEGER وREAL وBLOB. لمّا تُدخل قيمة، يحاول SQLite تحويلها إلى نوع العمود المُفضَّل، لكن إذا كان التحويل سيؤدي لفقد بيانات أو كان مستحيلاً، يخزّن القيمة كما هي. يعني الـ Affinity مجرد تلميح، ليس قيد صارم.
كيف يحدد SQLite نوع الـ Affinity للعمود؟
يفحص SQLite اسم النوع التي كتبته في CREATE TABLE ويبحث عن سلاسل نصية بترتيب محدد: لو الاسم يحتوي على INT يصير INTEGER؛ وإلا لو فيه CHAR أو CLOB أو TEXT يصير TEXT؛ وإلا لو فيه BLOB (أو ما حدّدت نوع أصلاً) يصير BLOB؛ وإلا لو فيه REAL أو FLOA أو DOUB يصير REAL؛ وغير ذلك يكون NUMERIC. ولهذا السبب VARCHAR(50) ينتهي كـ TEXT، وBIGINT ينتهي كـ INTEGER — الكلمات التي تكتبها تخضع لمطابقة نمطية.
هل يمكن لعمود في SQLite يقبل قيم من نوع مخالف؟
نعم، في الجداول العادية. عمود معرَّف كـ INTEGER راح يقبل بكل بساطة تخزين النص 'hello'، لأن الـ Affinity مجرد اقتراح للتحويل لا أكثر. إذا تبغى فرض صارم على الأنواع، استخدم جداول STRICT التي ترفض القيم غير المطابقة مباشرة. وراح نتطرّق لها في الجزء التالي.