الإجراءات المخزنة ودوال SQL
الإجراءات المخزنة ودوال SQL
تشبيه من الحياة الواقعية
تخيّل أنك في بنك:
- الإجراء المخزن مثل "عملية تجارية قياسية" في البنك — يتبع الموظف خطوات محددة مسبقاً، وأنت فقط تقول "أريد إيداع"، وتتم العملية بالكامل تلقائياً.
- الدالة مثل مفتاح الدالة على الآلة الحاسبة — مُدخِلات، تُرجع نتيجة، ويمكن تضمينها في حسابات أكبر.
المفاهيم الأساسية
ما هو الإجراء المخزن
الإجراء المخزن هو مجموعة مُجمَّعة مسبقاً من جمل SQL مخزنة في قاعدة البيانات يمكن أن:
- تقبل مُعامِلات إدخال
- تنفذ منطق أعمال معقد
- تُرجع مجموعات نتائج أو رموز حالة
- تُحسّن الأداء (مُجمَّع مسبقاً، تقليل حركة الشبكة)
صيغة الإجراء المخزن في MySQL
-- الهيكل الأساسي للإجراء المخزن في MySQL
DELIMITER //
CREATE PROCEDURE procedure_name (parameter_list)
BEGIN
-- تعريف المتغيرات
-- منطق الأعمال
-- إرجاع النتائج
END //
DELIMITER ;
صيغة الإجراء المخزن في PostgreSQL
-- الهيكل الأساسي للإجراء المخزن في PostgreSQL
CREATE OR REPLACE FUNCTION procedure_name(parameter_list)
RETURNS return_type AS $$
DECLARE
-- تعريف المتغيرات
BEGIN
-- منطق الأعمال
-- جملة RETURN
END;
$$ LANGUAGE plpgsql;
الدوال المخصصة في SQLite
SQLite لا يدعم الإجراءات المخزنة، لكنه يدعم إنشاء دوال محددة من قبل المستخدم عبر لغات البرمجة:
-- SQLite نفسه لا يدعم CREATE FUNCTION
-- يجب تسجيل الدوال المخصصة عبر Python أو C أو لغات برمجة أخرى
-- مثال Python (باستخدام وحدة sqlite3)
import sqlite3
def calculate_bonus(salary, rate):
return salary * rate
conn = sqlite3.connect('company.db')
conn.create_function('calculate_bonus', 2, calculate_bonus)
-- بعد التسجيل، يمكنك استخدامها في SQL
SELECT name, salary, calculate_bonus(salary, 0.1) AS bonus
FROM employees;
جمل التحكم في التدفق
IF/ELSE منطق شرطي
-- مثال MySQL
DELIMITER //
CREATE PROCEDURE check_salary(IN emp_id INT)
BEGIN
DECLARE emp_salary DECIMAL(10,2);
SELECT salary INTO emp_salary
FROM employees WHERE id = emp_id;
IF emp_salary > 10000 THEN
SELECT 'راتب مرتفع' AS status;
ELSEIF emp_salary > 5000 THEN
SELECT 'راتب متوسط' AS status;
ELSE
SELECT 'راتب منخفض' AS status;
END IF;
END //
DELIMITER ;
WHILE حلقة
-- مثال MySQL: حساب المجموع من 1 إلى N
DELIMITER //
CREATE PROCEDURE calculate_sum(IN n INT, OUT total INT)
BEGIN
DECLARE i INT DEFAULT 1;
SET total = 0;
WHILE i <= n DO
SET total = total + i;
SET i = i + 1;
END WHILE;
END //
DELIMITER ;
LOOP حلقة
-- مثال MySQL: LOOP مع LEAVE
DELIMITER //
CREATE PROCEDURE process_employees()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE emp_name VARCHAR(100);
DECLARE cur CURSOR FOR SELECT name FROM employees;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO emp_name;
IF done THEN
LEAVE read_loop;
END IF;
-- معالجة كل سجل
SELECT CONCAT('معالجة: ', emp_name);
END LOOP;
CLOSE cur;
END //
DELIMITER ;
مفهوم المؤشرات (Cursors)
تُستخدم المؤشرات لمعالجة مجموعات النتائج صف بصف:
-- تدفق استخدام المؤشر الأساسي
-- 1. تعريف المؤشر
DECLARE cur CURSOR FOR SELECT column FROM table;
-- 2. فتح المؤشر
OPEN cur;
-- 3. جلب البيانات
FETCH cur INTO variable;
-- 4. معالجة البيانات (عادةً في حلقة)
-- 5. إغلاق المؤشر
CLOSE cur;
الدوال المخزنة
الدوال المخزنة مشابهة للإجراءات المخزنة لكن لديها اختلافات مهمة:
-- دالة مخزنة في MySQL
DELIMITER //
CREATE FUNCTION get_annual_salary(monthly_salary DECIMAL(10,2))
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
RETURN monthly_salary * 12;
END //
DELIMITER ;
-- استخدام الدالة
SELECT name, get_annual_salary(salary) AS annual_salary
FROM employees;
الإجراءات المخزنة مقابل الدوال
| الميزة | الإجراء المخزن | الدالة المخزنة |
|---|---|---|
| طريقة الاستدعاء | جملة CALL | تُستدعى مباشرة في SQL |
| قيمة الإرجاع | يمكن أن تُرجع عدة مجموعات نتائج | يجب أن تُرجع قيمة واحدة |
| حالة الاستخدام | منطق أعمال معقد | حساب وتحويل |
| التحكم في المعاملات | يمكنها إدارة المعاملات | لا يمكنها إدارة المعاملات |
| في جمل SQL | لا يمكن استخدامها في SELECT | يمكن استخدامها في SELECT |
الأساسيات
إنشاء إجراء مخزن (MySQL)
-- إنشاء إجراء مخزن مع مُعامِلات
DELIMITER //
CREATE PROCEDURE get_employees_by_dept(
IN dept_name VARCHAR(50),
IN min_salary DECIMAL(10,2)
)
BEGIN
SELECT e.name, e.salary, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE d.department_name = dept_name
AND e.salary >= min_salary
ORDER BY e.salary DESC;
END //
DELIMITER ;
-- استدعاء الإجراء المخزن
CALL get_employees_by_dept('Tech', 8000);
إنشاء دالة مخزنة (PostgreSQL)
-- إنشاء دالة لحساب المكافأة
CREATE OR REPLACE FUNCTION calculate_bonus(
base_salary NUMERIC,
performance_rating INTEGER
)
RETURNS NUMERIC AS $$
DECLARE
bonus_rate NUMERIC;
BEGIN
CASE performance_rating
WHEN 1 THEN bonus_rate := 0.10;
WHEN 2 THEN bonus_rate := 0.15;
WHEN 3 THEN bonus_rate := 0.20;
WHEN 4 THEN bonus_rate := 0.25;
WHEN 5 THEN bonus_rate := 0.30;
ELSE bonus_rate := 0.05;
END CASE;
RETURN base_salary * bonus_rate;
END;
$$ LANGUAGE plpgsql;
-- استخدام الدالة
SELECT
name,
salary,
performance_rating,
calculate_bonus(salary, performance_rating) AS bonus
FROM employees;
أمثلة
مثال: إنشاء إجراء مخزن لتعيين موظف جديد (الصعوبة ⭐⭐⭐)
إنشاء إجراء مخزن لمعالجة عملية تعيين موظف جديد، بما في ذلك تعيين رقم موظف، وتحديد الراتب الأولي، وتسجيل معلومات التعيين.
-- نسخة MySQL
DELIMITER //
CREATE PROCEDURE hire_employee(
IN p_name VARCHAR(100),
IN p_department_id INT,
IN p_position VARCHAR(50),
IN p_salary DECIMAL(10,2),
OUT p_employee_id INT,
OUT p_message VARCHAR(200)
)
BEGIN
DECLARE dept_exists INT DEFAULT 0;
DECLARE max_id INT DEFAULT 0;
-- التحقق من وجود القسم
SELECT COUNT(*) INTO dept_exists
FROM departments WHERE id = p_department_id;
IF dept_exists = 0 THEN
SET p_message = 'خطأ: القسم المحدد غير موجود';
SET p_employee_id = -1;
ELSE
-- الحصول على أعلى رقم موظف
SELECT COALESCE(MAX(id), 0) INTO max_id FROM employees;
SET p_employee_id = max_id + 1;
-- إدراج سجل الموظف الجديد
INSERT INTO employees (id, name, department_id, position, salary, hire_date)
VALUES (p_employee_id, p_name, p_department_id, p_position, p_salary, CURDATE());
SET p_message = CONCAT('نجاح: تم تعيين الموظف ', p_name, ' بالرقم ', p_employee_id);
END IF;
END //
DELIMITER ;
-- مثال الاستدعاء
CALL hire_employee('John Doe', 1, 'مهندس مبتدئ', 8000.00, @new_id, @msg);
SELECT @new_id AS employee_id, @msg AS message;
مثال: نسخة PostgreSQL — دالة تعديل راتب الموظف (الصعوبة ⭐⭐⭐)
-- نسخة PostgreSQL
CREATE OR REPLACE FUNCTION adjust_salary(
p_employee_id INTEGER,
p_adjustment_type VARCHAR(10), -- 'RAISE' أو 'REDUCE'
p_percentage NUMERIC
)
RETURNS TABLE(
employee_name VARCHAR,
old_salary NUMERIC,
new_salary NUMERIC,
adjustment_amount NUMERIC
) AS $$
DECLARE
v_old_salary NUMERIC;
v_new_salary NUMERIC;
v_name VARCHAR;
BEGIN
-- الحصول على الراتب الحالي
SELECT name, salary INTO v_name, v_old_salary
FROM employees WHERE id = p_employee_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'رقم الموظف % غير موجود', p_employee_id;
END IF;
-- حساب الراتب الجديد
IF p_adjustment_type = 'RAISE' THEN
v_new_salary := v_old_salary * (1 + p_percentage / 100);
ELSE
v_new_salary := v_old_salary * (1 - p_percentage / 100);
END IF;
-- تحديث قاعدة البيانات
UPDATE employees SET salary = v_new_salary WHERE id = p_employee_id;
-- إرجاع النتيجة
employee_name := v_name;
old_salary := v_old_salary;
new_salary := v_new_salary;
adjustment_amount := v_new_salary - v_old_salary;
RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
-- مثال الاستخدام
SELECT * FROM adjust_salary(1, 'RAISE', 10);
سيناريوهات التطبيق
السيناريو 1: معالجة البيانات دفعة واحدة
-- MySQL: تحديث حالة الطلبات دفعة واحدة
DELIMITER //
CREATE PROCEDURE update_order_status()
BEGIN
DECLARE affected_rows INT DEFAULT 0;
-- بدء المعاملة
START TRANSACTION;
-- إلغاء الطلبات المعلقة لأكثر من 30 يوماً
UPDATE orders
SET status = 'cancelled',
updated_at = NOW()
WHERE status = 'pending'
AND created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
SET affected_rows = ROW_COUNT();
-- تسجيل العملية
INSERT INTO process_log (process_name, affected_rows, executed_at)
VALUES ('update_order_status', affected_rows, NOW());
-- الالتزام بالمعاملة
COMMIT;
SELECT CONCAT('تم إلغاء ', affected_rows, ' طلب منتهي الصلاحية') AS result;
END //
DELIMITER ;
السيناريو 2: إنشاء تقارير معقدة
-- PostgreSQL: إنشاء تقرير رواتب الأقسام
CREATE OR REPLACE FUNCTION generate_salary_report()
RETURNS TABLE(
department_name VARCHAR,
employee_count BIGINT,
avg_salary NUMERIC,
min_salary NUMERIC,
max_salary NUMERIC,
total_salary NUMERIC
) AS $$
BEGIN
RETURN QUERY
SELECT
d.department_name,
COUNT(e.id) AS employee_count,
ROUND(AVG(e.salary), 2) AS avg_salary,
MIN(e.salary) AS min_salary,
MAX(e.salary) AS max_salary,
SUM(e.salary) AS total_salary
FROM departments d
LEFT JOIN employees e ON d.id = e.department_id
GROUP BY d.department_name
ORDER BY total_salary DESC;
END;
$$ LANGUAGE plpgsql;
-- الاستخدام
SELECT * FROM generate_salary_report();
❓ أسئلة شائعة
س: SQLite لا يدعم الإجراءات المخزنة نهائياً — ماذا أفعل؟ ج: SQLite مُصمَّم كقاعدة بيانات خفيفة مدمجة ولا يدعم الإجراءات المخزنة. يمكنك تنفيذ منطق الأعمال باستخدام لغات البرمجة (Python، Java، إلخ)، أو استخدام العروض (VIEW) لتغليف الاستعلامات المعقدة.
س: ما الفرق بين الإجراء المخزن والعرض؟ ج: الععرض هو جدول افتراضي، يُستخدم أساساً لتبسيط الاستعلامات؛ الإجراء المخزن هو برنامج يمكن أن يحتوي على منطق أعمال معقد، تحكم في التدفق، وعمليات تعديل بيانات.
س: متى أستخدم الإجراءات المخزنة؟ ج: استخدمها عندما تحتاج إلى تغليف منطق أعمال معقد، تقليل حركة الشبكة، تحسين الأداء، أو توحيد واجهات الوصول إلى البيانات. الاستعلامات البسيطة أفضل للعروض.
س: كيف أُنقّح إجراءً مخزناً؟ ج: MySQL يمكنه استخدام جمل SELECT لإخراج النتائج الوسيطة؛ PostgreSQL يمكنه استخدام RAISE NOTICE لإخراج معلومات التصحيح؛ يمكنك أيضاً استخدام ميزات التصحيح في أدوات إدارة قواعد البيانات.
📖 ملخص
في هذا الدرس تعلمنا:
- مفاهيم وأغراض الإجراءات المخزنة
- صيغ الإجراءات المخزنة في MySQL و PostgreSQL
- بدائل الدوال المخصصة في SQLite
- جمل التحكم في التدفق (IF/ELSE، WHILE، LOOP)
- مفهوم واستخدام المؤشرات
- الفرق بين الدوال المخزنة والإجراءات المخزنة
- التطبيق في سيناريوهات أعمال حقيقية
📝 تمارين
-
تمرين أساسي: أنشئ إجراءً مخزناً يقبل مُعرِّف قسم كمعامل ويرجع أعلى 3 موظفين راتباً في ذلك القسم.
-
تمرين متوسط: أنشئ دالة تحسب بدل الأقدمية بناءً على سنوات خدمة الموظف (من hire_date إلى التاريخ الحالي):
- 1-3 سنوات: 5% من الراتب الأساسي
- 3-5 سنوات: 10% من الراتب الأساسي
- أكثر من 5 سنوات: 15% من الراتب الأساسي
-
سؤال تفكير: قارن بين إيجابيات وسلبيات معالجة منطق الأعمال في كود طبقة التطبيق مقابل الإجراءات المخزنة في قاعدة البيانات.
الدرس التالي → 23-triggers-events.md



