PostgreSQL: المشغلات ومشغلات الأحداث في PostgreSQL

آخر تحديث: 2026-08-26

1. ما ستتعلمه


2. القصة

Alice هي مهندسة قواعد بيانات في منصة تجارة إلكترونية. تحتاج إلى تنفيذ منطقين أساسيين للأتمتة:

  1. خصم المخزون التلقائي: عند إدراج طلب جديد في order_items، مشغل BEFORE INSERT يخصم تلقائياً products.stock_qty، ويرفض الإدراج إذا كان المخزون غير كافٍ.
  2. سجل تدقيق السعر: عند تحديث سعر منتج، مشغل 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 أساسي على مستوى الصف

SQL
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:

TEXT 📖 للعرض فقط
INSERT 0 1

(2) ▶ مثال: مشغل AFTER UPDATE لسجل التدقيق

SQL
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:

TEXT 📖 للعرض فقط
INSERT 0 1

(2) متغيرات NEW و OLD

التوقيت NEW OLD قابل للتعديل
BEFORE INSERT نعم (الصف المراد إدراجه) لا يمكن تعديل NEW
BEFORE UPDATE نعم (القيمة الجديدة) نعم (القيمة القديمة) يمكن تعديل NEW
BEFORE DELETE لا نعم (الصف المراد حذفه) غير قابل للتعديل
AFTER INSERT نعم (للقراءة فقط) لا غير قابل للتعديل
AFTER UPDATE نعم (للقراءة فقط) نعم (للقراءة فقط) غير قابل للتعديل

(3) ▶ مثال: BEFORE UPDATE يعدل NEW

SQL
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:

TEXT 📖 للعرض فقط
CREATE TABLE

4. المفهوم: مشغلات مستوى الصف مقابل مستوى العبارة

(1) اختلافات تكرار التنفيذ

البعد FOR EACH ROW FOR EACH STATEMENT
عدد التنفيذات مرة لكل صف متأثر مرة لكل عبارة SQL
NEW/OLD متاحة غير متاحة
تأثير الأداء يتناسب مع الصفوف المتأثرة عبء ثابت
الاستخدام النموذجي التحقق، حساب العمود، التتالي إحصائيات الملخص، تحديث الذاكرة المؤقتة

(4) ▶ مثال: مشغل مستوى العبارة يحدث الملخص

SQL
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:

TEXT 📖 للعرض فقط
INSERT 0 1

(5) ▶ مثال: مشغل مستوى الصف يتحقق من المخزون

SQL
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:

TEXT 📖 للعرض فقط
INSERT 0 1

5. المفهوم: المشغلات الشرطية وترتيب التنفيذ

(1) عبارة WHEN

عبارة WHEN تجعل المشغل ينفذ فقط عند تحقق شرط، مما يقلل عبء الاستدعاء غير الضروري.

(6) ▶ مثال: مشغل شرطي بـ WHEN

SQL
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:

TEXT 📖 للعرض فقط
INSERT 0 1

(2) قواعد ترتيب تنفيذ المشغلات

القاعدة الوصف
BEFORE قبل AFTER جميع BEFORE تنفذ قبل أي AFTER
نفس التوقيت، أبجدي حسب الاسم trg_a تنفذ قبل trg_b
INSTEAD OF يستبدل العملية الأصلية طرق العرض فقط، لا ين��ذ INSERT/UPDATE/DELETE الأصلي
BEFORE تعيد NULL يحظر العملية لـ UPDATE/INSERT، إعادة NULL تتخطى ذلك الصف

(7) ▶ مثال: ترتيب تنفيذ مشغلات متعددة

SQL
-- هذه المشغلات تنفذ بالترتيب الأبجدي: 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:

TEXT 📖 للعرض فقط
INSERT 0 1

6. المفهوم: مشغلات INSTEAD OF

(1) INSTEAD OF على طرق العرض

طريقة العرض نفسها لا تدعم INSERT/UPDATE/DELETE المباشر؛ مشغل INSTEAD OF يعترض العملية ويخصص منطق التنفيذ.

الخاصية الوصف
طرق العرض فقط لا يمكن استخدام INSTEAD OF على الجد��ول
يجب أن يكون FOR EACH ROW مستوى العبارة غير مدعوم
يستبدل العملية الأصلية السلوك الافتراضي لا ينفذ
جيد لطرق العرض القابلة للتحديث يعين كتابات طريقة العرض إلى الجداول الأساسية

(8) ▶ مثال: INSTEAD OF INSERT في طريقة عرض

SQL
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:

TEXT 📖 للعرض فقط
INSERT 0 1

(9) ▶ مثال: INSTEAD OF UPDATE في طريقة عرض

SQL
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:

TEXT 📖 للعرض فقط
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

SQL
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:

TEXT 📖 للعرض فقط
CREATE TABLE

(11) ▶ مثال: سجل تدقيق DDL

SQL
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:

TEXT 📖 للعرض فقط
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) ▶ مثال: تعطيل المشغلات للاستيراد الجماعي

SQL
-- تعطيل المشغلات لأداء الاستيراد الجماعي
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:

TEXT 📖 للعرض فقط
-- SQL statement executed successfully

(2) المشغلات مقابل منطق التطبيق

البعد المشغل منطق التطبيق
الاتساق ينشط لأي مسار كتابة يعتمد على التزام التطبيق بالقواعد
صعوبة التصحيح ضمني، صعب التتبع استدعاء صريح، سهل التصحيح
الأداء عبء إضافي لكل صف يمكن تجميعه/تحسينه
قابلية النقل صيغة خاصة بـ PG لغة عامة الأغراض
حالة الاستخدام فرض الثوابت، التدقيق، التتالي التدفقات المعقدة، عبر الأنظمة

(12) ▶ مثال: مشغل يفرض اتساق البيانات

SQL
-- فرض: إجمالي الطلب يجب أن يساوي دائماً مجموع عناصر السطر
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:

TEXT 📖 للعرض فقط
  result  
----------
   42.50
(1 row)

9. مخطط التدفق: قرار اختيار المشغل

100%
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 — خصم المخزون + تدقيق السعر + تحديث تلقائي للطابع الزمني:

SQL
-- 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();

❓ أسئلة شائعة

س ماذا يحدث إذا أعاد مشغل BEFORE قيمة NULL؟
ج لـ INSERT/UPDATE، إعادة NULL تعني تخطي عملية ذلك الصف (لا كتابة فعلية ولا مشغلات لاحقة تنفذ). ليس لها تأثير لـ DELETE. لاحظ أن قيمة إرجاع مشغل AFTER يتم تجاهلها.
س إذا كان للجدول مشغلات متعددة في نفس التوقيت، كيف يقرر الترتيب؟
ج PostgreSQL تنفذها بالترتيب الأبجدي حسب اسم المشغل — مثلاً trg_a قبل trg_b. جميع مشغلات BEFORE تنفذ قبل أي مشغلات AFTER.
س هل يمكن للمشغل تنفيذ COMMIT؟
ج مشغل الجدول العادي لا يمكنه تنفيذ COMMIT/ROLLBACK (يعمل داخل معاملة). للمعاملات المستقلة، استخدم dblink داخل دالة المشغل، أو نهج PG 14+ الإجرائي.
س هل يمكن لشرط عبارة WHEN الإشارة إلى جداول أخرى؟
ج لا. شرط WHEN يمكنه فقط الإشارة إلى أعمدة NEW/OLD — لا استعلامات فرعية أو إشارات إلى جداول أخرى. للشروط المعقدة، تحقق داخل جسم دالة المشغل.
س هل يمكن لمشغل INSTEAD OF استخدام FOR EACH STATEMENT؟
ج لا. مشغل INSTEAD OF يجب أن يكون FOR EACH ROW، لأنه يحتاج إلى تقرير لكل صف كيفية ا��تعيين إلى عمليات الجدول الأساسي.
س ما هو TG_TAG في مشغل الحدث؟
ج TG_TAG هو وسم أمر DDL الذي نشط الحدث، مثلاً CREATE TABLE، ALTER TABLE، DROP TABLE. يمكنك التصفية بـ WHEN علامة IN (...).
س هل استيراد COPY أسرع بعد تعطيل المشغلات؟
ج نعم. تعطيل مشغلات مستوى الصف أثناء استيراد البيانات الكبيرة يحسن الأداء بشكل كبير، لكن بعد إعادة التمكين يجب عليك التعامل يدوياً مع المنطق الذي كانت ستنفذه المشغلات (مثلاً الأعمدة المحسوبة، سجلات التدقيق).
س كيف أتجنب استدعاءات المشغلات التكرارية؟
ج المشغل A الذي يحدث الجدول T قد يشعل A مرة أخرى. تجنبه بـ: شرط WHEN، متغير حالة (مثلاً متغير على مستوى الحزمة)، أو التبديل إلى مشغل AFTER مع فحص شرطي.

📖 ملخص


📝 تمارين

  1. ⭐ اكتب دالة مشغل BEFORE UPDATE ومشغلاً، عندما يعدل عمود email في جدول customers، يعين تلقائياً updated_at إلى NOW().

  2. ⭐⭐ اكتب مشغل AFTER INSERT، عندما يضاف طلب جديد إلى جدول orders مع total_amount >= 1000 دولار، يدرج تلقائياً في جدول high_value_order_log (مسجلاً order_id، total_amount، customer_id، created_at).

  3. ⭐⭐⭐ أنشئ طريقة عرض vw_product_sales (تضم products + order_items لحساب المبيعات) واكتب مشغل INSTEAD OF UPDATE يعين total_sold المعدل في طريقة العرض مرة أخرى لتحديث products.stock_qty. ثم اكتب مشغل حدث يسجل جميع عمليات DDL من نوع CREATE TABLE و DROP TABLE في جدول ddl_audit_log.

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%