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

UPSERT في SQLite: ON CONFLICT DO UPDATE و DO NOTHING

شرح عملي لـ UPSERT في SQLite: جملة ON CONFLICT، الجدول الوهمي excluded، الفرق بين DO NOTHING و DO UPDATE، ولماذا هي أفضل من INSERT OR REPLACE.

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

الإدراج أو التحديث إذا كان السجل موجوداً مسبقاً

حاجة شائعة جداً: تريد إدراج صف، لكن إذا كان هناك صف بنفس المفتاح موجوداً مسبقاً، فعليك تحديثه بدلاً من ذلك. بدون UPSERT، ستضطر لكتابة SELECT أولاً، ثم التفرّع إلى INSERT أو UPDATE — وهذا يعني رحلتين إلى قاعدة البيانات، فضلاً عن وجود حالة تسابق (race condition) بينهما.

أمر UPSERT في sqlite ينجز كل ذلك في جملة واحدة:

أول مرة تشغّل الاستعلام، يُدرَج الصف. شغّله مرة ثانية بسعر مختلف ونفس قيمة sku، وسيُحدَّث الصف الموجود في مكانه. لا تكرار، ولا أخطاء.

تشريح جملة ON CONFLICT

الصيغة الكاملة لـ sqlite upsert:

INSERT INTO table (...) VALUES (...)
ON CONFLICT(conflict_target) DO UPDATE SET col = expr, ...
WHERE condition;

ثلاثة أجزاء تهمّك:

  • conflict_target — العمود أو الأعمدة التي عليها قيد UNIQUE أو PRIMARY KEY وتتوقع حدوث التعارض عندها. يستخدمها SQLite ليعرف أيّ فهرس يراقب.
  • DO UPDATE SET ... — ما الذي تريد تغييره في الصف الموجود عند حدوث تعارض. (أو DO NOTHING لتجاهل الأمر بصمت.)
  • WHERE اختياري — شرط إضافي يجب أن يتحقق حتى يُنفَّذ التحديث فعلًا.

لا بدّ أن يتطابق هدف التعارض مع قيد فريد حقيقي. فجملة ON CONFLICT(price) لن تعمل إذا لم يكن العمود price فريدًا، ببساطة لأنه لا يوجد ما يكتشف SQLite تعارضًا بناءً عليه.

DO NOTHING: أدرج إن لم يكن موجودًا، وإلا تجاهل

هذا هو الشكل الأبسط. مفيد عندما تملأ بيانات أوّلية أو تسجّل أحداثًا وتريد تجاوز التكرارات بهدوء دون ضجيج:

عملية الإدراج الثانية ستصطدم بنفس event_id، والمفروض أنها ترفع خطأ UNIQUE constraint failed. لكن مع DO NOTHING، تتجاوزها SQLite ببساطة. لا استثناء، ولا صف يتأثر.

هذا بالضبط ما يُعرف بـ"الإدراج المتكرر الآمن" (idempotent insert)، والذي يلجأ كثيرون لأجله إلى INSERT OR IGNORE. لكن DO NOTHING في الـ upsert يؤدي نفس الغرض، ويتكامل بشكل أفضل مع جملتَي WHERE وRETURNING.

الجدول الوهمي excluded في sqlite

عند وقوع التعارض، يصبح لديك صفّان في المشهد: الصف الموجود أصلاً في الجدول، والصف الجديد الذي حاولت إدراجه. وتمنحك SQLite طريقة للإشارة إلى كليهما.

  • أسماء الأعمدة المجرّدة (price، name) تشير إلى الصف الموجود حالياً.
  • excluded.column تشير إلى الصف الوارد الذي تم رفضه.

quantity = quantity + excluded.quantity تُقرأ ببساطة: «الكمية الحالية + الكمية الجديدة». بعد تنفيذ عمليتَي الإدراج، تصبح كمية A-100 تساوي 8. هذا النمط — أي تجميع القيم فوق صف موجود — من أكثر استخدامات UPSERT فائدةً وشيوعاً.

UPSERT شرطي باستخدام WHERE

تُتيح لك جملة WHERE في النهاية تخطّي التحديث ما لم يتحقّق شرط معيّن. يُطبَّق هذا الشرط على الصف الموجود فعلاً (ويمكنه أيضاً الإشارة إلى excluded.* للقيم الواردة الجديدة):

الصف الجديد يحمل قيمة updated_at أقدم، فيكون شرط WHERE غير متحقق ويُتجاهل التحديث. يحتفظ الصف الموجود بسعره الأحدث. لو عكست التواريخ، سيُنفَّذ التحديث. هذا هو النمط المعتاد لقاعدة "لا تكتب فوق البيانات إلا إذا كانت أحدث".

upsert على عدة صفوف في sqlite

يمكن أن تحتوي VALUES على عدة صفوف، وتُطبَّق ON CONFLICT على كل صف منها بشكل مستقل:

السجل A-100 يتعارض فيتم تحديثه، أما A-200 وA-300 فهما جديدان ويتم إدراجهما. تعليمة واحدة تجمع بين الإدراج والتحديث في نتيجة واحدة. هذه طريقة نظيفة لمزامنة دفعة من السجلات قادمة من مصدر خارجي.

الفرق بين UPSERT و INSERT OR REPLACE

للوهلة الأولى يبدو أن INSERT OR REPLACE يقوم بنفس المهمة، لكن الحقيقة غير ذلك.

اختفى عمود notes تمامًا. ما حدث هو أن INSERT OR REPLACE حذف الصف رقم 1 بالكامل وأدرج صفًا جديدًا مكانه، فأي عمود لم تذكره صراحةً رجع إلى قيمة NULL أو إلى قيمته الافتراضية. وفوق ذلك، تُفعِّل هذه العملية مُشغِّلات DELETE وتُسبِّب تتالي الحذف عبر مفاتيح ON DELETE الأجنبية.

أما عملية UPSERT فتُحافظ على الصف كما هو:

notes لا تزال موجودة كما هي. التغيير حصل فقط على الأعمدة المذكورة داخل SET. خُذها قاعدة: استخدم UPSERT افتراضيًا، ولا تلجأ إلى INSERT OR REPLACE إلا لو كنت فعلًا تريد دلالة "احذف ثم أعد الإدراج".

أهداف تعارض متعددة في sqlite upsert

لو كان الصف الواحد قابلًا للتعارض مع أكثر من قيد، تقدر تربط أكثر من جملة ON CONFLICT ورا بعض:

أيّ قيد يُطلَق أولًا هو الفائز، ويُنفَّذ DO UPDATE التابع له. عمليًا، معظم الجداول لها هدف تعارض واحد واضح — إمّا المفتاح الأساسي أو عمود UNIQUE واحد — ونادرًا ما ستحتاج لأكثر من جملة واحدة.

أخطاء شائعة في sqlite upsert

هناك بعض النقاط التي يقع فيها الكثيرون:

  • بدون فهرس فريد مطابق، لا يوجد UPSERT. جملة ON CONFLICT(col) تتطلّب أن يكون col إمّا PRIMARY KEY أو يحمل قيد UNIQUE، وإلا سترمي SQLite خطأ "no such constraint".
  • DO UPDATE لا يعمل إذا لم يحدث تعارض. فهو بديل للإدراج وليس سلوكًا إضافيًا. في أول مرة يظهر فيها المفتاح، يُنفَّذ الإدراج فقط.
  • excluded للقراءة فقط. يمكنك القراءة منه لكن لا يمكنك الكتابة إليه. هدف SET هو دائمًا الصف الموجود فعلًا في الجدول.
  • معرّفات INTEGER PRIMARY KEY المُولَّدة تلقائيًا. إذا لم تُمرِّر القيمة بنفسك، فكلّ عملية إدراج ستحصل على معرّف جديد — وبالتالي لا يوجد ما يتعارض معه. الـ UPSERT منطقي فقط عندما يكون العمود المتعارض يحمل قيمة محدّدة يُمرّرها المستدعي.

التالي: RETURNING

الـ UPSERT لا يخبرك أيّ الصفوف أُدرجت وأيّها تم تحديثها، ولا ما هي قيمها النهائية. لهذا الغرض تأتي جملة RETURNING — تُعيد لك الصفوف المتأثّرة في نفس الاستعلام، دون الحاجة إلى SELECT لاحق. هذا ما سنتناوله بعد قليل.

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

ما هو UPSERT في SQLite؟

ببساطة، UPSERT هو أمر INSERT يتحوّل تلقائيًا إلى UPDATE (أو يتجاهل العملية تمامًا) عندما يصطدم بقيد UNIQUE أو PRIMARY KEY. تكتبه بصيغة INSERT ... ON CONFLICT(column) DO UPDATE SET ... أو DO NOTHING. هذه الميزة متاحة في SQLite منذ الإصدار 3.24.0 الصادر عام 2018.

ما هو الجدول excluded في UPSERT؟

excluded جدول وهمي خاص يحتوي على الصف الذي حاولت إدراجه ورُفض بسبب التعارض. داخل DO UPDATE SET ... تشير إلى الصف الموجود مسبقًا باسم العمود مباشرة، وإلى الصف المرفوض عبر excluded.column. مثلًا SET price = excluded.price تعني: «استبدل قيمة price بالقيمة الجديدة التي جاءت مع أمر INSERT».

ما الفرق بين INSERT OR REPLACE و UPSERT؟

INSERT OR REPLACE يحذف الصف المتعارض كليًا ثم يُدرج صفًا جديدًا مكانه، وهذا يعني تنفيذ مُشغّلات DELETE، وكسر المفاتيح الأجنبية المرتبطة بـ ON DELETE CASCADE، وإعادة جميع الأعمدة إلى قيمها الافتراضية. أما UPSERT فيُحدّث الصف الحالي في مكانه، فتتغيّر فقط الأعمدة التي تحدّدها في SET. القاعدة: استخدم UPSERT دائمًا، إلا إذا كنت فعلًا تريد عملية حذف وإعادة إدراج كاملة.

هل يمكن تنفيذ UPSERT على عدة صفوف دفعة واحدة في SQLite؟

نعم، الصيغة INSERT INTO t(...) VALUES (...), (...), (...) ON CONFLICT(col) DO UPDATE SET ... تعمل بدون مشاكل. يتم فحص كل صف على حدة مقابل هدف التعارض، وداخل DO UPDATE يشير excluded إلى الصف القادم الذي تسبّب في التعارض تحديدًا.

Coddy programming languages illustration

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

ابدأ الآن