PostgreSQL: الإجراءات المخزنة و PL/pgSQL في PostgreSQL
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- CREATE FUNCTION / CREATE PROCEDURE (PG يميز بين الدوال والإجراءات المخزنة)
- صيغة PL/pgSQL: المتغيرات، الإسناد، IF/CASE/LOOP/WHILE/FOR
- أنماط المعاملات: IN / OUT / INOUT / VARIADIC
- RETURN و RETURN QUERY
- المؤشرات: CURSOR / REFCURSOR
- معالجة الاستثناءات: EXCEPTION / RAISE
- دوال المشغلات
- SQL الديناميكي: EXECUTE
2. القصة
Bob هو مهندس خلفية في منصة تجارة إلكترونية. كل صباح يحتاج إلى تشغيل سلسلة من مهام البيانات تلقائيًا:
- حساب ملخص مبيعات اليوم السابق
- تحديث طريقة العرض المادية
mv_daily_sales - إذا فشل الحساب، إخطار فريق العمليات
قرر Bob كتابة إجراء مخزن sp_daily_sales_refresh() بـ PL/pgSQL، مغلفًا كل المنطق على جانب قاعدة البيانات "للتنفيذ بنقرة واحدة."
3. المفهوم: FUNCTION مقابل PROCEDURE
(1) الفرق بين الدوال والإجراءات في PG
| الميزة | FUNCTION | PROCEDURE |
|---|---|---|
| قيمة الإرجاع | يجب أن يكون لها RETURNS | لا RETURNS (قد لا ترجع شيئًا) |
| أسلوب الاستدعاء | SELECT func() |
CALL proc() |
| التحكم بالمعاملات | لا يمكن COMMIT/ROLLBACK داخليًا | يمكن COMMIT/ROLLBACK داخليًا |
| الاستخدام في SQL | في SELECT/WHERE | لا يمكن تضمينها في SQL |
| معاملات INOUT | مدعومة، تصبح تلقائيًا أعمدة إرجاع | مدعومة، تمرر مرة أخرى عبر CALL |
(1) ▶ مثال
CREATE OR REPLACE FUNCTION fn_get_order_count(p_customer_id INT)
RETURNS INT
LANGUAGE plpgsql
AS $$
DECLARE
v_count INT;
BEGIN
SELECT COUNT(*) INTO v_count
FROM orders
WHERE customer_id = p_customer_id;
RETURN v_count;
END;
$$;
-- استدعاء كقيمة عددية في SELECT
SELECT fn_get_order_count(1001);
Output:
count
-------
5
(1 row)
(2) ▶ مثال
CREATE OR REPLACE PROCEDURE sp_reset_daily_stats()
LANGUAGE plpgsql
AS $$
BEGIN
TRUNCATE TABLE daily_stats;
INSERT INTO daily_stats (stat_date, total_orders, total_revenue)
VALUES (CURRENT_DATE, 0, 0.00);
COMMIT;
END;
$$;
-- استدعاء بـ CALL
CALL sp_reset_daily_stats();
Output:
INSERT 0 1
(2) متى تستخدم FUNCTION مقابل PROCEDURE
| السيناريو | الموصى به | السبب |
|---|---|---|
| حساب وإرجاع قيمة | FUNCTION | يمكن تضمينها في SQL |
| عمليات ETL دفعة | PROCEDURE | تدعم التحكم الداخلي بالمعاملات |
| استدعاء مشغل | FUNCTION | المشغلات تقبل الدوال فقط |
| مهمة مجدولة | PROCEDURE | يمكن الإيداع خطوة بخطوة |
(3) ▶ مثال
CREATE OR REPLACE FUNCTION fn_calc_tax(
p_amount NUMERIC,
p_rate NUMERIC DEFAULT 0.08
)
RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
BEGIN
RETURN ROUND(p_amount * p_rate, 2);
END;
$$;
SELECT fn_calc_tax(1000); -- استخدام الافتراضي 8%
SELECT fn_calc_tax(1000, 0.10); -- مخصص 10%
Output:
CREATE TABLE
4. المفهوم: أساسيات PL/pgSQL
(1) المتغيرات والإسناد
DECLARE
v_name TEXT;
v_price NUMERIC(10,2) := 0.00;
v_count INT;
v_row orders%ROWTYPE; -- نوع صف من الجدول
v_status TEXT NOT NULL := 'pending';
BEGIN
v_name := 'Bob';
SELECT unit_price INTO v_price FROM products WHERE product_id = 1;
END;
| أسلوب التصريح | الصيغة | الوصف |
|---|---|---|
| نوع أساسي | v_name TEXT; |
تصريح مباشر |
| قيمة افتراضية | v_price NUMERIC := 0; |
:= أو DEFAULT |
| نوع صف | v_row orders%ROWTYPE; |
يطابق هيكل الجدول |
| NOT NULL | v_status TEXT NOT NULL := 'x'; |
يجب تعيين قيمة ابتدائية |
(4) ▶ مثال
CREATE OR REPLACE FUNCTION fn_customer_summary(p_id INT)
RETURNS TEXT
LANGUAGE plpgsql
AS $$
DECLARE
v_name TEXT;
v_total NUMERIC(12,2);
v_msg TEXT;
BEGIN
SELECT first_name || ' ' || last_name, SUM(total_amount)
INTO v_name, v_total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE c.customer_id = p_id
GROUP BY c.customer_id, c.first_name, c.last_name;
v_msg := v_name || ': $' || COALESCE(v_total::TEXT, '0');
RETURN v_msg;
END;
$$;
Output:
result
----------
42.50
(1 row)
(2) العبارات الشرطية
(5) ▶ مثال
CREATE OR REPLACE FUNCTION fn_discount_tier(p_total NUMERIC)
RETURNS TEXT
LANGUAGE plpgsql
AS $$
BEGIN
IF p_total >= 10000 THEN
RETURN 'PLATINUM';
ELSIF p_total >= 5000 THEN
RETURN 'GOLD';
ELSIF p_total >= 1000 THEN
RETURN 'SILVER';
ELSE
RETURN 'BRONZE';
END IF;
END;
$$;
Output:
CREATE TABLE
(6) ▶ مثال
CREATE OR REPLACE FUNCTION fn_order_priority(p_amount NUMERIC)
RETURNS TEXT
LANGUAGE plpgsql
AS $$
BEGIN
CASE
WHEN p_amount >= 5000 THEN RETURN 'URGENT';
WHEN p_amount >= 1000 THEN RETURN 'HIGH';
WHEN p_amount >= 100 THEN RETURN 'NORMAL';
ELSE RETURN 'LOW';
END CASE;
END;
$$;
Output:
CREATE TABLE
(3) عبارات الحلقة
| نوع الحلقة | الصيغة | الأفضل لـ |
|---|---|---|
| LOOP | LOOP ... END LOOP; |
يحتاج EXIT يدوي |
| WHILE | WHILE cond LOOP ... END LOOP; |
فحص الشرط أولاً |
| FOR (عدد صحيح) | FOR i IN 1..10 LOOP ... END LOOP; |
عدد ثابت من التكرارات |
| FOR (استعلام) | FOR rec IN SELECT ... LOOP ... END LOOP; |
التكرار على نتائج الاستعلام |
(7) ▶ مثال
CREATE OR REPLACE FUNCTION fn_find_price_threshold(
p_target NUMERIC
)
RETURNS INT
LANGUAGE plpgsql
AS $$
DECLARE
v_limit INT := 10;
v_sum NUMERIC := 0;
v_id INT;
BEGIN
LOOP
SELECT unit_price INTO v_sum FROM products
WHERE product_id = v_limit;
v_sum := v_sum + p_target;
EXIT WHEN v_limit > 100 OR v_sum > p_target * 10;
v_limit := v_limit + 10;
END LOOP;
RETURN v_limit;
END;
$$;
Output:
result
----------
42.50
(1 row)
(8) ▶ مثال
CREATE OR REPLACE FUNCTION fn_sum_category_revenue()
RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
v_rec RECORD;
v_total NUMERIC(14,2) := 0;
BEGIN
FOR v_rec IN
SELECT category, SUM(unit_price * stock_qty) AS cat_rev
FROM products
GROUP BY category
LOOP
v_total := v_total + v_rec.cat_rev;
RAISE NOTICE 'Category %: $%', v_rec.category, v_rec.cat_rev;
END LOOP;
RETURN v_total;
END;
$$;
Output:
result
----------
42.50
(1 row)
5. المفهوم: أنماط المعاملات
(1) IN / OUT / INOUT / VARIADIC
| النمط | إدخال | إخراج | الوصف |
|---|---|---|---|
| IN (افتراضي) | نعم | لا | معامل للقراءة فقط |
| OUT | لا | نعم | معامل إخراج، يصبح تلقائيًا عمود إرجاع |
| INOUT | نعم | نعم | معامل ثنائي الاتجاه |
| VARIADIC | نعم | لا | قائمة وسائط متغيرة الطول (مصفوفة) |
(9) ▶ مثال
CREATE OR REPLACE FUNCTION fn_order_stats(
p_customer_id INT,
OUT o_count INT,
OUT o_total NUMERIC
)
LANGUAGE plpgsql
AS $$
BEGIN
SELECT COUNT(*), COALESCE(SUM(total_amount), 0)
INTO o_count, o_total
FROM orders
WHERE customer_id = p_customer_id;
END;
$$;
SELECT * FROM fn_order_stats(1001);
Output:
count
-------
5
(1 row)
(10) ▶ مثال
CREATE OR REPLACE FUNCTION fn_apply_discount(
p_price INOUT NUMERIC,
p_rate NUMERIC
)
LANGUAGE plpgsql
AS $$
BEGIN
p_price := ROUND(p_price * (1 - p_rate), 2);
END;
$$;
-- يرجع p_price المعدل كنتيجة
SELECT fn_apply_discount(99.99, 0.15);
Output:
count
-------
5
(1 row)
(2) RETURN مقابل RETURN QUERY
| العبارة | الغرض | نوع الإرجاع |
|---|---|---|
RETURN value; |
إرجاع قيمة واحدة | يطابق نوع RETURNS |
RETURN NEXT row; |
إلحاق صف واحد بمجموعة النتائج | RETURNS SETOF |
RETURN QUERY SELECT ...; |
إرجاع نتيجة استعلام كاملة | RETURNS SETOF/TABLE |
(11) ▶ مثال
CREATE OR REPLACE FUNCTION fn_orders_by_date(p_date DATE)
RETURNS SETOF orders
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT * FROM orders
WHERE order_date = p_date
ORDER BY order_id;
END;
$$;
SELECT * FROM fn_orders_by_date('2025-06-01');
Output:
CREATE TABLE
(12) ▶ مثال
CREATE OR REPLACE FUNCTION fn_top_customers(p_limit INT)
RETURNS TABLE(
customer_name TEXT,
total_spent NUMERIC,
order_count INT
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
c.first_name || ' ' || c.last_name,
SUM(o.total_amount)::NUMERIC,
COUNT(*)::INT
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
ORDER BY total_spent DESC
LIMIT p_limit;
END;
$$;
Output:
count
-------
5
(1 row)
6. المفهوم: المؤشرات
(1) المؤشرات الصريحة و REFCURSOR
| نوع المؤشر | التصريح | الأفضل لـ |
|---|---|---|
| مؤشر مرتبط | CURSOR (query) FOR |
استعلام ثابت |
| REFCURSOR | REFCURSOR |
استعلام ديناميكي، يمكن إرجاعه للمستدعي |
| مؤشر ضمني | FOR rec IN SELECT | تنقل بسيط |
(13) ▶ مثال
CREATE OR REPLACE FUNCTION fn_process_overdue_orders()
RETURNS INT
LANGUAGE plpgsql
AS $$
DECLARE
v_cur CURSOR(p_date DATE) FOR
SELECT order_id, customer_id, total_amount
FROM orders
WHERE order_date < p_date
AND order_status = 'pending';
v_rec RECORD;
v_count INT := 0;
BEGIN
OPEN v_cur(CURRENT_DATE - INTERVAL '7 days');
LOOP
FETCH v_cur INTO v_rec;
EXIT WHEN NOT FOUND;
UPDATE orders SET order_status = 'cancelled'
WHERE order_id = v_rec.order_id;
v_count := v_count + 1;
END LOOP;
CLOSE v_cur;
RETURN v_count;
END;
$$;
Output:
count
-------
5
(1 row)
(14) ▶ مثال
CREATE OR REPLACE FUNCTION fn_open_category_cursor(
p_category TEXT
)
RETURNS REFCURSOR
LANGUAGE plpgsql
AS $$
DECLARE
v_ref REFCURSOR;
BEGIN
OPEN v_ref FOR
SELECT product_id, product_name, unit_price
FROM products
WHERE category = p_category
ORDER BY unit_price DESC;
RETURN v_ref;
END;
$$;
BEGIN;
SELECT fn_open_category_cursor('Electronics');
-- يرجع اسم المؤشر مثل "<unnamed portal 1>"
FETCH ALL FROM "<unnamed portal 1>";
COMMIT;
Output:
CREATE TABLE
7. المفهوم: معالجة الاستثناءات
(1) كتلة EXCEPTION
BEGIN
-- المنطق العادي
EXCEPTION
WHEN OTHERS THEN
-- معالجة الخطأ
END;
| الحالة | الوصف |
|---|---|
NO_DATA_FOUND |
SELECT INTO لم يرجع أي صفوف |
TOO_MANY_ROWS |
SELECT INTO أرجع أكثر من صف واحد |
UNIQUE_VIOLATION |
انتهاك قيد التفرد |
FOREIGN_KEY_VIOLATION |
انتهاك مفتاح خارجي |
DIVISION_BY_ZERO |
قسمة على صفر |
OTHERS |
التقاط جميع الاستثناءات |
(15) ▶ مثال
CREATE OR REPLACE FUNCTION fn_safe_insert_product(
p_name TEXT, p_price NUMERIC, p_cat TEXT
)
RETURNS TEXT
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO products (product_name, unit_price, category)
VALUES (p_name, p_price, p_cat);
RETURN 'OK';
EXCEPTION
WHEN unique_violation THEN
RETURN 'DUPLICATE: ' || p_name;
END;
$$;
Output:
INSERT 0 1
(2) RAISE إشعار وخطأ
| مستوى RAISE | السلوك |
|---|---|
DEBUG |
سجل تطوير فقط |
LOG |
مكتوب إلى سجل الخادم |
NOTICE |
معروض للعميل |
WARNING |
معروض للعميل + سجل |
EXCEPTION |
يرمي خطأ، يتراجع عن المعاملة |
(16) ▶ مثال
CREATE OR REPLACE FUNCTION fn_validate_order(p_amount NUMERIC)
RETURNS BOOLEAN
LANGUAGE plpgsql
AS $$
BEGIN
IF p_amount <= 0 THEN
RAISE EXCEPTION 'Invalid order amount: $%', p_amount;
ELSIF p_amount > 100000 THEN
RAISE WARNING 'Large order: $% - requires approval', p_amount;
ELSE
RAISE NOTICE 'Order validated: $%', p_amount;
END IF;
RETURN true;
END;
$$;
Output:
CREATE TABLE
8. المفهوم: SQL الديناميكي
(1) استخدام EXECUTE
| السيناريو | الصيغة | الوصف |
|---|---|---|
| DDL ديناميكي | EXECUTE 'CREATE TABLE ...'; |
بناء SQL في وقت التشغيل |
| معلمي | EXECUTE fmt USING v1, v2; |
معلمي، آمن من الحقن |
| الحصول على نتيجة | EXECUTE sql INTO v_var; |
تخزين في متغير |
| الحصول عل�� عدة صفوف | FOR rec IN EXECUTE sql LOOP |
التكرار على استعلام ديناميكي |
(17) ▶ مثال
CREATE OR REPLACE FUNCTION fn_create_monthly_partition(
p_year INT, p_month INT
)
RETURNS TEXT
LANGUAGE plpgsql
AS $$
DECLARE
v_sql TEXT;
v_table TEXT;
v_start DATE;
v_end DATE;
BEGIN
v_table := 'orders_y' || p_year || 'm' || LPAD(p_month::TEXT, 2, '0');
v_start := make_date(p_year, p_month, 1);
v_end := v_start + INTERVAL '1 month';
v_sql := format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF orders
FOR VALUES FROM (%L) TO (%L)',
v_table, v_start, v_end
);
EXECUTE v_sql;
RETURN v_table || ' created';
END;
$$;
Output:
CREATE TABLE
(18) ▶ مثال
CREATE OR REPLACE FUNCTION fn_search_products(
p_col TEXT, p_val TEXT
)
RETURNS SETOF products
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY EXECUTE format(
'SELECT * FROM products WHERE %I = $1',
p_col
) USING p_val;
END;
$$;
SELECT * FROM fn_search_products('category', 'Electronics');
Output:
CREATE TABLE
9. مخطط التدفق: قرارات هيكل PL/pgSQL
flowchart TD
A[هل تحتاج تغليف المنطق؟] --> B{هل تحتاج قيمة إرجاع؟}
B -->|نعم| C[CREATE FUNCTION]
B -->|لا| D{هل تحتاج تحكم بالمعاملات؟}
D -->|نعم| E[CREATE PROCEDURE]
D -->|لا| C
C --> F{إرجاع صف واحد / عدة صفوف؟}
F -->|قيمة واحدة| G[RETURN value]
F -->|عدة صفوف| H{هل الاستعلام ثابت؟}
H -->|نعم| I[RETURN QUERY SELECT]
H -->|لا| J[FOR rec IN EXECUTE ... LOOP]
E --> K{هل تحتاج SQL ديناميكي؟}
K -->|نعم| L[EXECUTE format(...) USING]
K -->|لا| M[عبارات SQL ثابتة]
G --> N{هل يمكن أن يخطئ؟}
N -->|نعم| O[كتلة EXCEPTION]
N -->|لا| P[منطق مباشر]
I --> N
style A fill:#e1f5fe
style O fill:#ffcdd2
style L fill:#c8e6c9
10. مثال شامل
الإجراء المخزن اليومي لملخص المبيعات لـ Bob — يحسب المبيعات، يحدث طريقة العرض المادية، ويخطر بالأخطاء:
CREATE OR REPLACE PROCEDURE sp_daily_sales_refresh()
LANGUAGE plpgsql
AS $$
DECLARE
v_yesterday DATE := CURRENT_DATE - INTERVAL '1 day';
v_order_count INT;
v_total_revenue NUMERIC(14,2);
v_avg_order NUMERIC(10,2);
BEGIN
RAISE NOTICE 'بدء التحديث اليومي لـ %', v_yesterday;
SELECT COUNT(*), COALESCE(SUM(total_amount), 0),
COALESCE(AVG(total_amount), 0)
INTO v_order_count, v_total_revenue, v_avg_order
FROM orders
WHERE order_date = v_yesterday
AND order_status = 'completed';
INSERT INTO daily_sales_summary
(stat_date, order_count, total_revenue, avg_order_value, created_at)
VALUES
(v_yesterday, v_order_count, v_total_revenue, v_avg_order, NOW())
ON CONFLICT (stat_date) DO UPDATE
SET order_count = EXCLUDED.order_count,
total_revenue = EXCLUDED.total_revenue,
avg_order_value = EXCLUDED.avg_order_value;
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_sales;
RAISE NOTICE 'تم: % طلب، $% إيرادات، $% متوسط',
v_order_count, v_total_revenue, v_avg_order;
EXCEPTION
WHEN OTHERS THEN
INSERT INTO error_log (error_time, procedure_name, error_msg)
VALUES (NOW(), 'sp_daily_sales_refresh', SQLERRM);
RAISE NOTICE 'خطأ في التحديث اليومي: %', SQLERRM;
-- إخطار فريق العمليات عبر pg_notify
PERFORM pg_notify('ops_alerts',
'فشل تحديث المبيعا�� اليومي: ' || SQLERRM);
END;
$$;
❓ أسئلة شائعة
IF NOT FOUND THEN أو EXCEPTION WHEN NO_DATA_FOUND.%I في format يقتبس المعرفات تلقائيًا و %L يتعامل مع القيم الحرفية، بينما USING يعلمن التنفيذ لمنع حقن SQL. ربط السلاسل المباشر يحمل خطر الحقن ويجبرك على التعامل مع هروب علامات الاقتباس يدويًا.FOR rec IN SELECT في PL/pgSQL؟📖 ملخص
- PG يميز FUNCTION (يجب أن يرجع قيمة، يمكن تضمينه في SQL) عن PROCEDURE (لا قيمة إرجاع، يدعم التحكم بالمعاملات)
- PL/pgSQL يسند المتغيرات بـ
:=، يجلب قيم الاستعلام بـSELECT INTO، ويطابق أنواع الصفوف بـ%ROWTYPE - الشرطيات: كلا IF/ELSIF/ELSE و CASE؛ CASE يناسب المنطق متعدد الفروع بشكل أفضل
- الحلقات: LOOP (يحتاج EXIT)، WHILE (شرط مقدمًا)، FOR (عدد ثابت أو تنقل استعلام)
- أنماط المعاملات: IN للقراءة فقط، OUT عمود إخراج، INOUT ثنائي الاتجاه، VARIADIC يأخذ وسائط متغيرة
- RETURN يرجع قيمة واحدة؛ RETURN NEXT/QUERY يرجعان مجموعة
- المؤشرات: المؤشرات المرتبطة تناسب الاستعلامات الثابتة؛ REFCURSOR يناسب الاستعلامات الديناميكية والجلب المؤجل
- كتل EXCEPTION تلتقط الأخطاء لكنها تضيف عبء معاملة فرعية؛ RAISE يتحكم في مستوى الرسالة
- EXECUTE + format + USING يشغل SQL ديناميكي ويمنع الحقن
📝 تمارين
-
⭐ اكتب دالة
fn_get_product_price(p_id INT)تستخدم SELECT INTO للاستعلام عنunit_priceمن جدولproductsوترجع 0 إذا لم يوجد صف. -
⭐⭐ اكتب دالة
fn_customer_tier(p_customer_id INT)تستعلم عن إجمالي إنفاق العميل وترجع فئة PLATINUM/GOLD/SILVER/BRONZE باستخدام IF/ELSIF (العتبات: 10000 / 5000 / 1000 دولار). -
⭐⭐⭐ اكتب إجراء مخزن
sp_monthly_revenue_report(p_year INT, p_month INT): استعلم ديناميكيًا عن ملخص طلبات ذلك الشهر، INSERT في جدولmonthly_reports، التقط الأخطاء بـ EXCEPTION واكتبها إلىerror_log، أخرج التقدم بـ RAISE NOTICE، وابنِ اسم الجدول ديناميكيًا بـ format (مثلاً،orders_y2025m06).