PostgreSQL: عمليات INSERT و UPDATE و DELETE في PostgreSQL
آخر تحديث: 2026-08-26
تعديل البيانات (DML) هو العملية اليومية الأكثر تكرارًا — بالإضافة إلى SQL القياسي، تضيف PostgreSQL ميزتين مميزتين: RETURNING و UPSERT.
1. ما ستتعلمه
- INSERT INTO: صف واحد / صفوف متعددة / إدراج م�� استعلام
- جملة RETURNING (ميزة PG: تعيد الصفوف المتأثرة)
- UPDATE / DELETE مع RETURNING
- UPSERT: INSERT ON CONFLICT (ميزة PG)
- TRUNCATE TABLE لتفريغ جدول
- DML داخل معاملة (BEGIN / COMMIT / ROLLBACK)
2. قصة حقيقية لشخص عمليات
(1) المشكلة: بيانات مكررة عند استيراد المنتجات بالجملة
يحتاج Charlie إلى استيراد 5000 سجل منتج بالجملة إلى قاعدة بيانات التجارة الإلكترونية، لكن بعض المنتجات موجودة بالفعل (محددة بـ name). يحتاج إلى:
- للمنتجات الموجودة: تحديث السعر والمخزون (دون إثارة خطأ)
- للمنتجات الجديدة: إدراج بشكل طبيعي
- بعد الاستيراد: معرفة فورية للصفوف التي تم▶�دراجها حديثًا والتي تم تحديثها
(2) الحل: UPSERT + RETURNING
تتعامل INSERT ON CONFLICT (UPSERT) في PostgreSQL مع كل من الإدراج والتحديث في عبارة SQL واحدة، وتعيد جملة RETURNING الصفوف المتأثرة:
-- Upsert: إدراج منتجات جديدة، تحديث الموجودة
INSERT INTO products (name, price, stock, category)
VALUES ('Running Shoes', 99.99, 200, 'Footwear')
ON CONFLICT (name)
DO UPDATE SET
price = EXCLUDED.price,
stock = products.stock + EXCLUDED.stock
RETURNING id, name, price, stock,
CASE WHEN xmax = 0 THEN 'inserted' ELSE 'updated' END AS operation;
(3) النتيجة
- لا حاجة لـ SELECT أولاً لتحديد INSERT أو UPDATE (استعلام أقل)
- لا مشكلة تزامن (عملية ذرية)
- RETURNING تعيد النتيجة فورًا، لا حاجة لاستعلام ثانٍ
- تحسنت كفاءة الاستيراد بالجملة بأكثر من 10 أضعاف
3. INSERT
(1) الصيغة الأساسية
INSERT INTO table_name (column1, column2, ...)
VALUES (value1, value2, ...)
RETURNING * | column_list;
(1) ▶ مثال
-- إدراج صف واحد
INSERT INTO users (email, name, password_hash)
VALUES ('charlie@example.com', 'Charlie', '$2a$12$hash123');
-- إدراج مع RETURNING (الحصول على id المولد تلقائيًا)
INSERT INTO users (email, name, password_hash)
VALUES ('diana@example.com', 'Diana', '$2a$12$hash456')
RETURNING id, email, created_at;
Output:
id | email | created_at
----+----------------------+-------------------------------
5 | diana@example.com | 2026-07-13 10:30:00+00
(2) ▶ مثال
-- إدراج صفوف متعددة في عبارة واحدة (أسرع من INSERTs متعددة)
INSERT INTO products (name, price, stock, category) VALUES
('Wireless Mouse', 29.99, 500, 'Accessories'),
('USB-C Cable', 12.99, 1000, 'Accessories'),
('Mechanical Keyboard', 79.99, 200, 'Input Devices'),
('Monitor Stand', 49.99, 150, 'Accessories'),
('Webcam HD', 59.99, 300, 'Video');
Output:
INSERT 0 1
(3) ▶ مثال
-- إنشاء جدول أرشيف ونسخ البيانات من الأصل
CREATE TABLE orders_archive (LIKE orders INCLUDING ALL);
-- إدراج صفوف من استعلام SELECT
INSERT INTO orders_archive
SELECT * FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '90 days';
Output:
INSERT 0 1
4. جملة RETURNING (ميزة PG)
RETURNING هي ميزة خاصة بـ PostgreSQL — بعد INSERT / UPDATE / DELETE تعيد الصفوف المتأثرة مباشرة، دون حاجة لـ SELECT إضافي.
(1) RETURNING عبر العمليات
| العملية | الصيغة | تعيد |
|---|---|---|
| INSERT | INSERT ... RETURNING * |
الصف المدرج حديثًا |
| UPDATE | UPDATE ... RETURNING * |
الصف المحدث |
| DELETE | DELETE ... RETURNING * |
الصف المحذوف |
| المقارنة | MySQL | PostgreSQL |
|---|---|---|
| الحصول على المعرف بعد INSERT | SELECT LAST_INSERT_ID() |
INSERT ... RETURNING id |
| الحصول على الصفوف▶�لمحذوفة | SELECT أولاً، ثم DELETE | DELETE ... RETURNING * |
| الحصول على القيم بعد UPDATE | UPDATE أولاً، ثم SELECT | UPDATE ... RETURNING * |
(4) ▶ مثال
-- INSERT: الحصول على القيم المولدة تلقائيًا فورًا
INSERT INTO users (email, name, password_hash)
VALUES ('eve@example.com', 'Eve', '$2a$12$hash789')
RETURNING id, email, created_at;
-- UPDATE: رؤية القيم قبل وبعد
UPDATE products
SET price = 69.99, stock = stock - 10
WHERE id = 1
RETURNING id, name,
price AS new_price,
stock AS new_stock;
-- DELETE: أرشفة قبل الحذف
WITH deleted AS (
DELETE FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '1 year'
RETURNING *
)
INSERT INTO orders_archive SELECT * FROM deleted;
Output:
INSERT 0 1
5. UPDATE
(1) الصيغة الأساسية
UPDATE table_name
SET column1 = value1, column2 = value2, ...
[WHERE condition]
[RETURNING * | column_list];
(5) ▶ مثال
-- تحديث صف واحد
UPDATE users
SET name = 'Alice Smith', updated_at = NOW()
WHERE email = 'alice@example.com';
-- تحديث بشرط
UPDATE products
SET price = price * 0.9 -- خصم 10%
WHERE category = 'Accessories' AND stock > 100;
-- تحديث مع RETURNING
UPDATE products
SET stock = stock - 1
WHERE id = 1 AND stock > 0
RETURNING id, name, stock;
Output:
-- SQL statement executed successfully
(6) ▶ مثال
-- تحديث إجمالي الطلب بناءً على عناصر الطلب
UPDATE orders o
SET total_amount = (
SELECT SUM(quantity * unit_price)
FROM order_items oi
WHERE oi.order_id = o.id
)
WHERE o.status = 'pending';
-- تحديث باستخدام جملة FROM (امتداد PG)
UPDATE orders o
SET total_amount = oi_sum.total
FROM (
SELECT order_id, SUM(quantity * unit_price) AS total
FROM order_items
GROUP BY order_id
) oi_sum
WHERE o.id = oi_sum.order_id
AND o.status = 'pending';
Output:
result
----------
42.50
(1 row)
6. DELETE
(1) الصيغة الأساسية
DELETE FROM table_name
[WHERE condition]
[RETURNING * | column_list];
(7) ▶ مثال
-- حذف صفوف محددة
DELETE FROM products
WHERE stock = 0 AND is_available = false
RETURNING id, name;
-- حذف مع استعلام فرعي
DELETE FROM orders
WHERE user_id IN (
SELECT id FROM users WHERE is_active = false
);
-- حذف جميع الصفوف (استخدم TRUNCATE بدلاً من ذلك للجداول الكبيرة!)
-- DELETE FROM logs; -- بطيء، يولد WAL لكل صف
Output:
DELETE 2
(2) DELETE مقابل TRUNCATE
| البعد | DELETE | TRUNCATE |
|---|---|---|
| السرعة | يحذف صفًا بصف، بطيء | يفرغ دفعة واحدة، سريع جدًا |
| المعاملة | يمكن التراجع عنه (داخل معاملة) | يمكن أيضًا التراجع عنه (داخل معاملة) |
| المشغلات | يشغل مشغلات مستوى الصف | لا يشغل مشغلات مستوى الصف |
| إعادة تعيين التسلسل | لا يعيد التعيين | يمكن إعادة التعيين (RESTART IDENTITY) |
| WHERE | مدعوم | غير مدعوم (يفرغ الجدول بأكمله) |
| حالة الاستخدام | حذف بعض الصفوف | تفريغ الجدول بأكمله |
(8) ▶ مثال
-- اقتطاع جدول واحد (سريع، يعيد تعيين التخزين)
TRUNCATE TABLE logs;
-- اقتطاع جداول متعددة دفعة واحدة
TRUNCATE TABLE order_items, orders;
-- تتالي: اقتطاع أيضًا الجداول ذات مراجع المفاتيح الخارجية
TRUNCATE TABLE users CASCADE;
-- إعادة تعيين تسلسلات الزيادة التلقائية
TRUNCATE TABLE products RESTART IDENTITY;
Output:
-- SQL statement executed successfully
7. UPSERT (INSERT ON CONFLICT)
UPSERT هي واحدة من أكثر ميزات PostgreSQL العملية تميزًا — عندما تتعارض البيانات المدرجة مع صف موجود، تقوم تلقائيًا بتحديث بدلاً من إثارة خطأ.
(1) مخطط تدفق UPSERT
graph TB
START[إدراج صف] --> CONFLICT{قيد فريد<br/>تعارض؟}
CONFLICT -->|لا تعارض| INSERT[إدراج صف جديد<br/>RETURN المدرج]
CONFLICT -->|تعارض!| ACTION{إجراء ON CONFLICT<br/>؟}
ACTION -->|DO NOTHING| SKIP[تخطي هذا الصف<br/>لا خطأ، لا تحديث]
ACTION -->|DO UPDATE| UPDATE[تحديث الصف الموجود<br/>باستخدام EXCLUDED]
UPDATE --> RET2[RETURN الصف المحدث]
(2) الصيغة
INSERT INTO table_name (column_list)
VALUES (value_list)
ON CONFLICT (column_name | constraint_name)
DO NOTHING | DO UPDATE SET column = EXCLUDED.column ...
[RETURNING *];
| الكلمة المفتاحية | الوصف |
|---|---|
ON CONFLICT (column) |
العمود المراد كشف التعارض عليه (يجب أن يكون له قيد UNIQUE أو مفتاح أساسي) |
ON CONFLICT ON CONSTRAINT name |
تحديد اسم قيد محدد |
DO NOTHING |
تخطي الصف عند التعارض (لا خطأ، لا تحديث) |
DO UPDATE SET ... |
إجراء تحديث عند التعارض |
EXCLUDED |
جدول افتراضي يحمل القيم التي كان من المقرر إدراجها أصلاً |
(9) ▶ مثال
-- إدراج فقط إذا لم يكن البريد الإلكتروني موجودًا بالفعل
INSERT INTO users (email, name, password_hash)
VALUES ('alice@example.com', 'Alice', '$2a$12$newhash')
ON CONFLICT (email) DO NOTHING;
-- لا خطأ إذا كان البريد الإلكتروني موجودًا بالفعل
Output:
INSERT 0 1
(10) ▶ مثال
-- Upsert: إدراج منتجات جديدة، تحديث السعر/المخزون للمنتجات الموجودة
INSERT INTO products (name, price, stock, category)
VALUES
('Running Shoes', 99.99, 200, 'Footwear'),
('USB-C Cable', 14.99, 800, 'Accessories'),
('New Product', 39.99, 50, 'Gadgets')
ON CONFLICT (name)
DO UPDATE SET
price = EXCLUDED.price,
stock = products.stock + EXCLUDED.stock,
category = EXCLUDED.category
RETURNING id, name, price, stock;
Output:
INSERT 0 1
(11) ▶ مثال
-- تحديث فقط إذا كان السعر الجديد أقل
INSERT INTO products (name, price, stock, category)
VALUES ('Running Shoes', 79.99, 100, 'Footwear')
ON CONFLICT (name)
DO UPDATE SET
price = EXCLUDED.price,
stock = EXCLUDED.stock
WHERE EXCLUDED.price < products.price;
-- يحدث فقط إذا كان السعر الجديد أرخص من السعر الحالي
Output:
INSERT 0 1
8. DML داخل معاملة
(1) عمليات المعاملات الأساسية
| الأمر | الوصف |
|---|---|
BEGIN أو START TRANSACTION |
بدء معاملة |
COMMIT |
إيداع المعاملة (حفظ دائم) |
ROLLBACK |
التراجع عن المعاملة (إلغاء جميع التغييرات) |
SAVEPOINT name |
إنشاء نقطة حفظ |
ROLLBACK TO SAVEPOINT name |
التراجع إلى نقطة حفظ |
(12) ▶ مثال
-- بدء معاملة للاستيراد الدفعي
BEGIN;
-- إدراج طلب وعناصره كوحدة واحدة
INSERT INTO orders (user_id, total_amount, status)
VALUES (1, 0, 'pending')
RETURNING id;
-- افترض أن ما سبق أعاد id = 10
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES
(10, 1, 2, 89.99),
(10, 2, 5, 12.99);
-- تحديث إجمالي الطلب
UPDATE orders
SET total_amount = (2 * 89.99 + 5 * 12.99)
WHERE id = 10;
-- تحقق قبل الإيداع
SELECT * FROM orders WHERE id = 10;
SELECT * FROM order_items WHERE order_id = 10;
-- كل شيء جيد؟ إيداع
COMMIT;
-- شيء خاطئ؟ تراجع عن كل شيء
-- ROLLBACK;
Output:
result
----------
42.50
(1 row)
9. مثال كامل: استيراد المنتجات بالجملة
-- ============================================
-- مثال كامل: استيراد المنتجات بالجملة مع UPSERT
-- Charlie يستورد 5000 منتج، بعضها موجود بالفعل
-- ============================================
-- الخطوة 1: إنشاء جدول استيراد مؤقت
CREATE TEMP TABLE import_products (
name VARCHAR(200) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock INTEGER DEFAULT 0,
category VARCHAR(50)
);
-- الخطوة 2: تحميل بيانات الاستيراد (محاكاة بصفوف عينة)
INSERT INTO import_products (name, price, stock, category) VALUES
('Running Shoes', 99.99, 200, 'Footwear'), -- موجود بالفعل
('Laptop Backpack', 59.99, 300, 'Accessories'), -- موجود بالفعل، تغير السعر
('Smart Water Bottle', 34.99, 500, 'Gadgets'), -- منتج جديد
('Wireless Earbuds', 79.99, 400, 'Audio'), -- منتج جديد
('Yoga Mat', 24.99, 600, 'Fitness'); -- منتج جديد
-- الخطوة 3: Upsert جميع بيانات الاستيراد في عبارة واحدة
BEGIN;
INSERT INTO products (name, price, stock, category)
SELECT name, price, stock, category
FROM import_products
ON CONFLICT (name)
DO UPDATE SET
price = EXCLUDED.price,
stock = products.stock + EXCLUDED.stock
RETURNING id, name, price, stock;
-- الخطوة 4: التحقق من النتائج
SELECT id, name, price, stock
FROM products
WHERE name IN ('Running Shoes', 'Laptop Backpack',
'Smart Water Bottle', 'Wireless Earbuds', 'Yoga Mat')
ORDER BY name;
-- الخطوة 5: التنظيف
DROP TABLE import_products;
COMMIT;
❓ أسئلة شائعة
EXCLUDED.price هو السعر الجديد و products.price هو السعر القديم.sql_safe_updates = on (شغّله في psql).WITH moved AS (DELETE FROM active WHERE ... RETURNING *) INSERT INTO archive SELECT * FROM moved.📖 ملخص
- INSERT يدعم صف واحد، صفوف متعددة، وإدراج من استعلام؛ الإدراج متعدد الصفوف أكثر كفاءة بكثير من الإدراج صفًا بصف
- جملة RETURNING (ميزة PG) تسمح لـ INSERT/UPDATE/DELETE بإعادة الصفوف المتأثرة مباشرة، دون استعلام ثانٍ
- UPDATE يدعم التحديثات المبنية على JOIN (جملة FROM)، أوضح من الاستعلام الفرعي
- DELETE يزيل الصفوف واحدًا تلو الآخر (بطيء)؛ TRUNCATE يفرغ دفعة واحدة (سريع)؛ يمكن التراجع عن TRUNCATE داخل معاملة
- UPSERT (INSERT ON CONFLICT) هي ميزة PG أساسية: عبارة SQL واحدة تتعامل مع "إدراج إذا لم يكن موجودًا، تحديث إذا كان موجودًا"
- الجدول الافتراضي EXCLUDED يشير إلى القيم المدرجة حديثًا، مميزًا إياها عن القيم الموجودة في الجدول
- المعاملات (BEGIN/COMMIT/ROLLBACK) تحمي العمليات الدفعية؛ عند الخطأ يمكنك التراجع
📝 تمارين
-
أساسي (★): أنشئ جدول
tags(id SERIAL مفتاح أساسي، name VARCHAR(50) UNIQUE)، أدرج 5 علامات، واستخدم RETURNING لإعادة id و name لكل صف مدرج. -
متوسط (★★): نفذ UPSERT على جدول
productsمن هذا الدرس — أدرج 3 صفوف منتجات، أحدها له اسم موجود بالفعل. للمنتج الموجود، حدث السعر؛ للمنتجات الجديدة، أدرج بشكل طبيعي. استخدم RETURNING لعرض نتيجة العملية. -
تحدي (★★★): في معاملة واحدة، أكمل ما يلي: 1) أنشئ جدول
products_backup(نفس هيكلproducts)؛ 2) انقل المنتجات ذات السعر < 30 منproductsإلىproducts_backupباستخدام DELETE ... RETURNING + INSERT؛ 3) بعد التحقق من نتيجة الترحيل، COMMIT. اكتب سكربت SQL الك��مل.