من الأنماط المتكرّرة جدًا في تطوير الويب: إدراج صف إن لم يكن موجودًا، أو تحديثه إن كان موجودًا — عدّاد زيارات صفحة، آخر وقت دخول مستخدم، أو مزامنة بيانات من مصدر خارجي. الحل الساذج بخطوتين منفصلتين (تحقّق ثم إدراج) يبدو منطقيًا لكنه مليء بفخّ خفي. هذا شرح عملي للفخّ، ولحلّه في 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;
جدول المقارنة
| المعيار | PostgreSQL | MySQL |
|---|---|---|
| الجملة | ON CONFLICT (col) DO UPDATE | ON DUPLICATE KEY UPDATE |
| تحديد عمود التعارض | إلزامي مع DO UPDATE | غير مطلوب — تلقائي من أي قيد فريد |
| الإشارة للقيمة الجديدة | EXCLUDED.column | new.column (أو VALUES() القديمة والمهملة) |
| تجاهل التكرار بلا تحديث | ON CONFLICT ... DO NOTHING | INSERT IGNORE (أمر منفصل بالكامل) |
| تحديث مشروط | WHERE بعد SET | غير مدعوم بنفس الصياغة المباشرة |
أيّهما تختار؟
لا يوجد اختيار فعلي هنا — الصياغة مرتبطة بقاعدة البيانات التي تستخدمها فعلًا في مشروعك، لا بتفضيل شخصي. الفكرة الأهم أن UPSERT مفهوم واحد بصياغتين: كلاهما يحل نفس المشكلة (إدراج أو تحديث ذرّي دون شرط سباق)، لكن كل قاعدة بيانات تعبّر عنه بقواعدها الخاصة. إذا كنت تكتب كودًا يجب أن يعمل على الاثنين (مكتبة ORM مثلًا)، تحقّق من أن الأداة التي تستخدمها تولّد الصياغة الصحيحة لقاعدة البيانات المستهدفة تلقائيًا.
أكمل مسار SQL الكامل بالعربي لتتعمّق أكثر في الاستعلامات المتقدّمة.