---
title: 'Postgres MERGE RETURNING: القيم القديمة والجديدة (2026)'
description: Postgres 18 بيسمح لـ MERGE RETURNING إنها ترجع قيم الصفوف القديمة والجديدة في استعلام واحد. شوف merge_action()، وبديل xmax، وكود Node.js/TypeScript اللي تم التحقق منه.
url: https://nerdleveltech.com/ar/postgres-merge-returning-old-new-values
language: ar
date: '2026-07-02'
category: backend
tags:
  - postgresql
  - postgres 18
  - merge
  - upsert
  - sql
  - node-postgres
  - typescript
  - backend
markdown_url: https://nerdleveltech.com/ar/postgres-merge-returning-old-new-values.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/postgres-merge-returning-old-new-values'
terms: https://nerdleveltech.com/llms.txt
---

# Postgres MERGE RETURNING: القيم القديمة والجديدة (2026)

حصلت MERGE في Postgres على بند RETURNING في الإصدار 17، وأضاف الإصدار 18 أسماء مستعارة خاصة OLD و NEW تعرض قيم الصف السابقة والحالية في استعلام واحد — مما يستبدل الحيلة غير الموثقة xmax = 0 لتحديد ما إذا كان الصف قد أُدخل أو تم تحديثه.[^mergedoc][^release18-4]

## ملخص

`MERGE ... RETURNING` موجود منذ Postgres 17. الجديد في **Postgres 18** (الصادر في 25 سبتمبر 2025، الإصدار الفرعي الحالي 18.4) هو القدرة على كتابة `OLD.column` و `NEW.column` — أو `OLD.*` / `NEW.*` — داخل بند RETURNING ذلك، لـ `INSERT`, `UPDATE`, `DELETE`, *و* `MERGE`.[^versioning][^release18-4] مع `merge_action()`، التي تُبلغ `'INSERT'`, `'UPDATE'`, أو `'DELETE'` لكل صف متأثر، يمكنك بناء سجل مراجعة كامل للUpsert — القيمة القديمة، القيمة الجديدة، والإجراء الذي تم — في بيان واحد، بدون تفعيلات ولا استعلام `SELECT` لاحق.[^mergedoc] تم تشغيل كل استعلام ورسالة خطأ في هذه المقالة على نسخة حية من PostgreSQL 18.4 مثبتة محليًا بدلاً من كتابتها من الذاكرة.

## ما ستتعلمه

- الفرق الحقيقي بين `MERGE` و `INSERT ... ON CONFLICT` (upsert)
- منذ أي إصدار من Postgres يدعم `MERGE` `RETURNING` على الإطلاق
- كيف تعرف ما إذا أجرت `MERGE` إدخالًا أو تحديثًا باستخدام `merge_action()`
- كيف تعيد القيم القديمة والجديدة من بيان `MERGE` أو upsert واحد
- ما إذا كنت لا تزال بحاجة إلى حيلة `xmax = 0` في Postgres 18
- كيف تبني سجل مراجعة بدون تفعيلات من بيان `MERGE` واحد
- ما الخطأ الذي ترفعه Postgres إذا كانت بيانات المصدر تحتوي على مفاتيح مكررة
- كيف تستهلك أعمدة `OLD`/`NEW` RETURNING من Node.js باستخدام `pg` و TypeScript

## ما الفرق بين MERGE في Postgres و INSERT ... ON CONFLICT (upsert)؟

`INSERT ... ON CONFLICT DO UPDATE` (upsert الكلاسيكي) يبدأ دائمًا من `INSERT`: إذا تم انتهاك قيد فريد، فإنه يعود إلى `UPDATE`. `MERGE` أكثر عمومية — فهو يدمج جدول الهدف مع المصدر باستخدام شرط `ON` عشوائي ويسمح لك بالفرع إلى `WHEN MATCHED`, `WHEN NOT MATCHED`, و (منذ Postgres 17) `WHEN NOT MATCHED BY SOURCE`, لذا يمكنه الإدخال والتحديث *و* الحذف في بيان واحد.[^mergedoc] تم إضافة `MERGE` في Postgres 15 (الصادر 13 أكتوبر 2022)؛ `INSERT ... ON CONFLICT` موجود منذ Postgres 9.5.[^pg15]

```sql
-- Classic upsert: always an INSERT, falls back to UPDATE on conflict
INSERT INTO webhook (id, data)
VALUES (1, 'payload')
ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data;

-- MERGE: an explicit join with independent branches per outcome
MERGE INTO products p
USING staging s ON p.product_code = s.product_code
WHEN MATCHED THEN
  UPDATE SET product_name = s.product_name, price = s.price, stock_quantity = s.stock_quantity
WHEN NOT MATCHED THEN
  INSERT (product_code, product_name, price, stock_quantity)
  VALUES (s.product_code, s.product_name, s.price, s.stock_quantity);
```

إذا كنت تزامن جدولًا كاملاً من المؤقت مع جدول الهدف — حالة ETL/مزامنة البيانات الكلاسيكية — فإن `MERGE` عادةً ما يكون الخيار الأفضل، لأن شرط الانضمام يمكنه المطابقة على أي شيء، وليس فقط قيد فريد.

## هل يدعم MERGE في Postgres بند RETURNING؟

نعم، منذ Postgres 17. `RETURNING` على `MERGE` يحسب مخرجاته من صف المصدر بالإضافة إلى صف الهدف الذي أُدخل أو تم تحديثه أو حُذف، ويقبل دالة `merge_action()` الخاصة لتقرير أي إجراء تم تشغيله.[^mergedoc] في Postgres 15 أو 16، يعمل `MERGE` بشكل صحيح لكن `RETURNING` غير مقبول — ستحصل على خطأ في التركيب وتحتاج إلى استعلام `SELECT` منفصل لرؤية النتيجة.

```sql
MERGE INTO products p
USING staging s ON p.product_code = s.product_code
WHEN MATCHED THEN
  UPDATE SET product_name = s.product_name, price = s.price, stock_quantity = s.stock_quantity
WHEN NOT MATCHED THEN
  INSERT (product_code, product_name, price, stock_quantity)
  VALUES (s.product_code, s.product_name, s.price, s.stock_quantity)
RETURNING merge_action() AS action, p.product_code, p.price;
```

تم تشغيله على نسخة حية من PostgreSQL 18.4 تحتوي على صف واحد موجود (`LAPTOP-001`) وسطوين في الجدول المؤقت (تحديث لـ `LAPTOP-001` و `MONITOR-001` جديد تمامًا)، يُعيد:

```text
 action | product_code | price
--------+--------------+---------
 UPDATE | LAPTOP-001   | 1099.99
 INSERT | MONITOR-001  |  399.99
```

## كيف أعرف ما إذا أجرت MERGE إدخالًا أو تحديثًا؟

أضف `merge_action()` إلى قائمة `RETURNING`. إنها دالة خاصة، صالحة فقط داخل بند `MERGE ... RETURNING`, وتُقيّم إلى السلسلة النصية `'INSERT'`, `'UPDATE'`, أو `'DELETE'` بناءً على الفرع الذي تم تشغيله لهذا الصف.[^mergedoc] كانت متاحة منذ نفس إصدار Postgres 17 الذي أضاف `RETURNING` إلى `MERGE` — لا تحتاج إلى Postgres 18 لمعرفة أي إجراء تم تشغيله؛ تحتاج إلى 18 لرؤية القيم القديمة والجديدة *بجانب* ذلك (القسم التالي).

```sql
RETURNING merge_action() AS action, p.product_code;
```

النتيجة المُختبرة لنفس التشغيل أعلاه: `LAPTOP-001` تُبلغ `action = 'UPDATE'`, `MONITOR-001` تُبلغ `action = 'INSERT'` — مطابقة تمامًا للفرع الذي تم تشغيله لكل صف في بند `WHEN MATCHED` / `WHEN NOT MATCHED`.

## كيف أحصل على القيم القديمة والجديدة من استعلام upsert أو MERGE واحد؟

استخدم أسماء المستعارة `OLD` و `NEW` داخل `RETURNING`, المضافة في Postgres 18. `OLD.column` (أو `OLD.*`) تعيد قيمة الصف *قبل* تشغيل البيان؛ `NEW.column` (أو `NEW.*`) تعيدها *بعد*. يعمل هذا أيضًا لـ `UPDATE`/`DELETE`/`INSERT` العادية، ليس فقط `MERGE` — ويعمل لـ upserts الكلاسيكية `INSERT ... ON CONFLICT` أيضًا.[^release18-4]

```sql
MERGE INTO products p
USING staging s ON p.product_code = s.product_code
WHEN MATCHED THEN
  UPDATE SET product_name = s.product_name, price = s.price, stock_quantity = s.stock_quantity
WHEN NOT MATCHED THEN
  INSERT (product_code, product_name, price, stock_quantity)
  VALUES (s.product_code, s.product_name, s.price, s.stock_quantity)
RETURNING
  merge_action()                            AS action,
  COALESCE(new.product_code, old.product_code) AS product_code,
  old.price                                 AS old_price,
  new.price                                 AS new_price,
  (old.price IS DISTINCT FROM new.price)    AS price_changed;
```

النتيجة المُتحقق منها:

```text
 action | product_code | old_price | new_price | price_changed
--------+--------------+-----------+-----------+---------------
 UPDATE | LAPTOP-001   |    999.99 |   1099.99 | t
 INSERT | MONITOR-001  |           |    399.99 | t
```

لاحظ أن `MONITOR-001` — صف جديد تمامًا — لديه `old_price` فارغ (`NULL`)، لأنه لم تكن هناك حالة "قبل"، لكن `price_changed` يعود `t` (صحيح)، وليس `NULL`. ذلك لأن `IS DISTINCT FROM` آمن للقيم NULL: على عكس مشغل `=` العادي، الذي يعيد `NULL` (لا صحيح ولا خطأ) عندما يكون أحد الجانبين `NULL`, `IS DISTINCT FROM` يعيد دائمًا قيمة بوليان حقيقية ويعامل `NULL` كمختلف عن أي قيمة غير `NULL`. لذا `NULL IS DISTINCT FROM 399.99` يُقيّم بشكل صحيح إلى `true` — تحصل على إشارة "تم تغيير هذا الصف" واضحة للإدخالات أيضًا، دون الحاجة إلى `COALESCE` أو فروع إضافية.

إذا تصادمت معرفاتك مع `old`/`new` (أو كنت داخل دالة تفعيل حيث تم أخذ هذه الأسماء بالفعل)، أعد تسمية الأسماء المستعارة:

```sql
RETURNING WITH (OLD AS o, NEW AS n) o.price AS old_price, n.price AS new_price;
```

تم اختبارها بنفس الطريقة مثل الأسماء المستعارة الافتراضية — هذا مجرد راحة في التسمية.

## هل ما زلت بحاجة إلى حيلة xmax للكشف عن صف جديد في Postgres 18؟

لا، لكنها لا تزال تعمل إذا كنت عالقاً في إصدار قديم. قبل Postgres 18، كانت الطريقة الشائعة لمعرفة ما إذا كان الصف الذي تم عمل upsert له جديداً هي التحقق من عمود النظام الداخلي `xmax`: `(xmax = 0) AS is_new`، بما أن الصف المدرج حديثاً لا يحتوي على قفل صف وبالتالي يكون `xmax = 0`.[^mergedoc] هذا يعمل، ولكنه يعتمد على عمود نظام داخلي تقول وثائق PostgreSQL نفسها أن التطبيقات "لا تحتاج حقاً إلى الاهتمام به" — فهو متاح لعمليات تدوين MVCC الداخلية، وليس موثقاً كـ API مستقر لاكتشاف الصفوف الجديدة.[^syscols]

تم تشغيل كليهما بالتتابع مقابل نفس قاعدة البيانات الحية لهذا المقال، على نفس الجدول، للتأكد من تطابقهما:

```sql
-- Old way (works on Postgres 9.5+, still works on 18)
INSERT INTO webhook (id, data) VALUES (3, 'xmax-test')
ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data
RETURNING id, (xmax = 0) AS is_new_xmax;
-- -> is_new_xmax: true (fresh row)

-- New way (Postgres 18+, no internal-column dependency)
INSERT INTO webhook (id, data) VALUES (2, 'brand-new')
ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data
RETURNING id, old.data AS old_data, (old IS NULL) AS is_new;
-- -> old_data: null, is_new: true
```

`(old IS NULL) AS is_new` هو بديل Postgres 18: مقروء، موثق، ولا يعتمد على كيفية تنفيذ قفل الصفوف داخلياً.

## كيف يمكنني بناء سجل تدقيق من upsert بدون triggers؟

قم بتغليف `MERGE ... RETURNING` في CTE وقم بتغذية مخرجاته مباشرة في `INSERT` على جدول التدقيق الخاص بك — لا حاجة لـ trigger، ويظل داخل نفس المعاملة (transaction) مثل الكتابة الأصلية.

```sql
CREATE TABLE product_audit (
  audit_id    SERIAL PRIMARY KEY,
  product_code VARCHAR(50),
  action      VARCHAR(10),
  old_values  JSONB,
  new_values  JSONB,
  changed_at  TIMESTAMPTZ DEFAULT now()
);

WITH merge_results AS (
  MERGE INTO products p
  USING staging s ON p.product_code = s.product_code
  WHEN MATCHED THEN
    UPDATE SET product_name = s.product_name, price = s.price, stock_quantity = s.stock_quantity
  WHEN NOT MATCHED THEN
    INSERT (product_code, product_name, price, stock_quantity)
    VALUES (s.product_code, s.product_name, s.price, s.stock_quantity)
  RETURNING
    merge_action() AS action,
    COALESCE(new.product_code, old.product_code) AS product_code,
    CASE WHEN old.product_code IS NOT NULL
      THEN jsonb_build_object('name', old.product_name, 'price', old.price, 'stock', old.stock_quantity)
    END AS old_values,
    CASE WHEN new.product_code IS NOT NULL
      THEN jsonb_build_object('name', new.product_name, 'price', new.price, 'stock', new.stock_quantity)
    END AS new_values
)
INSERT INTO product_audit (product_code, action, old_values, new_values)
SELECT product_code, action, old_values, new_values FROM merge_results
RETURNING product_code, action, old_values, new_values;
```

النتيجة المؤكدة لنفس مزامنة الصفين المستخدمة في هذا المقال:

```json
[
  {
    "product_code": "LAPTOP-001",
    "action": "UPDATE",
    "old_values": { "name": "Premium Laptop", "price": 999.99, "stock": 50 },
    "new_values": { "name": "Premium Laptop Pro", "price": 1099.99, "stock": 45 }
  },
  {
    : "MONITOR-001",
    "action": "INSERT",
    "old_values": null,
    "new_values": { "name": "4K Monitor", "price": 399.99, "stock": 75 }
  }
]
```

يُظهر `MONITOR-001` بشكل صحيح `action: "INSERT"` مع `old_values: null` — ومن الجدير بالذكر صراحةً، أن مثالاً مشابهاً منشوراً على مدونة أحد مزودي Postgres يخطئ في هذه الحالة تحديداً، حيث يُظهر `"UPDATE"` لصف تثبت بيانات الإعداد الخاصة به أنه تم إدراجه حديثاً.[^pgedge] قم بحماية استدعاءات `jsonb_build_object` باستخدام فحص `CASE WHEN ... IS NOT NULL` كما هو موضح أعلاه؛ لأن استدعاء `jsonb_build_object` دون قيد أو شرط على أعمدة مصدر `NULL` لا يزال ينتج كائن JSON بقيم `null` بداخله بدلاً من SQL `NULL` نظيف.

## أي إصدار من Postgres أحتاجه فعلياً لهذا؟

هناك ثلاث بوابات ميزات منفصلة، والخلط بينها هو المصدر الأكثر شيوعاً لارتباك "لماذا لا يعمل هذا":

| الميزة                                                               | الحد الأدنى للإصدار | تاريخ الإصدار               |
| -------------------------------------------------------------------- | ------------------- | --------------------------- |
| جملة `MERGE` نفسها                                                   | Postgres 15         | 13 أكتوبر 2022[^pg15]       |
| بند `RETURNING` في `MERGE`                                           | Postgres 17         | 26 سبتمبر 2024[^versioning] |
| الأسماء المستعارة `OLD`/`NEW` في `RETURNING` (بما في ذلك في `MERGE`) | Postgres 18         | 25 سبتمبر 2025[^versioning] |

إذا كنت تستخدم Postgres 15 أو 16، فإن `MERGE` تعمل ولكن `RETURNING` لا تعمل — قم بتشغيل `SELECT` منفصل بعد ذلك إذا كنت بحاجة لرؤية النتائج. إذا كنت تستخدم Postgres 17، فإن `MERGE ... RETURNING` و `merge_action()` تعملان، لكن الأسماء المستعارة `OLD`/`NEW` غير موجودة بعد — ستعود إلى خدعة `xmax` أو `SELECT` يدوي قبل/بعد. كما أصلح Postgres 18.4 (الإصدار الفرعي الحالي وقت كتابة هذا المقال) خطأً خاصاً بـ `MERGE` حيث لم يتم الإبلاغ عن التحديثات المتزامنة تحت مستويات العزل `REPEATABLE READ` أو `SERIALIZABLE` بشكل صحيح كفشل تسلسل (serialization failures) — وهو أمر يستحق المعرفة إذا كنت تقوم بتشغيل `MERGE` تحت مستويات عزل صارمة.[^release18-4]

## ماذا يحدث إذا كانت بيانات المصدر تحتوي على مفاتيح مكررة تطابق نفس صف الهدف؟

يقوم Postgres بإصدار خطأ بدلاً من اختيار واحد بصمت. يضمن `MERGE` تأثر كل صف هدف مرة واحدة على الأكثر لكل جملة؛ إذا تطابق صفان أو أكثر من المصدر مع نفس صف الهدف، تفشل الجملة.

```sql
CREATE TABLE src (id INT, val TEXT);
INSERT INTO src VALUES (1, 'x'), (1, 'y'); -- two source rows, same id

MERGE INTO tgt t USING src s ON t.id = s.id
WHEN MATCHED THEN UPDATE SET val = s.val
WHEN NOT MATCHED THEN INSERT (id, val) VALUES (s.id, s.val);
```

الخطأ المختبر، حرفياً من نسخة Postgres 18.4 حية:

```text
ERROR:  MERGE command cannot affect row a second time
```

قم بإزالة التكرار من المصدر (أو أضف شرطاً لفك الارتباط إلى شرط الانضمام) قبل تشغيل `MERGE`. لدى `INSERT ... ON CONFLICT DO UPDATE` نفس القيد وتصدر خطأً متطابقاً تقريباً — `ON CONFLICT DO UPDATE command cannot affect row a second time` — إذا تعارض صفان في نفس قائمة `VALUES` على نفس صف الهدف؛ تم تأكيد ذلك من خلال تشغيل الجملتين بالتتابع مقابل نسخة Postgres 18.4 حية. في المقابل، لا يصدر `ON CONFLICT DO NOTHING` خطأً عند التكرار — بل يطبق بصمت أول صف مطابق ويتخطى الباقي.

## كيف يمكنني قراءة هذا من Node.js و TypeScript باستخدام تعريف pg؟

قم بالاستعلام عنه مثل أي جملة أخرى باستخدام `pg` (يُبلغ `npm view pg` حالياً عن `8.22.0` كأحدث إصدار)، ولكن انتبه لمشكلتين لا تظهران في أمثلة SQL البحتة: أعمدة `NUMERIC` التي يتم إرجاعها مباشرة (غير مغلفة في JSON) تعود كـ **سلاسل نصية (strings)**، و `merge_action()` يحتاج إلى نوع سلسلة نصية عادي، وليس enum من Postgres.

```bash
npm install pg
npm install -D @types/pg TypeScript
```

```TypeScript
import { Client, type QueryResult } from 'pg';

interface MergeAuditRow {
  action: 'INSERT' | 'UPDATE' | 'DELETE';
  product_code: string;
  old_price: string | null; // NUMERIC comes back as a string from node-postgres
  new_price: string | null;
  price_changed: boolean; // IS DISTINCT FROM is null-safe — never SQL NULL
}

const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect();

const result: QueryResult<MergeAuditRow> = await client.query(`
  MERGE INTO products p
  USING staging s ON p.product_code = s.product_code
  WHEN MATCHED THEN
    UPDATE SET product_name = s.product_name, price = s.price, stock_quantity = s.stock_quantity
  WHEN NOT MATCHED THEN
    INSERT (product_code, product_name, price, stock_quantity)
    VALUES (s.product_code, s.product_name, s.price, s.stock_quantity)
  RETURNING
    merge_action() AS action,
    COALESCE(new.product_code, old.product_code) AS product_code,
    old.price AS old_price,
    new.price AS new_price,
    (old.price IS DISTINCT FROM new.price) AS price_changed;
`);

for (const row of result.rows) {
  const oldPrice = row.old_price ? Number(row.old_price) : null;
  const newPrice = row.new_price ? Number(row.new_price) : null;
  console.log(row.action, row.product_code, oldPrice, '->', newPrice);
}
```

تم التأكيد مباشرة مقابل استعلام حي: أعمدة `old_price`/`new_price` المختارة كأعمدة `NUMERIC` مجردة تصل كسلاسل JSON مثل `"999.99"`، وهو سلوك `node-postgres` القياسي لنوع `NUMERIC` (يتجنب فقدان دقة الفاصلة العائمة الصامت). إذا كنت تفضل تلقي أرقام JS حقيقية بدون تحويل `Number()` يدوي، فقم بتغليف القيم في `jsonb_build_object(...)` في SQL كما هو موضح في قسم سجل التدقيق أعلاه — أكد نفس الاختبار الحي أن القيم الرقمية المتداخلة داخل نتيجة `JSONB` يتم فك تشفيرها كأرقام JS أصلية (على سبيل المثال، `999.99`، وليس `"999.99"`)، لأن `pg` يحلل أعمدة `JSONB` باستخدام `JSON.parse` داخلياً بدلاً من تطبيق محلل السلسلة النصية الخام لـ `NUMERIC`.

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

**هل يدعم Postgres MERGE بند RETURNING؟** نعم، منذ Postgres 17. في الإصدارين 15 أو 16، يتم تشغيل `MERGE` ولكن يتم رفض `RETURNING`.[^mergedoc]

**كيف أعرف ما إذا كان MERGE قد أجرى INSERT أم UPDATE؟** أضف `merge_action()` إلى قائمة `RETURNING` — حيث يعيد السلسلة النصية الحرفية `'INSERT'`، أو `'UPDATE'`، أو `'DELETE'` لكل صف.[^mergedoc]

**كيف أحصل على القيم القديمة والجديدة معاً من upsert واحد أو MERGE؟** استخدم الأسماء المستعارة `OLD`/`NEW` التي تمت إضافتها في Postgres 18: `RETURNING old.col AS old_val, new.col AS new_val`. يعمل هذا على جمل `INSERT`/`UPDATE`/`DELETE` العادية أيضاً، وليس فقط `MERGE`.[^release18-4]

**هل لا زلت بحاجة لخدعة xmax = 0 في Postgres 18؟** لا — `(old IS NULL) AS is_new` تؤدي نفس المهمة دون الاعتماد على عمود داخلي غير موثق. لا تزال خدعة `xmax` تعمل إذا كنت تستخدم إصداراً أقدم.

**ما هو إصدار Postgres الذي أحتاجه؟** يحتاج `MERGE` إلى إصدار 15+، ويحتاج `MERGE ... RETURNING` إلى 17+، بينما تحتاج الأسماء المستعارة `OLD`/`NEW` في `RETURNING` إلى 18+.[^pg15][^versioning]

**ما الخطأ الذي يظهره Postgres إذا تطابق صفان من المصدر مع نفس الصف المستهدف في عملية MERGE؟** `ERROR: MERGE command cannot affect row a second time` — قم بإزالة التكرار من المصدر أو أضف شرطاً لفك الارتباط (tie-breaker) إلى شرط `ON`.

**هل يقوم node-postgres بإرجاع قيم NUMERIC كأرقام JavaScript؟** ليس بشكل افتراضي — يظهر عمود `NUMERIC` المجرد كسلسلة نصية (مثل `"999.99"`) لتجنب فقدان الدقة. أما القيم المتداخلة داخل نتيجة `JSONB` (عبر `jsonb_build_object`، على سبيل المثال) فيتم فك تشفيرها كأرقام JS حقيقية بدلاً من ذلك.

## ملخص

إن `MERGE ... RETURNING` (Postgres 17+) بالإضافة إلى الأسماء المستعارة `OLD`/`NEW` (Postgres 18+) تحل محل حلين مؤقتين استخدما طويلاً في آن واحد: حيلة `xmax = 0` لاكتشاف الصفوف الجديدة، واستعلام `SELECT` منفصل لالتقاط القيم قبل وبعد التعديل. عند وضعها داخل CTE، يمكن لنفس العبارة أيضاً كتابة سجل مراجعة (audit trail) كامل بدون أي triggers. اعرف إصدارك — 15 لـ `MERGE` نفسها، و17 لـ `RETURNING`، و18 لـ `OLD`/`NEW` — وتذكر، عند استخدام Node.js، أن أعمدة `NUMERIC` المجردة تصل كسلاسل نصية بينما لا يحدث ذلك للقيم المتداخلة في `JSONB`.

بالنسبة لجانب مستوى الصف في عمليات الكتابة متعددة المستأجرين التي غالباً ما تأتي بجانب هذا النوع من مهام المزامنة، راجع دليلنا حول [أمان مستوى الصف في Postgres لتطبيقات Node.js متعددة المستأجرين](https://nerdleveltech.com/postgres-row-level-security-multi-tenant-nodejs-tutorial). إذا كنت تقوم بنمذجة العلاقة بين منطقة التخزين المؤقت (staging) والهدف باستخدام ORM بدلاً من SQL الخام، فإن [استعلامات Drizzle 관계형 API](https://nerdleveltech.com/drizzle-orm-relational-queries-nested-with-tutorial) تغطي استعلامات `with` المتداخلة على نفس النوع من الجداول. وإذا كنت بحاجة إلى تتبع عمليات كتابة سجل المراجعة لمهمة مزامنة وصولاً إلى الطلب الذي تسبب فيها، فإن [معرفات الارتباط (correlation IDs) مع AsyncLocalStorage في Node.js](https://nerdleveltech.com/nodejs-correlation-id-asynclocalstorage-tutorial) توضح كيفية تمرير هذا السياق.

[^mergedoc]: PostgreSQL, "MERGE" command reference (v18) — RETURNING, merge\_action(), and xmax discussion. <https://www.postgresql.org/docs/current/sql-merge.html>

[^release18-4]: PostgreSQL, "E.1. Release 18.4" release notes (2026-05-14) — OLD/NEW RETURNING aliases and MERGE serialization-failure fix. <https://www.postgresql.org/docs/18/release-18-4.html>

[^versioning]: PostgreSQL, "Versioning Policy" — release dates and current minor versions. <https://www.postgresql.org/support/versioning/>

[^pg15]: PostgreSQL, "PostgreSQL 15 Released!" (October 13, 2022) — introduction of the MERGE command. <https://www.postgresql.org/about/news/postgresql-15-released-2526/>

[^syscols]: PostgreSQL, "5.6. System Columns" (v18) — xmax definition and "you do not really need to be concerned about these columns" guidance. <https://www.postgresql.org/docs/current/ddl-system-columns.html>

[^pgedge]: pgEdge, "PostgreSQL 18 RETURNING & MERGE RETURNING Explained" (January 6, 2026) — published audit-trail example. <https://www.pgedge.com/blog/postgresql-18-returning-enhancements-a-game-changer-for-modern-applications>
