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

ROWID في SQLite: المفتاح الخفي و WITHOUT ROWID

تعرّف على ما هو ROWID فعلياً في SQLite، ومتى يصبح INTEGER PRIMARY KEY مجرد اسم بديل له، ولماذا توجد جداول WITHOUT ROWID أصلاً.

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

كل جدول في SQLite يخفي عمودًا سريًا

أنشئ جدولًا عاديًا في SQLite، وستجد أن هناك عمودًا واحدًا موجودًا فعلًا دون أن تُصرّح به:

عمود rowid في sqlite حقيقي وموجود فعلًا. فـ SQLite يُسنِد قيمة rowid لكل صف في كل جدول عادي، سواء طلبتَ ذلك أم لا. هو عدد صحيح بإشارة بطول 64 بت، فريد داخل الجدول، وهو المفتاح الفعلي الذي يستخدمه SQLite للوصول إلى الصفوف داخل بنية B-tree التخزينية. تخيّله كالعمود الفقري للجدول، أي الفهرس الذي يُبقي كل شيء آخر مُرتَّبًا.

عادةً لا تراه لأن SELECT * لا يُدرجه في النتائج، فلا بد أن تطلبه باسمه صراحةً.

أسماء rowid الثلاثة في sqlite

نظرًا لأن rowid يَرِد كثيرًا في استعلامات SQL المكتوبة لقواعد بيانات أخرى، يقبل SQLite ثلاثة أسماء للعمود نفسه:

كل من rowid و oid و _rowid_ يشير إلى نفس العمود المخفي. لو صادف وعرّفت عمودًا حقيقيًا بأحد هذه الأسماء، فعمودك هو من سيفوز وسيختفي الاسم المستعار — لكن هذا هو الاستثناء الوحيد. في الكود اليومي، اكتب rowid وانتهى الأمر.

عبارة INTEGER PRIMARY KEY هي الكلمة السحرية

هنا تكمن النقطة التي تربك كل من يأتي من قواعد بيانات أخرى. إذا عرّفت عمودًا بالصيغة INTEGER PRIMARY KEY بالضبط، فهذا العمود لا يُخزَّن بشكل منفصل — بل يصبح هو الـ rowid نفسه:

rowid و id هما نفس العمود لكن باسمَين مختلفَين. إذا أدرجتَ صفًّا جديدًا دون تحديد قيمة لـ id، فسيُسنَد له تلقائيًا رقم صحيح (يكون عادةً أكبر قيمة rowid موجودة + 1). لهذا السبب يُعدّ INTEGER PRIMARY KEY الطريقة الأكفأ لإضافة مفتاح أساسي ذاتي التزايد لأي جدول في sqlite — فلا يوجد عمود إضافي ولا فهرس إضافي، بل rowid نفسه يقوم بالمهمة.

انتبه إلى طريقة الكتابة بدقة: فـ INT PRIMARY KEY ليس مكافئًا لـ INTEGER PRIMARY KEY، إذ يختلف تأثير كلٍّ من INT وINTEGER هنا اختلافًا جوهريًا:

في الجدول a، تجد أن id و rowid متطابقان. أما في الجدول b، فإن id مجرد عمود عادي، و rowid يبقى عموداً صحيحاً مخفياً منفصلاً عنه. والأسوأ من ذلك أن b.id لا يُملأ تلقائياً عند الإدراج، بل تكون قيمته NULL ما لم تُحدِّدها بنفسك. لذلك التزم بكتابة INTEGER PRIMARY KEY كاملةً متى أردت سلوك الـ alias في sqlite.

كيفية الحصول على rowid بعد الإدراج في sqlite

بعد تنفيذ أمر INSERT، كثيراً ما تحتاج إلى معرفة الـ rowid الذي أُسنِد للصف الجديد، خصوصاً حين تريد ربط صف فرعي به. ولهذا الغرض توفّر sqlite الدالة last_insert_rowid():

تُعيد هذه الدالة قيمة rowid لآخر عملية إدراج ناجحة على الاتصال الحالي. ومعظم برامج تعريف قواعد البيانات تتيح نفس القيمة عبر cursor.lastrowid أو ما يشبهها. كذلك تُعدّ جملة RETURNING (التي سنتناولها لاحقًا) طريقة بديلة لاسترجاع هذه القيمة ضمن عملية الإدراج نفسها.

قيم rowid في sqlite ليست دائمة

تبقى قيمة rowid الخاصة بالصف ثابتة ما دام الصف موجودًا، لكنها ليست معرّفًا مدى الحياة. فتنفيذ VACUUM قد يُعيد ترقيم قيم rowid، وإذا حذفت صفًا فقد يُعاد استخدام رقمه في عملية إدراج لاحقة:

لاحظ أن الصف الجديد قد يُعيد استخدام rowid القديم أو لا يُعيد استخدامه، وذلك بحسب إصدار SQLite والظروف المحيطة — والفكرة هنا أنك لا تستطيع الاعتماد على بقاء هذا المعرّف فريدًا إلى الأبد. إذا كنت بحاجة إلى معرّف يصمد بعد عمليات الحذف وVACUUM والتصدير، فعرّف عمود INTEGER PRIMARY KEY خاصًا بك (وهو ما يُثبّت القيمة على الصف نفسه)، وفكّر في استخدام الكلمة المفتاحية AUTOINCREMENT إذا كنت تحتاج تحديدًا إلى قيم تتزايد تصاعديًا ولا يُعاد استخدامها أبدًا.

جداول WITHOUT ROWID في SQLite

في بعض الأحيان يكون rowid عبئًا زائدًا لا تحتاجه — وغالبًا ما يحدث ذلك عندما لا يكون مفتاحك الحقيقي عددًا صحيحًا. لنفترض أن لديك جدولًا للمدن مفتاحه هو الاسم؛ ستجد نفسك أمام بنيتين منفصلتين: شجرة B-tree الخاصة بـ rowid، وفهرس مستقل على name لفرض المفتاح الأساسي. ما تفعله WITHOUT ROWID هو دمج هاتين البنيتين في بنية واحدة:

الآن أصبح العمود name هو مفتاح التخزين الفعلي. عمليات البحث عن طريق name تتخطى مستوى من التوجيه غير المباشر، كما أن حجم الجدول يصبح أصغر. لكن هناك مقايضات:

  • لا وجود لأعمدة rowid أو oid أو _rowid_، فهي ببساطة غير موجودة.
  • الدالة last_insert_rowid() لا تتحدّث عند الإدراج في هذا الجدول.
  • إدخال/إخراج BLOB التزايدي وبعض ميزات النسخ المتماثل غير متاحة.
  • يجب أن يحتوي الجدول على PRIMARY KEY معلَن.

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

النموذج الذهني

باختصار شديد:

  • كل جدول عادي في SQLite يحتوي على مفتاح عددي مخفي بحجم 64 بت يُسمّى rowid.
  • استخدام INTEGER PRIMARY KEY (بهذه الصيغة بالضبط) يجعل عمودك اسمًا بديلًا لهذا المفتاح.
  • استخدم last_insert_rowid() لقراءة القيمة التي تم تعيينها للتوّ.
  • يمكن إعادة استخدام قيم rowid بعد الحذف، كما يمكن إعادة ترقيمها عبر VACUUM.
  • جداول WITHOUT ROWID تُلغي هذا المفتاح المخفي وتستخدم المفتاح الأساسي الذي أعلنته مباشرةً، وهو خيار مفيد للمفاتيح غير العددية، لكنه يُكلّفك التنازل عن بعض الميزات.

في معظم الأحيان لن تشغل بالك بـ rowid على الإطلاق. تُعلِن id INTEGER PRIMARY KEY، وتترك SQLite يتولّى الترقيم، وتمضي في عملك. تبرز أهمية هذه التفاصيل حين تضبط أداء التخزين، أو تقرأ مخططات جاهزة، أو تتساءل لماذا يتصرّف INT PRIMARY KEY بشكل مختلف عن INTEGER PRIMARY KEY.

ما التالي: NOT NULL و DEFAULT

بعد أن استقرّت هوية الصف، تأتي الطبقة التالية وهي التأكد من أن بقية الأعمدة تحمل قيمًا منطقية. الجملتان NOT NULL و DEFAULT تقومان بالنصيب الأكبر من هذه المهمة، وسنتناولهما في الدرس القادم.

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

ما هو ROWID في SQLite؟

كل جدول عادي في SQLite يحتوي على عمود مخفي باسم rowid من نوع integer بإشارة وحجم 64-bit، وهذا العمود يُميّز كل صف بشكل فريد. يستخدمه SQLite داخلياً كمفتاح فعلي في تخزين B-tree، ويمكنك قراءته مباشرة عبر SELECT rowid, * FROM t حتى لو لم تُعرّفه أنت في الجدول.

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

rowid موجود دائماً بشكل تلقائي، أما PRIMARY KEY فهو شيء أنت من يُعلنه. الحالة الخاصة هي INTEGER PRIMARY KEY — هنا يصبح العمود مجرد اسم بديل (alias) لـ rowid بدلاً من أن يكون عموداً مستقلاً. أي مفتاح أساسي آخر — سواء كان نصياً أو مركّباً أو حتى INT PRIMARY KEY بدون كلمة INTEGER كاملة — يُخزَّن بجانب rowid لا بديلاً عنه.

ماذا تفعل WITHOUT ROWID في SQLite؟

خيار WITHOUT ROWID يطلب من SQLite تجاهل العمود المخفي rowid واستخدام PRIMARY KEY الذي عرّفته أنت كمفتاح تخزين فعلي. هذا قد يوفّر مساحة ويُسرّع عمليات البحث في الجداول التي تستخدم مفاتيح غير رقمية، لكنه يُعطّل بعض الميزات مثل last_insert_rowid() و incremental BLOB I/O. لذلك استخدمه عن قناعة، لا كإعداد افتراضي.

Coddy programming languages illustration

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

ابدأ الآن