PostgreSQL: المشغلات ومشغلات الأحداث في PostgreSQL
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- CREATE TRIGGER (BEFORE / AFTER / INSTEAD OF)
- مشغلات مستوى الصف مقابل مشغلات مستوى العبارة
- متغيرات السجلات NEW / OLD
- المشغلات الشرطية (عبارة WHEN)
- ترتيب تنفيذ المشغلات (أبجدي حسب الاسم)
- مشغلات INSTEAD OF (على طرق العرض)
- مشغلات الأحداث (أحداث DDL: CREATE/ALTER/DROP TABLE)
- تمكين/تعطيل المشغلات
- المشغلات مقابل منطق التطبيق
2. القصة
Alice هي مهندسة قواعد بيانات في منصة تجارة إلكترونية. تحتاج إلى تنفيذ منطقين أساسيين للأتمتة:
- خصم المخزون التلقائي: عند إدراج طلب جديد في
order_items، مشغل BEFORE INSERT يخصم تلقائياًproducts.stock_qty، ويرفض الإدراج إذا كان المخزون غير كافٍ. - سجل تدقيق السعر: عند تحديث سعر منتج، مشغل AFTER UPDATE يسجل الأسعار القديمة والجديدة في جدول
price_audit_log.
تختار Alice تنفيذ ذلك بالمشغلات، لضمان تطبيق قواعد العمل بشكل متسق بغض النظر عن أي تطبيق يكتب البيانات.
3. المفهوم: أساسيات المشغلات
(1) نظرة عامة على أنواع المشغلات
| التوقيت | مستوى الصف (FOR EACH ROW) | مستوى العبارة (FOR EACH STATEMENT) |
|---|---|---|
| BEFORE | يمكن تعديل NEW، رفض العملية | لا NEW/OLD، يمكن التحقق/التحضير |
| AFTER | يمكن قراءة NEW/OLD، التسجيل | جيد لإحصائيات الملخص |
| INSTEAD OF | طرق العرض فقط، يستبدل العملية الأصلية | لا ينطبق على مستوى العبارة |
(1) ▶ مثال: إنشاء مشغل BEFORE INSERT أساسي على مستوى الصف
CREATE OR REPLACE FUNCTION fn_before_order_item()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.created_at := NOW();
NEW.line_total := NEW.quantity * NEW.unit_price;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_before_order_item
BEFORE INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION fn_before_order_item();
Output:
INSERT 0 1
(2) ▶ مثال: مشغل AFTER UPDATE لسجل التدقيق
CREATE OR REPLACE FUNCTION fn_audit_price_change()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.unit_price IS DISTINCT FROM OLD.unit_price THEN
INSERT INTO price_audit_log
(product_id, old_price, new_price, changed_by, changed_at)
VALUES
(NEW.product_id, OLD.unit_price, NEW.unit_price,
CURRENT_USER, NOW());
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_audit_price
AFTER UPDATE OF unit_price ON products
FOR EACH ROW
EXECUTE FUNCTION fn_audit_price_change();
Output:
INSERT 0 1
(2) متغيرات NEW و OLD
| التوقيت | NEW | OLD | قابل للتعديل |
|---|---|---|---|
| BEFORE INSERT | نعم (الصف المراد إدراجه) | لا | يمكن تعديل NEW |
| BEFORE UPDATE | نعم (القيمة الجديدة) | نعم (القيمة القديمة) | يمكن تعديل NEW |
| BEFORE DELETE | لا | نعم (الصف المراد حذفه) | غير قابل للتعديل |
| AFTER INSERT | نعم (للقراءة فقط) | لا | غير قابل للتعديل |
| AFTER UPDATE | نعم (للقراءة فقط) | نعم (للقراءة فقط) | غير قابل للتعديل |
(3) ▶ مثال: BEFORE UPDATE يعدل NEW
CREATE OR REPLACE FUNCTION fn_auto_update_timestamp()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := NOW();
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_products_updated_at
BEFORE UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION fn_auto_update_timestamp();
Output:
CREATE TABLE
4. المفهوم: مشغلات مستوى الصف مقابل مستوى العبارة
(1) اختلافات تكرار التنفيذ
| البعد | FOR EACH ROW | FOR EACH STATEMENT |
|---|---|---|
| عدد التنفيذات | مرة لكل صف متأثر | مرة لكل عبارة SQL |
| NEW/OLD | متاحة | غير متاحة |
| تأثير الأداء | يتناسب مع الصفوف المتأثرة | عبء ثابت |
| الاستخدام النموذجي | التحقق، حساب العمود، التتالي | إحصائيات الملخص، تحديث الذاكرة المؤقتة |
(4) ▶ مثال: مشغل مستوى العبارة يحدث الملخص
CREATE OR REPLACE FUNCTION fn_refresh_order_stats()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_order_stats;
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_refresh_order_stats
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH STATEMENT
EXECUTE FUNCTION fn_refresh_order_stats();
Output:
INSERT 0 1
(5) ▶ مثال: مشغل مستوى الصف يتحقق من المخزون
CREATE OR REPLACE FUNCTION fn_check_stock()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_stock INT;
BEGIN
SELECT stock_qty INTO v_stock
FROM products
WHERE product_id = NEW.product_id;
IF v_stock < NEW.quantity THEN
RAISE EXCEPTION 'Insufficient stock: product % has % units, requested %',
NEW.product_id, v_stock, NEW.quantity;
END IF;
UPDATE products
SET stock_qty = stock_qty - NEW.quantity
WHERE product_id = NEW.product_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_check_stock
BEFORE INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION fn_check_stock();
Output:
INSERT 0 1
5. المفهوم: المشغلات الشرطية وترتيب التنفيذ
(1) عبارة WHEN
عبارة WHEN تجعل المشغل ينفذ فقط عند تحقق شرط، مما يقلل عبء الاستدعاء غير الضروري.
(6) ▶ مثال: مشغل شرطي بـ WHEN
CREATE OR REPLACE FUNCTION fn_log_big_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO big_order_log (order_id, total_amount, created_at)
VALUES (NEW.order_id, NEW.total_amount, NOW());
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_log_big_order
AFTER INSERT ON orders
FOR EACH ROW
WHEN (NEW.total_amount >= 5000)
EXECUTE FUNCTION fn_log_big_order();
Output:
INSERT 0 1
(2) قواعد ترتيب تنفيذ المشغلات
| القاعدة | الوصف |
|---|---|
| BEFORE قبل AFTER | جميع BEFORE تنفذ قبل أي AFTER |
| نفس التوقيت، أبجدي حسب الاسم | trg_a تنفذ قبل trg_b |
| INSTEAD OF يستبدل العملية الأصلية | طرق العرض فقط، لا ين��ذ INSERT/UPDATE/DELETE الأصلي |
| BEFORE تعيد NULL يحظر العملية | لـ UPDATE/INSERT، إعادة NULL تتخطى ذلك الصف |
(7) ▶ مثال: ترتيب تنفيذ مشغلات متعددة
-- هذه المشغلات تنفذ بالترتيب الأبجدي: a -> b -> c
CREATE TRIGGER trg_a_validate
BEFORE INSERT ON orders FOR EACH ROW
EXECUTE FUNCTION fn_validate_order();
CREATE TRIGGER trg_b_calc_tax
BEFORE INSERT ON orders FOR EACH ROW
EXECUTE FUNCTION fn_calc_order_tax();
CREATE TRIGGER trg_c_notify
AFTER INSERT ON orders FOR EACH ROW
EXECUTE FUNCTION fn_notify_new_order();
Output:
INSERT 0 1
6. المفهوم: مشغلات INSTEAD OF
(1) INSTEAD OF على طرق العرض
طريقة العرض نفسها لا تدعم INSERT/UPDATE/DELETE المباشر؛ مشغل INSTEAD OF يعترض العملية ويخصص منطق التنفيذ.
| الخاصية | الوصف |
|---|---|
| طرق العرض فقط | لا يمكن استخدام INSTEAD OF على الجد��ول |
| يجب أن يكون FOR EACH ROW | مستوى العبارة غير مدعوم |
| يستبدل العملية الأصلية | السلوك الافتراضي لا ينفذ |
| جيد لطرق العرض القابلة للتحديث | يعين كتابات طريقة العرض إلى الجداول الأساسية |
(8) ▶ مثال: INSTEAD OF INSERT في طريقة عرض
CREATE VIEW vw_customer_orders AS
SELECT
c.customer_id,
c.first_name,
c.last_name,
o.order_id,
o.total_amount
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id;
CREATE OR REPLACE FUNCTION fn_insert_customer_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO customers (first_name, last_name)
VALUES (NEW.first_name, NEW.last_name)
ON CONFLICT DO NOTHING;
INSERT INTO orders (customer_id, total_amount, order_date)
SELECT customer_id, NEW.total_amount, CURRENT_DATE
FROM customers
WHERE first_name = NEW.first_name AND last_name = NEW.last_name;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_insert_customer_order
INSTEAD OF INSERT ON vw_customer_orders
FOR EACH ROW
EXECUTE FUNCTION fn_insert_customer_order();
Output:
INSERT 0 1
(9) ▶ مثال: INSTEAD OF UPDATE في طريقة عرض
CREATE OR REPLACE FUNCTION fn_update_customer_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE customers
SET first_name = NEW.first_name,
last_name = NEW.last_name
WHERE customer_id = NEW.customer_id;
UPDATE orders
SET total_amount = NEW.total_amount
WHERE order_id = NEW.order_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_update_customer_order
INSTEAD OF UPDATE ON vw_customer_orders
FOR EACH ROW
EXECUTE FUNCTION fn_update_customer_order();
Output:
CREATE TABLE
7. المفهوم: مشغلات الأحداث
(1) مشغلات أحداث DDL (ميزة خاصة بـ PG)
مشغلات الأحداث تنشط عند أوامر DDL (CREATE/ALTER/DROP)، مستقلة عن أي جدول محدد.
| الحدث | التوقيت |
|---|---|
ddl_command_start |
قبل تنفيذ DDL |
ddl_command_end |
بعد تنفيذ DDL |
sql_drop |
قبل تنفيذ أمر DROP |
table_rewrite |
قبل إعادة كتابة الجدول (مثلاً ALTER TYPE) |
(10) ▶ مثال: مشغل حدث يمنع DROP TABLE
CREATE OR REPLACE FUNCTION fn_block_drop_table()
RETURNS EVENT_TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION 'DROP TABLE is not allowed in production!';
END;
$$;
CREATE EVENT TRIGGER etg_block_drop
ON sql_drop
WHEN tag IN ('DROP TABLE')
EXECUTE FUNCTION fn_block_drop_table();
Output:
CREATE TABLE
(11) ▶ مثال: سجل تدقيق DDL
CREATE TABLE ddl_audit_log (
id SERIAL PRIMARY KEY,
event_type TEXT,
tag TEXT,
object_type TEXT,
object_name TEXT,
command_text TEXT,
current_user TEXT,
event_time TIMESTAMP DEFAULT NOW()
);
CREATE OR REPLACE FUNCTION fn_log_ddl()
RETURNS EVENT_TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_obj RECORD;
BEGIN
v_obj := NULL;
INSERT INTO ddl_audit_log (event_type, tag, object_type, object_name, current_user)
VALUES (TG_EVENT, TG_TAG,
v_obj.object_type, v_obj.object_identity,
CURRENT_USER);
RAISE NOTICE 'DDL logged: % %', TG_EVENT, TG_TAG;
END;
$$;
CREATE EVENT TRIGGER etg_log_ddl
ON ddl_command_end
EXECUTE FUNCTION fn_log_ddl();
Output:
INSERT 0 1
(2) مشغلات الأحداث مقابل مشغلات الجداول
| البعد | مشغل الجدول | مشغل الحدث |
|---|---|---|
| الكائن المرتبط | جدول/طريقة عرض محدد | أحداث DDL عمومية |
| DML/DDL | DML (INSERT/UPDATE/DELETE) | DDL (CREATE/ALTER/DROP) |
| NEW/OLD | متاحة | لا شيء |
| TG_TAG | لا شيء | نعم (وسم مثل DROP TABLE) |
| الاستخدام النموذجي | التحقق، التدقيق، التتالي | تدقيق DDL، التحكم الأمني |
8. المفهوم: تمكين/تعطيل والمشغلات مقابل منطق التطبيق
(1) تمكين/تعطيل المشغلات
| الأمر | التأثير |
|---|---|
ALTER TABLE t DISABLE TRIGGER trg_name; |
تعطيل مشغل محدد |
ALTER TABLE t ENABLE TRIGGER trg_name; |
تمكين مشغل محدد |
ALTER TABLE t DISABLE TRIGGER ALL; |
تعطيل جميع المشغلات |
ALTER TABLE t ENABLE TRIGGER ALL; |
تمكين جميع المشغلات |
(1) ▶ مثال: تعطيل المشغلات للاستيراد الجماعي
-- تعطيل المشغلات لأداء الاستيراد الجماعي
ALTER TABLE products DISABLE TRIGGER ALL;
COPY products(product_name, unit_price, category, stock_qty)
FROM '/data/products_bulk.csv' WITH (FORMAT csv, HEADER true);
-- إعادة التمكين بعد الاستيراد
ALTER TABLE products ENABLE TRIGGER ALL;
Output:
-- SQL statement executed successfully
(2) المشغلات مقابل منطق التطبيق
| البعد | المشغل | منطق التطبيق |
|---|---|---|
| الاتساق | ينشط لأي مسار كتابة | يعتمد على التزام التطبيق بالقواعد |
| صعوبة التصحيح | ضمني، صعب التتبع | استدعاء صريح، سهل التصحيح |
| الأداء | عبء إضافي لكل صف | يمكن تجميعه/تحسينه |
| قابلية النقل | صيغة خاصة بـ PG | لغة عامة الأغراض |
| حالة الاستخدام | فرض الثوابت، التدقيق، التتالي | التدفقات المعقدة، عبر الأنظمة |
(12) ▶ مثال: مشغل يفرض اتساق البيانات
-- فرض: إجمالي الطلب يجب أن يساوي دائماً مجموع عناصر السطر
CREATE OR REPLACE FUNCTION fn_enforce_order_total()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_calculated NUMERIC;
BEGIN
SELECT COALESCE(SUM(line_total), 0) INTO v_calculated
FROM order_items
WHERE order_id = NEW.order_id;
IF v_calculated IS DISTINCT FROM (
SELECT total_amount FROM orders WHERE order_id = NEW.order_id
) THEN
UPDATE orders SET total_amount = v_calculated
WHERE order_id = NEW.order_id;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_enforce_order_total
AFTER INSERT OR UPDATE ON order_items
FOR EACH ROW
EXECUTE FUNCTION fn_enforce_order_total();
Output:
result
----------
42.50
(1 row)
9. مخطط التدفق: قرار اختيار المشغل
flowchart TD
A[بحاجة لمنطق أتمتة؟] --> B{DML أم DDL؟}
B -->|DDL| C[مشغل حدث<br/>EVENT TRIGGER]
B -->|DML| D{جدول أم طريقة عرض؟}
D -->|طريقة عرض| E[مشغل INSTEAD OF<br/>FOR EACH ROW]
D -->|جدول| F{تعديل البيانات قبل العملية؟}
F -->|نعم| G[مشغل BEFORE<br/>يمكن تعديل NEW]
F -->|لا| H{بحاجة لتسجيل/تتالي؟}
H -->|نعم| I[مشغل AFTER<br/>يمكن قراءة NEW/OLD]
H -->|لا| J[لا حاجة لمشغل]
G --> K{لكل صف أم لكل SQL؟}
I --> K
K -->|لكل صف| L[FOR EACH ROW]
K -->|لكل SQL| M[FOR EACH STATEMENT]
L --> N{بحاجة لشرط؟}
N -->|نعم| O[عبارة WHEN]
N -->|لا| P[بدون شرط]
style C fill:#fff9c4
style E fill:#e1bee7
style G fill:#c8e6c9
style I fill:#bbdefb
10. مثال شامل
مخطط مشغلات التجارة الإلكترونية لـ Alice — خصم المخزون + تدقيق السعر + تحديث تلقائي للطابع الزمني:
-- 1. تحديث تلقائي للطابع الزمني عند تغييرات المنتج
CREATE OR REPLACE FUNCTION fn_products_timestamp()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := NOW();
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_products_timestamp
BEFORE UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION fn_products_timestamp();
-- 2. خصم المخزون عند إدراج عنصر طلب، رفض إذا كان غير كافٍ
CREATE OR REPLACE FUNCTION fn_decrease_stock()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_stock INT;
BEGIN
SELECT stock_qty INTO v_stock FROM products
WHERE product_id = NEW.product_id FOR UPDATE;
IF v_stock IS NULL THEN
RAISE EXCEPTION 'Product % not found', NEW.product_id;
ELSIF v_stock < NEW.quantity THEN
RAISE EXCEPTION 'Insufficient stock: product % (stock=%, requested=%)',
NEW.product_id, v_stock, NEW.quantity;
END IF;
UPDATE products SET stock_qty = stock_qty - NEW.quantity
WHERE product_id = NEW.product_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_decrease_stock
BEFORE INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION fn_decrease_stock();
-- 3. سجل تدقيق عند تغير السعر
CREATE OR REPLACE FUNCTION fn_price_audit()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.unit_price IS DISTINCT FROM OLD.unit_price THEN
INSERT INTO price_audit_log
(product_id, old_price, new_price, changed_by, changed_at)
VALUES
(NEW.product_id, OLD.unit_price, NEW.unit_price,
CURRENT_USER, NOW());
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_price_audit
AFTER UPDATE OF unit_price ON products
FOR EACH ROW
WHEN (OLD.unit_price IS DISTINCT FROM NEW.unit_price)
EXECUTE FUNCTION fn_price_audit();
❓ أسئلة شائعة
trg_a قبل trg_b. جميع مشغلات BEFORE تنفذ قبل أي مشغلات AFTER.CREATE TABLE، ALTER TABLE، DROP TABLE. يمكنك التصفية بـ WHEN علامة IN (...).📖 ملخص
- التوقيت: BEFORE (يمكن تعديل NEW)، AFTER (تسجيل)، INSTEAD OF (تعيين طريقة العرض)
- مشغلات مستوى الصف تنفذ لكل صف؛ مستوى العبارة تنفذ مرة لكل عبارة SQL
- NEW تحمل القيمة الجديدة، OLD تحمل القيمة القديمة؛ NEW قابلة للتعديل في مرحلة BEFORE
- عبارة WHEN تصفي الشروط، مق��لة استدعاءات المشغلات غير الضرورية
- المشغلات في نفس التوقيت تنفذ بالترتيب الأبجدي للاسم
- INSTEAD OF لطرق العرض فقط ويجب أن تكون FOR EACH ROW
- مشغلات الأحداث (ميزة خاصة بـ PG) تستمع لأوامر DDL — جيدة للتدقيق والتحكم الأمني
- DISABLE/ENABLE TRIGGER يتحكم في حالة المشغل؛ التعطيل أثناء الاستيراد الجماعي يسرع الأمور
- المشغلات تضمن اتساق البيانات لكن تضيف تعقيداً في التصحيح — زن بعناية
📝 تمارين
-
⭐ اكتب دالة مشغل BEFORE UPDATE ومشغلاً، عندما يعدل عمود
emailفي جدولcustomers، يعين تلقائياًupdated_atإلىNOW(). -
⭐⭐ اكتب مشغل AFTER INSERT، عندما يضاف طلب جديد إلى جدول
ordersمعtotal_amount >= 1000دولار، يدرج تلقائياً في جدولhigh_value_order_log(مسجلاً order_id، total_amount، customer_id، created_at). -
⭐⭐⭐ أنشئ طريقة عرض
vw_product_sales(تضم products + order_items لحساب المبيعات) واكتب مشغل INSTEAD OF UPDATE يعينtotal_soldالمعدل في طريقة العرض مرة أخرى لتحديثproducts.stock_qty. ثم اكتب مشغل حدث يسجل جميع عمليات DDL من نوعCREATE TABLEوDROP TABLEفي جدولddl_audit_log.