Menu
flag Ar iconالعربيةdown icon

قيود UNIQUE في SQLite: أعمدة مفردة ومركبة وتعامل NULL

شرح عملي لقيود UNIQUE في SQLite: على مستوى العمود والجدول، المفاتيح المركبة، الفرق مع PRIMARY KEY، تعامل NULL، وكيف تحل خطأ التكرار.

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

قيد UNIQUE في SQLite: لا تكرار مسموح به

قيد UNIQUE في SQLite هو طريقتك لإخبار قاعدة البيانات بأن القيم في عمود معيّن (أو في مجموعة أعمدة) لا يجوز أن تتكرر بين الصفوف. باختصار، هو ما يضمن لك أن يستحيل وجود مستخدمَين بنفس البريد الإلكتروني، أو ظهور رمز منتج أكثر من مرّة واحدة في الجدول.

عملية الإدراج الثالثة تفشل وتظهر رسالة UNIQUE constraint failed: users.email. السبب أن SQLite يتحقق من القيد مع كل عملية كتابة ويرفض أي صف قد يُنتج قيمة مكررة. أول صفّين يُحفظان بنجاح، أما الثالث فلا يصل إلى الجدول أصلاً.

خلف الكواليس، يُنفَّذ قيد UNIQUE على هيئة فهرس فريد (unique index) — وهو نفس بنية البيانات التي يستخدمها SQLite للبحث السريع — ولذلك فإن التحقق رخيص الكلفة، كما يُفهرَس العمود تلقائياً.

صياغة القيد على مستوى العمود مقابل مستوى الجدول

يمكنك كتابة UNIQUE بطريقتين: إما بشكل مضمَّن بجوار العمود مباشرة، أو كبند مستقل في نهاية تعريف الجدول:

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

قيد فريد على عدة أعمدة في SQLite (Composite UNIQUE)

أحيانًا لا يكون العمود الواحد فريدًا بذاته، لكن مجموعة الأعمدة معًا يجب أن تكون كذلك. فالمستخدم يستطيع التسجيل في كورسات متعددة، والكورس الواحد يضم مستخدمين كُثُرًا — لكن لا يجوز أن يتكرر الزوج (user_id, course_id) مرتين:

القيد مفروض على الزوج نفسه، لا على أيٍّ من العمودين بمفرده. فالمستخدم رقم 1 يستطيع التسجيل في كثير من الدورات، والدورة رقم 100 يستطيع أن يلتحق بها كثير من المستخدمين — لكن لا يتكرر التسجيل لنفس التركيبة مرتين.

هذا هو النمط الأساسي والمعتاد لجداول الربط في علاقات متعدّد إلى متعدّد (many-to-many).

الفرق بين UNIQUE و PRIMARY KEY

الاسمان متشابهان والعلاقة بينهما قائمة، لكنّهما ليسا الشيء نفسه:

  • الجدول يحوي مفتاحًا أساسيًا PRIMARY KEY واحدًا على الأكثر، بينما يستطيع أن يحوي قيود UNIQUE متعدّدة.
  • PRIMARY KEY يمثّل هويّة الصف — وهو ما تشير إليه المفاتيح الأجنبية، وما يصبح اسمًا مرادفًا لـ rowid.
  • أمّا UNIQUE فمعناه ببساطة: "هذه القيمة (أو هذه التركيبة) لا تتكرّر."
  • في الجدول العادي، يستطيع عمود UNIQUE أن يحتوي قيم NULL، بينما لا يقبلها PRIMARY KEY (مع استثناء تاريخي واحد سنتجاوزه هنا).

ومن الأشكال الشائعة:

id هو ما تستند إليه بقية قواعد البيانات في المرجعية. أمّا email و username فهما فريدان لأنّ التطبيق يفرض ذلك، لا لأنّهما يمثّلان الهويّة. فلو غيّر المستخدم بريده الإلكتروني، يبقى id كما هو — وهذه هي الفائدة من الفصل بينهما.

مفاجأة NULL في الأعمدة الفريدة

هذه النقطة تُربك الجميع تقريبًا في المرّة الأولى. فعمود UNIQUE في SQLite يقبل أيّ عدد تشاء من قيم NULL:

ثلاث قيم NULL؟ لا مشكلة. لكن قيمتين من 'ada@example.com'؟ هنا يقع التعارض.

السبب أن SQL يتعامل مع NULL على أنها "قيمة مجهولة"، وقيمتان مجهولتان لا تُعتبران متساويتين، ولذلك لا يستطيع فحص التفرّد اعتبارهما مكرّرتين. إذا كنت تريد السماح بقيمة NULL واحدة على الأكثر، فأنظف حلّ هو استخدام NOT NULL UNIQUE معًا. أمّا إذا كانت قيم NULL مقبولة لكن تريد قيمة واحدة فقط لكل تركيبة مع أعمدة أخرى، فاللجوء إلى الفهرس الجزئي (partial index) هو الحل، وسنتناوله لاحقًا في فصل الفهارس.

التعامل مع التعارضات: ON CONFLICT في SQLite

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

الطريقة الأولى مدمجة داخل القيد نفسه عبر ON CONFLICT:

في المرة الثانية التي يُدرج فيها theme، يُحذف الصف الموجود ويأخذ الصف الجديد مكانه. أما الخيارات الأخرى فهي IGNORE (يتجاهل بصمت)، وABORT (الخيار الافتراضي)، وFAIL، وROLLBACK.

الطريقة الثانية تكون على مستوى التعليمة نفسها باستخدام صياغة الـ upsert، وهي عادةً أكثر مرونة لأنها تتيح تحديث أعمدة بعينها:

عملية الإدراج الأولى تنشئ الصف بنجاح. أمّا العمليتان التاليتان فتصطدمان بقيد UNIQUE وتنتقلان إلى فرع DO UPDATE، فيزداد عمود count. هذا هو نمط الـ upsert المعروف بـ INSERT ... ON CONFLICT — وله صفحة مخصّصة لاحقًا.

الفرق بين قيد UNIQUE وفهرس UNIQUE

الأمر CREATE UNIQUE INDEX يؤدّي نفس المهمّة التي يؤدّيها قيد UNIQUE. بل إنّ قيد UNIQUE يُنشئ خلف الكواليس فهرسًا فريدًا (unique index) — فالاثنان في الحقيقة آليّة واحدة تقريبًا بقناعَين مختلفَين.

متى تختار هذا أو ذاك:

  • القيد (Constraint) عندما يكون التفرّد جزءاً أصيلاً من تعريف الجدول، فيظهر موثَّقاً بجوار الأعمدة مباشرة.
  • الفهرس الفريد (unique index) حين تحتاج فهرساً جزئياً (مع جملة WHERE)، أو ترغب في تسميته باسم محدد، أو تريد إضافته إلى جدول موجود دون إعادة كتابته. فأمر ALTER TABLE في SQLite لا يستطيع إضافة قيد، لكنه يقبل دائماً إضافة فهرس.

سلوك الكتابة (writes) متطابق في الحالتين، والاختيار في الغالب يتعلق بالمكان الذي تريد أن تعيش فيه القاعدة داخل المخطط (schema).

إضافة قيد UNIQUE إلى جدول موجود

أمر ALTER TABLE في SQLite محدود عمداً، فلا يوجد فيه ALTER TABLE ... ADD CONSTRAINT. أمامك خياران عمليّان:

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

تنبيه مهم: إذا حاولت فرض التفرّد على عمود يحتوي أصلًا على قيم مكرّرة، فإن CREATE UNIQUE INDEX سيفشل. نظّف الصفوف المكرّرة أولًا، ثم أضِف الفهرس.

رسالة الخطأ عند فشل قيد UNIQUE: كيف تقرأها؟

رسالة الخطأ تخبرك بدقّة عن القيد الذي تسبّب في المشكلة:

خطأ: فشل قيد UNIQUE: users.email
خطأ: فشل قيد UNIQUE: enrollments.user_id, enrollments.course_id

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

  1. حدّد الصف الذي يحمل القيمة المتعارضة (SELECT ... WHERE email = '...').
  2. قرّر ما إذا كنت تريد تحديث ذلك الصف، أو تجاهل عملية الإدخال، أو استخدام قيمة مختلفة.
  3. إذا كانت التكرارات متوقّعة وترغب في دمجها، فاستخدم INSERT ... ON CONFLICT DO UPDATE.

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

التالي: حذف الجداول وتعديلها

لا يمكن إضافة قيد UNIQUE إلى جدول موجود عبر أمر ALTER TABLE بسيط. هذا القيد بالذات هو ما يجعل SQLite يلجأ إلى رقصة خاصة لتعديل المخطط تُعرف بإعادة كتابة الجدول، وهي موضوع الصفحة التالية، إلى جانب أساسيات حذف الجداول بطريقة نظيفة.

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

كيف أضيف قيد UNIQUE في SQLite؟

تقدر تضيفه على مستوى العمود مباشرة هكذا email TEXT UNIQUE، أو تكتبه على مستوى الجدول لأكثر من عمود بصيغة UNIQUE(col1, col2). خلف الكواليس ينشئ SQLite فهرساً فريداً (unique index) لفرض القيد، ويرفض أي INSERT أو UPDATE يؤدي إلى قيمة مكررة.

ما الفرق بين UNIQUE وPRIMARY KEY في SQLite؟

الجدول يقبل PRIMARY KEY واحداً فقط، بينما يمكن أن يحتوي على عدة قيود UNIQUE. كذلك PRIMARY KEY يستلزم ضمنياً NOT NULL (في الجداول الصارمة STRICT ومع INTEGER PRIMARY KEY)، أما العمود UNIQUE فيقبل عدة قيم NULL. باختصار: استخدم المفتاح الأساسي لتعريف هوية الصف، وUNIQUE لباقي الأعمدة التي يجب ألا تتكرر.

لماذا يسمح SQLite بتكرار قيم NULL في عمود UNIQUE؟

لأن SQL تتعامل مع NULL على أنها قيمة "غير معروفة"، واثنان من المجهول لا يُعتبران متساويين. لذلك العمود UNIQUE يقبل أي عدد من الصفوف بقيمة NULL، والشرط ينطبق فقط على القيم غير الفارغة. لو أردت السماح بـ NULL واحدة فقط، أضف NOT NULL أو استخدم فهرساً فريداً جزئياً (partial unique index).

كيف أحل خطأ UNIQUE constraint failed؟

هذا الخطأ يعني أن عملية INSERT أو UPDATE ستُنتج قيمة مكررة في عمود UNIQUE (أو PRIMARY KEY). الحل: إما تغيّر القيمة التي تحاول إدراجها، أو تحذف الصف الموجود أولاً، أو تستخدم INSERT ... ON CONFLICT (أي عملية upsert) لتخبر SQLite بما يفعله عند حدوث التعارض.

Coddy programming languages illustration

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

ابدأ الآن