تخطَّ إلى المحتوى

٢١ يوليو ٢٠٢٦

UPSERT بالعربي: ON CONFLICT في PostgreSQL مقابل ON DUPLICATE KEY UPDATE في MySQL

شارك المقال:

من الأنماط المتكرّرة جدًا في تطوير الويب: إدراج صف إن لم يكن موجودًا، أو تحديثه إن كان موجودًا — عدّاد زيارات صفحة، آخر وقت دخول مستخدم، أو مزامنة بيانات من مصدر خارجي. الحل الساذج بخطوتين منفصلتين (تحقّق ثم إدراج) يبدو منطقيًا لكنه مليء بفخّ خفي. هذا شرح عملي للفخّ، ولحلّه في PostgreSQL وMySQL — أشهر قاعدتَي SQL استخدامًا.

الفخّ: شرط السباق (Race Condition)

-- 1) تحقّق
SELECT id FROM page_views WHERE page_id = 42;

-- 2) بناءً على النتيجة: INSERT إن لم يوجد، أو UPDATE إن وُجد

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

PostgreSQL: ON CONFLICT

INSERT INTO page_views (page_id, views)
VALUES (42, 1)
ON CONFLICT (page_id) DO UPDATE
SET views = page_views.views + 1;
  • ON CONFLICT (page_id): يحدّد صراحة عمود (أو أعمدة) التعارض، ويجب أن يكون عليه فهرس فريد أو مفتاح أساسي — بدونه يرفض PostgreSQL الأمر لأنه لا يعرف أي تعارض يقصد.
  • EXCLUDED: جدول وهمي يمثّل الصف الذي كان سيُدرَج لولا التعارض، تستخدمه للوصول للقيم الجديدة داخل SET.
  • بديل DO UPDATE هو DO NOTHING لتجاهل التكرار بصمت بلا أي تحديث.

MySQL: ON DUPLICATE KEY UPDATE

INSERT INTO page_views (page_id, views)
VALUES (42, 1)
ON DUPLICATE KEY UPDATE views = views + 1;

هنا لا داعي لتحديد عمود التعارض صراحة — MySQL يتحقّق تلقائيًا من أي مفتاح أساسي أو فهرس فريد على الجدول. للإشارة إلى القيمة الجديدة المقترح إدراجها في تعبير أكثر تعقيدًا، تُستخدم إما VALUES(column) (الصياغة القديمة، أصبحت مهملة رسميًا منذ MySQL 8.0.19 ومرشّحة للإزالة مستقبلًا)، أو صياغة أحدث بأسماء مستعارة:

INSERT INTO page_views (page_id, views) VALUES (42, 1) AS new
ON DUPLICATE KEY UPDATE views = page_views.views + new.views;

جدول المقارنة

المعيارPostgreSQLMySQL
الجملةON CONFLICT (col) DO UPDATEON DUPLICATE KEY UPDATE
تحديد عمود التعارضإلزامي مع DO UPDATEغير مطلوب — تلقائي من أي قيد فريد
الإشارة للقيمة الجديدةEXCLUDED.columnnew.column (أو VALUES() القديمة والمهملة)
تجاهل التكرار بلا تحديثON CONFLICT ... DO NOTHINGINSERT IGNORE (أمر منفصل بالكامل)
تحديث مشروطWHERE بعد SETغير مدعوم بنفس الصياغة المباشرة

أيّهما تختار؟

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

أكمل مسار SQL الكامل بالعربي لتتعمّق أكثر في الاستعلامات المتقدّمة.

📚 مصادر رسمية للتعمّق: توثيق SQL في PostgreSQL

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

ما معنى UPSERT بالضبط؟

كلمة مركّبة من Update وInsert: أمر واحد يحاول إدراج صف جديد، وإذا اصطدم بقيد فريد (Unique أو Primary Key) موجود مسبقًا يحدّث الصف الموجود بدل أن يفشل بخطأ. الهدف تفادي شرط السباق (Race Condition) الذي يحدث لو نفّذت SELECT للتحقق ثم INSERT كخطوتين منفصلتين.

لماذا لا أكتفي بـ SELECT ثم INSERT أو UPDATE في كود التطبيق؟

لأن بين تنفيذ SELECT وتنفيذ INSERT قد يُدرج طلب آخر متزامن نفس الصف، فيفشل طلبك بخطأ تكرار مفتاح رغم أن التحقق بدا سليمًا لحظة تنفيذه. UPSERT يحل هذا لأنه أمر واحد تُقيّمه قاعدة البيانات ذرّيًا.

هل تحتاج فهرسًا فريدًا لكي يعمل UPSERT؟

نعم في الحالتين. في PostgreSQL عمود ON CONFLICT يجب أن يكون عليه قيد UNIQUE أو مفتاح أساسي، وإلا يرفض الأمر تحديد هدف التعارض. في MySQL لا تحدّد عمودًا صراحة، لكن الشرط نفسه ضمنيًا: يجب وجود مفتاح أساسي أو فهرس فريد على الجدول ليكون هناك تكرار أصلًا يفعّل التحديث.

اقرأ أيضًا

تصفّح كل المقالات في المدوّنة، أو ابدأ التعلّم من المسارات و خرائط الطريق.