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

حذف الصفوف في SQLite: DELETE مع WHERE وRETURNING

تعرّف على طريقة عمل DELETE في SQLite: كتابة شرط WHERE بأمان، حذف كل الصفوف، تمرير الحذف للجداول المرتبطة عبر CASCADE، واسترجاع الصفوف المحذوفة باستخدام RETURNING.

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

أمر DELETE في SQLite: حذف الصفوف فقط لا غير

أمر DELETE مهمته إزالة الصفوف من الجدول. هو لا يحذف الجدول نفسه، ولا يُعدّل بنيته (schema)، ولا يمسّ أي جدول آخر (إلا إذا كنت قد فعّلت الحذف المتسلسل CASCADE). الصيغة بسيطة ومختصرة:

DELETE FROM users WHERE id = 2; يبحث عن الصفوف المطابقة للشرط ثم يحذفها. أما الصفّان الآخران فيبقيان كما هما دون أي تغيير. والجدول نفسه لا يزال موجوداً، ويمكنك متابعة الإدراج فيه بشكل طبيعي.

الفكرة باختصار: تخيّل أمر DELETE كأنه SELECT، لكن بدل أن يُرجِع لك الصفوف المطابقة، يتخلّص منها.

جملة WHERE هي بطلة العملية كلها

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

لقد اختفت المسودّتان غير المنشورتين اللتان لم تحظَيا بأي مشاهدة، بينما بقيت الصفوف المنشورة سليمة لأن الشرط لم ينطبق عليها. ويمكنك استخدام أي تعبير تقبله جملة WHERE، سواء كان IN أو LIKE أو BETWEEN أو استعلامات فرعية أو تركيبات من AND وOR.

عادة يستحق أن تتبنّاها: قبل تنفيذ أمر DELETE، جرّب نفس شرط WHERE مع SELECT أولاً.

-- معاينة ما سيتم حذفه:
SELECT * FROM posts WHERE published = 0 AND views = 0;

-- هل أنت راضٍ عن الصفوف؟ الآن قم بحذفها:
DELETE FROM posts WHERE published = 0 AND views = 0;

هذي الرقصة من خطوتين أنقذت قواعد بيانات أكثر من كل أدوات النسخ الاحتياطي مجتمعة.

استخدام DELETE بدون WHERE يفرّغ الجدول بالكامل

لو أهملت شرط WHERE مع DELETE، راح يحذف كل الصفوف الموجودة في الجدول:

الجدول صار فارغًا لكنه ما زال موجودًا. لا تحتوي SQLite على عبارة TRUNCATE، فالمكافئ لها هو DELETE FROM table;، وتطبّق SQLite داخليًا ما يُعرف بـ"تحسين الاقتطاع" (truncate optimization) الذي يُسقط كل الصفحات دفعة واحدة بدلًا من حذف الصفوف صفًا صفًا. إنها عملية سريعة، لكنها تبقى عملية ضمن معاملة (transaction) يمكن التراجع عنها.

أما إذا كنت تستخدم AUTOINCREMENT على المفتاح الأساسي، فالعدّاد لن يُصفَّر تلقائيًا. ولكي تبدأ المعرّفات من 1 من جديد، عليك أيضًا حذف صف التسلسل الخاص بالجدول:

DELETE FROM log;
DELETE FROM sqlite_sequence WHERE name = 'log';

بالنسبة للعمود INTEGER PRIMARY KEY العادي (بدون AUTOINCREMENT)، فإن SQLite يُعيد استخدام المعرّفات تلقائياً، لذا لا حاجة لهذه الخطوة.

حذف عدة صفوف محددة دفعة واحدة

تُعدّ IN الطريقة الأنظف لحذف مجموعة معروفة من الصفوف:

يمكنك أيضًا تنفيذ عملية حذف صف في SQLite اعتمادًا على استعلام فرعي (subquery)، وهذا مفيد عندما تكون الصفوف المراد حذفها معرَّفة عبر ربط (join) مع جدول آخر:

لا يدعم SQLite صيغة DELETE ... JOIN كما تفعل MySQL، لكن استخدام استعلام فرعي داخل WHERE يؤدي نفس الغرض تمامًا.

RETURNING: استرجاع الصفوف المحذوفة

أضف RETURNING للحصول على الصفوف المحذوفة كمجموعة نتائج، تمامًا مثل استعلام SELECT:

ستحصل في المقابل على id وemail لكل صف محذوف. وهذا أمر بالغ الفائدة في حالات مثل:

  • تسجيل ما تم حذفه فعليًا بدقة.
  • بناء ميزة التراجع (undo) عبر تخزين الصفوف المُعادة في مكان ما.
  • التأكد من أن عملية الحذف طالت الصفوف المتوقعة فعلًا، وكل ذلك في رحلة واحدة إلى قاعدة البيانات.

تعمل RETURNING مع INSERT وUPDATE وDELETE، وقد خصصنا لها صفحة مستقلة نشرحها فيها بالتفصيل.

استخدام ON DELETE CASCADE لحذف الصفوف المرتبطة

عندما يربط مفتاح خارجي بين جدول أب وجدول ابن، فإن حذف صف من جدول الأب يترك صفوفًا يتيمة في جدول الابن — ما لم تُخبر SQLite بأن تُجري الحذف على نحو متسلسل (cascade):

حذف المؤلف يؤدي تلقائياً إلى حذف كتبه. أما بدون ON DELETE CASCADE، فإن نفس الأمر DELETE إما أن ينجح ويترك كتباً يتيمة بلا مؤلف (إذا كانت المفاتيح الأجنبية معطّلة)، أو يفشل برسالة خطأ في القيود (إذا كانت مُفعّلة).

الفخ الكبير هنا: المفاتيح الأجنبية معطّلة افتراضياً في SQLite. لذا يجب تنفيذ PRAGMA foreign_keys = ON; مع كل اتصال جديد. وإن لم تُفعّل هذه الـ pragma، فإن ON DELETE CASCADE يُتجاهَل بصمت — وتبقى الكتب في مكانها. معظم تعريفات (drivers) التطبيقات إما تضبط هذا الإعداد نيابةً عنك أو توفّر خياراً لتفعيله، فتحقق من التعريف الذي تستخدمه.

من خيارات التتالي الأخرى التي يجدر بك معرفتها: ON DELETE SET NULL (لتفريغ قيمة المفتاح الأجنبي)، وON DELETE RESTRICT (لرفض عملية الحذف إذا كانت هناك سجلات مرتبطة)، وON DELETE NO ACTION (وهو السلوك الافتراضي — ويعمل غالباً مثل RESTRICT).

استخدام DELETE مع LIMIT (خيار يُحدَّد وقت الترجمة)

بعض إصدارات SQLite تدعم الصيغة DELETE ... LIMIT، وهي مفيدة لتقليص الجداول الضخمة على دفعات صغيرة:

DELETE FROM logs
WHERE created_at < '2024-01-01'
ORDER BY created_at
LIMIT 1000;

يتطلب هذا أن تكون SQLite مُجمَّعة مع الخيار SQLITE_ENABLE_UPDATE_DELETE_LIMIT. الإصدارات الرسمية ومعظم مكتبات الربط للغات البرمجة (مثل sqlite3 في بايثون، وbetter-sqlite3 في Node) تأتي مع هذا الخيار مُفعَّلاً. أما إذا كانت نسختك لا تدعمه، فستظهر لك رسالة خطأ في بناء الجملة، وحينها يمكنك اللجوء إلى استعلام فرعي بديل:

DELETE FROM logs
WHERE id IN (
    SELECT id FROM logs
    WHERE created_at < '2024-01-01'
    ORDER BY created_at
    LIMIT 1000
);

الحذف على دفعات يحافظ على حجم صغير للمعاملات (Transactions)، وهذا أمر مهم عندما تكون هناك اتصالات أخرى تقرأ من قاعدة البيانات في نفس الوقت.

تغليف عمليات الحذف الكبيرة داخل معاملة

أمر DELETE في SQLite معاملاتي ضمنيًا — يعني إما أن تُحذف كل الصفوف المطابقة أو لا يُحذف أي شيء منها. لكن عندما تكون على وشك حذف كمية كبيرة من البيانات، فإن تغليف العملية داخل معاملة صريحة يمنحك إمكانية تنفيذ ROLLBACK إذا لاحظت أن شيئًا ما ليس على ما يرام:

ROLLBACK يتراجع عن عملية الحذف بالكامل. في جلسة حقيقية، ستستخدم COMMIT بمجرد أن يبدو العدد صحيحًا. كما أن المعاملات (Transactions) أسرع بشكل ملحوظ عند حذف عدد كبير من الصفوف صفًا تلو الآخر — فتغليف الحلقة بـ BEGIN/COMMIT يجنّبك استدعاء fsync مع كل عملية حذف.

أشياء لا يقوم DELETE بحذفها فعليًا

هناك بعض النقاط التي يلتبس فيها الأمر كثيرًا، ويجدر التنبيه إليها:

  • DELETE FROM table; يُفرّغ الجدول لكنه لا يحذفه. لإزالة الجدول نفسه استخدم DROP TABLE table;.
  • أمر DELETE لا يُقلّص حجم ملف قاعدة البيانات. الصفحات يتم تعليمها كصفحات حرة لإعادة الاستخدام لاحقًا. ولاسترجاع المساحة فعليًا على القرص، شغّل VACUUM; (سنتناوله في فصل الأداء).
  • حذف صف في sqlite لا يؤدي تلقائيًا إلى حذف الصفوف الأبناء في الجداول الأخرى، إلا إذا كانت ON DELETE CASCADE مُفعَّلة و المفاتيح الأجنبية مُفعَّلة عبر PRAGMA foreign_keys.
  • جملة DELETE التي لا تطابق أي صف ليست خطأ. هي جملة ناجحة لكن قيمة changes() = 0. إذا كنت تحتاج التأكد، تحقق من عدد الصفوف المتأثرة.

التالي: UPSERT

في كثير من الأحيان، أنت لا تريد الحذف فعليًا — بل تريد إدراج الصف إن كان جديدًا، أو تحديثه إن كان موجودًا مسبقًا. SQLite يسمي هذه العملية UPSERT، وعبارة ON CONFLICT تختصر العملية في جملة واحدة بدلاً من ثلاث. هذا ما سنراه في الفصل القادم.

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

كيف أحذف صفًا واحدًا في SQLite؟

تستخدم الصيغة DELETE FROM table_name WHERE condition;، حيث يحدّد شرط WHERE الصفوف التي ستُحذف. مثلًا الأمر DELETE FROM users WHERE id = 7; يحذف المستخدم صاحب المعرّف 7 فقط. انتبه: إذا أهملت WHERE، فسيُمسح كل ما في الجدول.

كيف أحذف جميع الصفوف من جدول في SQLite؟

نفّذ DELETE FROM table_name; بدون شرط WHERE. لا يوجد في SQLite أمر TRUNCATE، فالـDELETE بدون شرط هو البديل، ويتعامل معه المحرّك داخليًا بطريقة محسّنة تُعرف بـ"truncate optimization". وإذا أردت إعادة تصفير عدّاد AUTOINCREMENT أيضًا، فاحذف السجلات المقابلة من جدول sqlite_sequence بعد ذلك.

هل يدعم SQLite تمرير الحذف إلى الجداول المرتبطة (CASCADE)؟

نعم، شرط أن تُعرّف المفتاح الأجنبي بـON DELETE CASCADE، وأن تُفعّل المفاتيح الأجنبية بالأمر PRAGMA foreign_keys = ON;. المفاتيح الأجنبية معطّلة افتراضيًا في SQLite، لذا فإن نسيان هذا الـPRAGMA يعني أن الـCASCADE سيُتجاهل بصمت دون أي تحذير.

كيف أعرف ما الصفوف التي حُذفت فعلًا؟

أضف عبارة RETURNING في نهاية الأمر، مثل: DELETE FROM users WHERE active = 0 RETURNING id, email;، فترجع لك الصفوف المحذوفة تمامًا كما لو نفّذت SELECT. هذه الميزة مفيدة لتسجيل العمليات (logging)، أو بناء خاصية تراجع (undo)، أو للتأكد من أنك حذفت ما تقصده بالضبط.

Coddy programming languages illustration

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

ابدأ الآن