PostgreSQL: ممارسة شاملة للميزات المتقدمة في PostgreSQL

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

1. ما ستتعلمه


2. القصة

أليس مهندسة قواعد بيانات في شركة SaaS. يحتاج قسم الموارد البشرية منها بناء قاعدة البيانات لنظام إدارة الموظفين. المتطلبات هي:

  1. إحصائيات الحضور: أيام الحضور الشهرية وعدد مرات التأخير وساعات العمل الإضافية لكل موظف، مرتبة باستخدام الدوال النافذية
  2. ترتيب الأداء: درجة مركبة من تقييمات متعددة الأبعاد، مخزنة مؤقتاً في عرض مجسّد
  3. علامات المهارات: علامات مهارات كل موظف غير ثابتة، لذا يتم تخزينها بشكل مرن في JSONB
  4. بحث الموظفين: البحث السريع عن الموظفين بالاسم أو المهارة أو القسم وغيرها باستخدام البحث النصي الكامل

تحتاج أليس إلى الجمع بين الدوال النافذية والعرض والفهارس وJSONB والبحث النصي الكامل لإنجاز هذا المشروع.


3. المفهوم: تصميم قاعدة بيانات المشروع

(1) مخطط ER

100%
erDiagram
    EMPLOYEES ||--o{ ATTENDANCE : has
    EMPLOYEES ||--o{ PERFORMANCE : receives
    EMPLOYEES }o--|| DEPARTMENTS : belongs_to

    EMPLOYEES {
        int employee_id PK
        text name
        int department_id FK
        text position
        date hire_date
        jsonb skills
        tsvector search_doc
    }

    DEPARTMENTS {
        int department_id PK
        text dept_name
        text location
    }

    ATTENDANCE {
        int attendance_id PK
        int employee_id FK
        date attend_date
        text status
        time check_in_time
        time check_out_time
        numeric overtime_hours
    }

    PERFORMANCE {
        int perf_id PK
        int employee_id FK
        int year
        int quarter
        numeric technical_score
        numeric communication_score
        numeric leadership_score
        numeric overall_score
    }

(1) ▶ مثال

SQL
CREATE TABLE departments (
  department_id SERIAL PRIMARY KEY,
  dept_name TEXT NOT NULL,
  location TEXT NOT NULL
);

CREATE TABLE employees (
  employee_id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  department_id INT NOT NULL REFERENCES departments(department_id),
  position TEXT NOT NULL,
  hire_date DATE NOT NULL DEFAULT CURRENT_DATE,
  skills JSONB NOT NULL DEFAULT '[]',
  search_doc tsvector
);

CREATE TABLE attendance (
  attendance_id SERIAL PRIMARY KEY,
  employee_id INT NOT NULL REFERENCES employees(employee_id),
  attend_date DATE NOT NULL,
  status TEXT NOT NULL CHECK (status IN ('present','late','absent','leave')),
  check_in_time TIME,
  check_out_time TIME,
  overtime_hours NUMERIC(4,2) DEFAULT 0
);

CREATE TABLE performance (
  perf_id SERIAL PRIMARY KEY,
  employee_id INT NOT NULL REFERENCES employees(employee_id),
  year INT NOT NULL,
  quarter INT NOT NULL CHECK (quarter BETWEEN 1 AND 4),
  technical_score NUMERIC(5,2) CHECK (technical_score BETWEEN 0 AND 100),
  communication_score NUMERIC(5,2) CHECK (communication_score BETWEEN 0 AND 100),
  leadership_score NUMERIC(5,2) CHECK (leadership_score BETWEEN 0 AND 100),
  overall_score NUMERIC(5,2) CHECK (overall_score BETWEEN 0 AND 100),
  UNIQUE (employee_id, year, quarter)
);

النتيجة:

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

(2) إدراج بيانات الاختبار

(2) ▶ مثال

SQL
INSERT INTO departments (dept_name, location) VALUES
  ('Engineering', 'Floor 3'),
  ('Sales', 'Floor 2'),
  ('Marketing', 'Floor 1'),
  ('HR', 'Floor 1');

INSERT INTO employees (name, department_id, position, hire_date, skills) VALUES
  ('Alice Chen', 1, 'Senior Engineer', '2022-03-15',
   '["PostgreSQL","Python","Docker","Kubernetes"]'),
  ('Bob Wang', 1, 'Junior Engineer', '2023-06-01',
   '["Java","Spring","MySQL"]'),
  ('Charlie Zhang', 2, 'Sales Manager', '2021-01-10',
   '["Negotiation","CRM","English","French"]'),
  ('Diana Liu', 3, 'Marketing Specialist', '2023-09-20',
   '["SEO","Content Writing","Google Analytics"]'),
  ('Edward Wu', 1, 'DevOps Engineer', '2022-11-01',
   '["Docker","Kubernetes","AWS","Terraform"]'),
  ('Fiona Li', 4, 'HR Manager', '2020-05-15',
   '["Recruiting","Employee Relations","Payroll"]');

النتيجة:

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

(3) ▶ مثال

SQL
INSERT INTO attendance (employee_id, attend_date, status, check_in_time, check_out_time, overtime_hours)
SELECT
  e.employee_id,
  d.dt::date,
  CASE WHEN random() < 0.05 THEN 'absent'
       WHEN random() < 0.12 THEN 'late'
       ELSE 'present'
  END,
  CASE WHEN random() < 0.12 THEN '09:15' ELSE '08:55' END,
  CASE WHEN random() < 0.15 THEN '19:00' ELSE '18:00' END,
  CASE WHEN random() < 0.15 THEN 1.0 ELSE 0 END
FROM employees e
CROSS JOIN (
  SELECT generate_series('2025-01-01'::timestamp, '2025-03-31'::timestamp, '1 day') AS dt
) d
WHERE EXTRACT(dow FROM d.dt) NOT IN (0, 6);

النتيجة:

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

(4) ▶ مثال

SQL
INSERT INTO performance (employee_id, year, quarter, technical_score, communication_score, leadership_score, overall_score)
VALUES
  (1, 2025, 1, 92, 85, 88, 89.0),
  (2, 2025, 1, 78, 72, 65, 72.3),
  (3, 2025, 1, 70, 90, 85, 82.0),
  (4, 2025, 1, 75, 88, 72, 78.3),
  (5, 2025, 1, 88, 76, 80, 82.0),
  (6, 2025, 1, 65, 92, 90, 82.3);

النتيجة:

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

4. المفهوم: الدوال النافذية — إحصائيات الحضور والترتيب

(1) ملخص الحضور الشهري

(5) ▶ مثال

SQL
SELECT
  e.employee_id,
  e.name,
  d.dept_name,
  TO_CHAR(a.attend_date, 'YYYY-MM') AS month,
  COUNT(*) FILTER (WHERE a.status = 'present') AS present_days,
  COUNT(*) FILTER (WHERE a.status = 'late') AS late_days,
  COUNT(*) FILTER (WHERE a.status = 'absent') AS absent_days,
  SUM(a.overtime_hours) AS total_overtime
FROM employees e
JOIN attendance a ON e.employee_id = a.employee_id
JOIN departments d ON e.department_id = d.department_id
GROUP BY e.employee_id, e.name, d.dept_name, TO_CHAR(a.attend_date, 'YYYY-MM')
ORDER BY e.employee_id, month;

النتيجة:

TEXT 📖 للعرض فقط
 count 
-------
      5
(1 row)
الدالة الوصف
COUNT(*) FILTER (WHERE ...) عدّ مشروط (خاص بـ PostgreSQL، أوضح من SUM(CASE))
TO_CHAR(date, 'YYYY-MM') التجميع حسب الشهر

(2) ترتيب الحضور داخل القسم

(6) ▶ مثال

SQL
SELECT
  e.name,
  d.dept_name,
  COUNT(*) FILTER (WHERE a.status = 'present') AS present_days,
  SUM(a.overtime_hours) AS total_overtime,
  RANK() OVER (PARTITION BY d.dept_name ORDER BY COUNT(*) FILTER (WHERE a.status = 'present') DESC) AS attendance_rank,
  RANK() OVER (PARTITION BY d.dept_name ORDER BY SUM(a.overtime_hours) DESC) AS overtime_rank
FROM employees e
JOIN attendance a ON e.employee_id = a.employee_id
JOIN departments d ON e.department_id = d.department_id
WHERE a.attend_date BETWEEN '2025-01-01' AND '2025-03-31'
GROUP BY e.employee_id, e.name, d.dept_name
ORDER BY d.dept_name, attendance_rank;

النتيجة:

TEXT 📖 للعرض فقط
 count 
-------
      5
(1 row)
الدالة النافذية معالجة القيم المتساوية حالة الاستخدام
RANK() التساوي يتخطى الرتب (1,1,3) ترتيب يسمح بالتساوي
DENSE_RANK() التساوي لا يتخطى الرتب (1,1,2) ترتيب متصل
ROW_NUMBER() لا يسمح بالتساوي أبداً (1,2,3) ترتيب فريد

(7) ▶ مثال

SQL
SELECT
  e.name,
  a.attend_date,
  a.status,
  SUM(CASE WHEN a.status = 'present' THEN 1 ELSE 0 END)
    OVER (PARTITION BY e.employee_id ORDER BY a.attend_date) AS cumulative_present
FROM employees e
JOIN attendance a ON e.employee_id = a.employee_id
WHERE e.employee_id = 1
ORDER BY a.attend_date;

النتيجة:

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

(8) ▶ مثال

SQL
WITH monthly_stats AS (
  SELECT
    e.employee_id,
    e.name,
    TO_CHAR(a.attend_date, 'YYYY-MM') AS month,
    COUNT(*) FILTER (WHERE a.status = 'present') AS present_days
  FROM employees e
  JOIN attendance a ON e.employee_id = a.employee_id
  GROUP BY e.employee_id, e.name, TO_CHAR(a.attend_date, 'YYYY-MM')
)
SELECT
  name,
  month,
  present_days,
  LAG(present_days) OVER (PARTITION BY employee_id ORDER BY month) AS prev_month,
  present_days - LAG(present_days) OVER (PARTITION BY employee_id ORDER BY month) AS diff
FROM monthly_stats
ORDER BY employee_id, month;

النتيجة:

TEXT 📖 للعرض فقط
 count 
-------
      5
(1 row)

5. المفهوم: العرض والعرض المجسّد — تغليف ترتيب الأداء

(1) العرض مقابل العرض المجسّد

البُعد العرض (View) العرض المجسّد (Materialized View)
يخزن البيانات لا؛ يُحسب مباشرة عند كل استعلام نعم؛ يقرأ النتيجة المخزنة مباشرة
حداثة البيانات في الوقت الفعلي يتطلب REFRESH يدوي
أداء الاستعلام نفس أداء الاستعلام الأساسي سريع (مُحسب مسبقاً)
حالة الاستخدام بيانات تتحدث بشكل متكرر التقارير/الإحصائيات، بيانات تتغير نادراً

(9) ▶ مثال

SQL
CREATE VIEW v_performance_ranking AS
SELECT
  e.employee_id,
  e.name,
  d.dept_name,
  e.position,
  p.year,
  p.quarter,
  p.technical_score,
  p.communication_score,
  p.leadership_score,
  p.overall_score,
  RANK() OVER (PARTITION BY d.dept_name ORDER BY p.overall_score DESC) AS dept_rank,
  RANK() OVER (ORDER BY p.overall_score DESC) AS company_rank
FROM employees e
JOIN performance p ON e.employee_id = p.employee_id
JOIN departments d ON e.department_id = d.department_id;

SELECT * FROM v_performance_ranking WHERE quarter = 1 ORDER BY company_rank;

النتيجة:

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

(10) ▶ مثال

SQL
CREATE MATERIALIZED VIEW mv_performance_ranking AS
SELECT
  e.employee_id,
  e.name,
  d.dept_name,
  p.year,
  p.quarter,
  p.overall_score,
  RANK() OVER (ORDER BY p.overall_score DESC) AS company_rank
FROM employees e
JOIN performance p ON e.employee_id = p.employee_id
JOIN departments d ON e.department_id = d.department_id
WITH DATA;

-- تحديث عند تغير بيانات الأداء
REFRESH MATERIALIZED VIEW mv_performance_ranking;

-- تحديث متزامن (لا يحجب عمليات القراءة)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_performance_ranking;

النتيجة:

TEXT 📖 للعرض فقط
CREATE TABLE
طريقة REFRESH القفل الوصف
REFRESH MATERIALIZED VIEW ACCESS EXCLUSIVE يحجب القراءة والكتابة، لكن لا يحتاج فهرس فريد
REFRESH ... CONCURRENTLY SHARE لا يحجب القراءة، يتطلب فهرس فريد

(11) ▶ مثال

SQL
CREATE MATERIALIZED VIEW mv_attendance_summary AS
SELECT
  e.employee_id,
  e.name,
  d.dept_name,
  TO_CHAR(a.attend_date, 'YYYY-MM') AS month,
  COUNT(*) FILTER (WHERE a.status = 'present') AS present_days,
  COUNT(*) FILTER (WHERE a.status = 'late') AS late_days,
  COUNT(*) FILTER (WHERE a.status = 'absent') AS absent_days,
  SUM(a.overtime_hours) AS total_overtime
FROM employees e
JOIN attendance a ON e.employee_id = a.employee_id
JOIN departments d ON e.department_id = d.department_id
GROUP BY e.employee_id, e.name, d.dept_name, TO_CHAR(a.attend_date, 'YYYY-MM')
WITH DATA;

النتيجة:

TEXT 📖 للعرض فقط
 count 
-------
      5
(1 row)

6. المفهوم: تحسين استراتيجية الفهرسة

(1) مبادئ تصميم الفهارس

المبدأ الوصف
فهرسة الأعمدة المستخدمة في شروط WHERE المتكررة حالة الاستخدام الأساسية
فهرسة أعمدة JOIN أعمدة المفاتيح الأجنبية لا تحتوي على فهرس بشكل افتراضي؛ أنشئها يدوياً
تجنب الفهرسة المفرطة كل فهرس يضيف عبئاً على عمليات الكتابة
مراقبة ترتيب أعمدة الفهرس المركب أعمدة شرط المساواة أولاً، أعمدة شرط النطاق أخيراً
التحقق بـ EXPLAIN ANALYZE خطة التنفيذ الفعلية هي الحكم النهائي

(12) ▶ مثال

SQL
-- فهرس لاستعلامات الحضور حسب الموظف ونطاق التاريخ
CREATE INDEX idx_attendance_emp_date
ON attendance (employee_id, attend_date);

-- فهرس لاستعلامات الأداء حسب السنة/الربع
CREATE INDEX idx_performance_emp_quarter
ON performance (employee_id, year, quarter);

-- فهرس للموظفين حسب القسم
CREATE INDEX idx_employees_dept
ON employees (department_id);

-- فهرس جزئي: فهرسة سجلات الحضور غير الغياب فقط
CREATE INDEX idx_attendance_present
ON attendance (employee_id, attend_date)
WHERE status != 'absent';

النتيجة:

TEXT 📖 للعرض فقط
CREATE TABLE
نوع الفهرس الصيغة حالة الاستخدام
B-tree (افتراضي) CREATE INDEX idx ON t(col) المساواة، النطاق، الترتيب
فهرس مركب CREATE INDEX idx ON t(col1, col2) استعلامات متعددة الأعمدة
فهرس جزئي CREATE INDEX idx ON t(col) WHERE ... فهرسة الصفوف التي تفي بشرط فقط
فهرس تعبيري CREATE INDEX idx ON t(lower(col)) استعلامات على نتائج الدوال

(13) ▶ مثال

SQL
-- قبل الفهرس: Seq Scan
EXPLAIN ANALYZE
SELECT * FROM attendance WHERE employee_id = 1 AND attend_date >= '2025-01-01';

-- بعد الفهرس: Index Scan
CREATE INDEX idx_attendance_emp_date ON attendance (employee_id, attend_date);
EXPLAIN ANALYZE
SELECT * FROM attendance WHERE employee_id = 1 AND attend_date >= '2025-01-01';

النتيجة:

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

(14) ▶ مثال

SQL
-- تضمين أعمدة لتجنب الوصول إلى الجدول
CREATE INDEX idx_attendance_covering
ON attendance (employee_id, attend_date)
INCLUDE (status, overtime_hours);

-- هذا الاستعلام يحتاج الفهرس فقط، بدون الوصول إلى الجدول
EXPLAIN ANALYZE
SELECT status, overtime_hours
FROM attendance
WHERE employee_id = 1 AND attend_date >= '2025-01-01';

النتيجة:

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

7. المفهوم: JSONB لتخزين علامات المهارات

(1) تصميم JSONB لعلامات المهارات

(15) ▶ مثال

SQL
-- الاستعلام عن مهارة محددة
SELECT name, skills
FROM employees
WHERE skills @> '["PostgreSQL"]';

-- الاستعلام عن مهارات متعددة (يحتوي على أي منها)
SELECT name, skills
FROM employees
WHERE skills ?| array['PostgreSQL', 'Docker'];

-- الاستعلام عن مهارات متعددة (يحتوي على جميعها)
SELECT name, skills
FROM employees
WHERE skills @> '["Docker", "Kubernetes"]';

النتيجة:

TEXT 📖 للعرض فقط
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(16) ▶ مثال

SQL
SELECT
  e.name,
  jsonb_array_elements_text(e.skills) AS skill
FROM employees e
ORDER BY e.name;

النتيجة:

TEXT 📖 للعرض فقط
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(17) ▶ مثال

SQL
SELECT
  skill,
  COUNT(*) AS employee_count
FROM (
  SELECT jsonb_array_elements_text(skills) AS skill FROM employees
) sub
GROUP BY skill
ORDER BY employee_count DESC;

النتيجة:

TEXT 📖 للعرض فقط
 count 
-------
      5
(1 row)

(2) فهرس JSONB لتسريع استعلامات المهارات

(18) ▶ مثال

SQL
CREATE INDEX idx_employees_skills ON employees USING gin (skills);

-- فهرس GIN يدعم استعلامات @> و ?|
EXPLAIN ANALYZE
SELECT name FROM employees WHERE skills @> '["PostgreSQL"]';

النتيجة:

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

(19) ▶ مثال

SQL
-- الترقية من مصفوفة بسيطة إلى مهارات مهيكلة
ALTER TABLE employees ADD COLUMN skill_profile JSONB DEFAULT '{}';

UPDATE employees SET skill_profile = '{
  "primary": ["PostgreSQL", "Python"],
  "secondary": ["Docker"],
  "certifications": ["AWS Solutions Architect"],
  "years_of_experience": {"PostgreSQL": 5, "Python": 8}
}'::jsonb
WHERE employee_id = 1;

-- الاستعلام حسب المهارة الأساسية
SELECT name, skill_profile -> 'primary' AS primary_skills
FROM employees
WHERE skill_profile -> 'primary' @> '["PostgreSQL"]';

-- الاستعلام حسب الشهادة
SELECT name
FROM employees
WHERE skill_profile -> 'certifications' @> '["AWS Solutions Architect"]';

-- الاستعلام حسب سنوات الخبرة
SELECT name
FROM employees
WHERE (skill_profile -> 'years_of_experience' ->> 'PostgreSQL')::int >= 3;

النتيجة:

TEXT 📖 للعرض فقط
UPDATE 3

8. المفهوم: البحث النصي الكامل للبحث عن الموظفين

(1) بناء مستند البحث tsvector

(20) ▶ مثال

SQL
-- إضافة عمود مستند البحث
ALTER TABLE employees ADD COLUMN search_doc tsvector;

-- إنشاء دالة لبناء مستند البحث
CREATE FUNCTION employees_search_update() RETURNS trigger AS $$
BEGIN
  NEW.search_doc :=
    setweight(to_tsvector('english', COALESCE(NEW.name, '')), 'A') ||
    setweight(to_tsvector('english', COALESCE(NEW.position, '')), 'B') ||
    setweight(to_tsvector('simple', COALESCE(
      array_to_string(
        ARRAY(SELECT jsonb_array_elements_text(NEW.skills)), ' '
      ), '')), 'C');
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- إنشاء المُحفّز
CREATE TRIGGER trg_employees_search
BEFORE INSERT OR UPDATE OF name, position, skills ON employees
FOR EACH ROW EXECUTE FUNCTION employees_search_update();

-- تحديث الصفوف الموجودة
UPDATE employees SET name = name; -- يُفعّل المُحفّز، يملأ search_doc

النتيجة:

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

(21) ▶ مثال

SQL
CREATE INDEX idx_employees_search ON employees USING gin (search_doc);

النتيجة:

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

(2) دالة البحث

(22) ▶ مثال

SQL
CREATE FUNCTION search_employees(p_query TEXT)
RETURNS TABLE (
  employee_id INT,
  name TEXT,
  position TEXT,
  dept_name TEXT,
  skills JSONB,
  rank REAL,
  headline TEXT
) AS $$
BEGIN
  RETURN QUERY
  SELECT
    e.employee_id,
    e.name,
    e.position,
    d.dept_name,
    e.skills,
    ts_rank(e.search_doc, websearch_to_tsquery('english', p_query)) AS rank,
    ts_headline(
      'english',
      COALESCE(e.name, '') || ' ' || COALESCE(e.position, ''),
      websearch_to_tsquery('english', p_query),
      'StartSel=<mark>,StopSel=</mark>'
    ) AS headline
  FROM employees e
  JOIN departments d ON e.department_id = d.department_id
  WHERE e.search_doc @@ websearch_to_tsquery('english', p_query)
  ORDER BY rank DESC;
END;
$$ LANGUAGE plpgsql;

-- اختبار: البحث عن مهندس
SELECT * FROM search_employees('engineer');

-- اختبار: البحث عن مهارات Docker
SELECT * FROM search_employees('Docker');

النتيجة:

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

9. المفهوم: الاستعلام الشامل في التطبيق العملي

(1) تقرير الموظفين متعدد الأبعاد

(23) ▶ مثال

SQL
CREATE VIEW v_employee_dashboard AS
SELECT
  e.employee_id,
  e.name,
  d.dept_name,
  e.position,
  e.hire_date,
  EXTRACT(YEAR FROM age(CURRENT_DATE, e.hire_date)) AS years_of_service,
  e.skills,
  p.overall_score AS latest_perf_score,
  p_r.company_rank,
  a_s.present_days AS q1_present,
  a_s.late_days AS q1_late,
  a_s.total_overtime AS q1_overtime
FROM employees e
JOIN departments d ON e.department_id = d.department_id
LEFT JOIN LATERAL (
  SELECT overall_score FROM performance
  WHERE employee_id = e.employee_id
  ORDER BY year DESC, quarter DESC LIMIT 1
) p ON true
LEFT JOIN LATERAL (
  SELECT company_rank FROM mv_performance_ranking
  WHERE employee_id = e.employee_id
  ORDER BY year DESC, quarter DESC LIMIT 1
) p_r ON true
LEFT JOIN LATERAL (
  SELECT
    COUNT(*) FILTER (WHERE status = 'present') AS present_days,
    COUNT(*) FILTER (WHERE status = 'late') AS late_days,
    SUM(overtime_hours) AS total_overtime
  FROM attendance
  WHERE employee_id = e.employee_id
    AND attend_date BETWEEN '2025-01-01' AND '2025-03-31'
) a_s ON true;

النتيجة:

TEXT 📖 للعرض فقط
 count 
-------
      5
(1 row)

(24) ▶ مثال

SQL
SELECT
  d.dept_name,
  AVG(p.overall_score) AS avg_score,
  MAX(p.overall_score) AS max_score,
  MIN(p.overall_score) AS min_score,
  COUNT(*) AS employee_count,
  RANK() OVER (ORDER BY AVG(p.overall_score) DESC) AS dept_rank
FROM departments d
JOIN employees e ON d.department_id = e.department_id
JOIN performance p ON e.employee_id = p.employee_id
WHERE p.year = 2025 AND p.quarter = 1
GROUP BY d.dept_name
ORDER BY avg_score DESC;

النتيجة:

TEXT 📖 للعرض فقط
 count 
-------
      5
(1 row)

(2) تحليل فجوة المهارات

(25) ▶ مثال

SQL
SELECT
  d.dept_name,
  skill,
  COUNT(*) AS employees_with_skill,
  ROUND(COUNT(*)::numeric / SUM(COUNT(*)) OVER (PARTITION BY d.dept_name) * 100, 1) AS pct
FROM employees e
JOIN departments d ON e.department_id = d.department_id
CROSS JOIN LATERAL jsonb_array_elements_text(e.skills) AS skill
GROUP BY d.dept_name, skill
ORDER BY d.dept_name, employees_with_skill DESC;

النتيجة:

TEXT 📖 للعرض فقط
 count 
-------
      5
(1 row)

10. في التطبيق العملي: قاعدة بيانات كاملة لنظام إدارة الموظفين

تدمج أليس جميع الوحدات في مشروع واحد متكامل.

SQL
-- ============================================
-- نظام إدارة الموظفين - المخطط الكامل
-- ============================================

-- الخطوة 1: الجداول (تم إنشاؤها أعلاه بالفعل)
-- التأكد من وجود جميع الجداول والفهارس والمُحفّزات

-- الخطوة 2: الفهارس الأساسية
CREATE INDEX idx_attendance_emp_date
ON attendance (employee_id, attend_date);
CREATE INDEX idx_performance_emp_quarter
ON performance (employee_id, year, quarter);
CREATE INDEX idx_employees_dept
ON employees (department_id);
CREATE INDEX idx_employees_skills
ON employees USING gin (skills);
CREATE INDEX idx_employees_search
ON employees USING gin (search_doc);

-- الخطوة 3: العروض المجسّدة
CREATE MATERIALIZED VIEW mv_dept_performance AS
SELECT
  d.dept_name,
  p.year,
  p.quarter,
  AVG(p.overall_score) AS avg_score,
  AVG(p.technical_score) AS avg_tech,
  AVG(p.communication_score) AS avg_comm,
  AVG(p.leadership_score) AS avg_lead,
  COUNT(*) AS headcount
FROM departments d
JOIN employees e ON d.department_id = e.department_id
JOIN performance p ON e.employee_id = p.employee_id
GROUP BY d.dept_name, p.year, p.quarter
WITH DATA;

CREATE UNIQUE INDEX idx_mv_dept_perf
ON mv_dept_performance (dept_name, year, quarter);

-- الخطوة 4: دالة تقرير الحضور
CREATE FUNCTION get_attendance_report(
  p_year INT, p_month INT
) RETURNS TABLE (
  employee_id INT, name TEXT, dept_name TEXT,
  present_days BIGINT, late_days BIGINT,
  absent_days BIGINT, overtime_hours NUMERIC,
  attendance_rate NUMERIC, dept_rank BIGINT
) AS $$
BEGIN
  RETURN QUERY
  SELECT
    e.employee_id,
    e.name,
    d.dept_name,
    COUNT(*) FILTER (WHERE a.status = 'present') AS present_days,
    COUNT(*) FILTER (WHERE a.status = 'late') AS late_days,
    COUNT(*) FILTER (WHERE a.status = 'absent') AS absent_days,
    COALESCE(SUM(a.overtime_hours), 0) AS overtime_hours,
    ROUND(
      COUNT(*) FILTER (WHERE a.status IN ('present','late'))::numeric
      / NULLIF(COUNT(*), 0) * 100, 1
    ) AS attendance_rate,
    RANK() OVER (PARTITION BY d.dept_name
      ORDER BY COUNT(*) FILTER (WHERE a.status = 'present') DESC)
  FROM employees e
  JOIN attendance a ON e.employee_id = a.employee_id
  JOIN departments d ON e.department_id = d.department_id
  WHERE EXTRACT(YEAR FROM a.attend_date) = p_year
    AND EXTRACT(MONTH FROM a.attend_date) = p_month
  GROUP BY e.employee_id, e.name, d.dept_name
  ORDER BY d.dept_name, dept_rank;
END;
$$ LANGUAGE plpgsql;

-- الخطوة 5: البحث عن الموظفين (تم إنشاؤه أعلاه بالفعل)

-- الخطوة 6: اختبار النظام الكامل
-- تقرير الحضور لشهر يناير 2025
SELECT * FROM get_attendance_report(2025, 1);

-- مقارنة أداء الأقسام
SELECT * FROM mv_dept_performance
WHERE year = 2025 AND quarter = 1
ORDER BY avg_score DESC;

-- البحث عن موظفين بمهارات Docker و Kubernetes
SELECT name, position, skills
FROM employees
WHERE skills @> '["Docker","Kubernetes"]';

-- البحث النصي الكامل عن "مهندس"
SELECT name, position, rank
FROM search_employees('engineer');

-- تحديث العرض المجسّد عند تغير البيانات
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_dept_performance;
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_performance_ranking;

❓ أسئلة شائعة

س متى يجب استخدام REFRESH CONCURRENTLY على العرض المجسّد؟
ج استخدم CONCURRENTLY عندما يُستعلم العرض المجسّد بشكل متكرر وحجب القراءة غير مقبول. يتطلب فهرس فريد واحد على الأقل على العرض المجسّد. إذا كانت فترة عدم التوفر القصيرة مقبولة، فإن REFRESH العادي أسرع.
س أيهما أداء أفضل، صيغة FILTER أم CASE WHEN؟
ج الأداء متساوٍ تقريباً — PostgreSQL يحوّل FILTER داخلياً إلى خطة تنفيذ معادلة لـ CASE WHEN. صيغة FILTER أوضح وأكثر قابلية للقراءة، لذا يُنصح بها.
س ما الفرق بين LATERAL JOIN والاستعلام الفرعي؟
ج LATERAL يسمح للاستعلام الفرعي بالرجوع إلى أعمدة من الاستعلام الخارجي، مثل الاستعلام الفرعي المرتبط لكنه يمكنه إرجاع صفوف متعددة. الاستعلام الفرعي العادي لا يمكنه الرجوع إلى أعمدة خارجية. LATERAL يناسب سيناريو "حساب نتيجة واحدة لكل صف خارجي."
س مصفوفة مهارات JSONB مقابل جدول ربط — أيهما أفضل؟
ج إذا كانت المهارات تحتاج إلى إدارة مستقلة (إضافة/حذف/تعديل، إحصائيات، استعلامات علائقية)، فإن جدول الربط (employee_skills) أفضل. إذا كانت المهارات مجرد سمات تشبه العلامات مع أنماط استعلام بسيطة، فإن JSONB أكثر مرونة. في هذا المشروع المهارات تشبه العلامات، لذا JSONB أنسب.
س لماذا استخدام إعداد english بدلاً من simple عند البحث عن مهارة Docker؟
ج إعداد english يقوم بتجذيع كلمات البحث، لكن "Docker" اسم علم لذا ليس لتجذيع أي تأثير. إعداد simple سيعمل أيضاً، لكن إذا احتوى مصطلح البحث على كلمات إنجليزية شائعة (مثلاً "running")، فإن تجذيع إعداد english يعمل بشكل أفضل. يستخدم هذا المشروع إعداد english بشكل موحد.
س كيف أقوم بتحديث العرض المجسّد تلقائياً وفق جدول زمني؟
ج PostgreSQL لا يحتوي على تحديث تلقائي مدمج. يمكنك استخدام امتداد pg_cron لتشغيل REFRESH وفق جدول، أو استخدام cron/جدول المهام في نظام التشغيل لاستدعاء أمر psql. على سبيل المثال، تحديث يومي: SELECT cron.schedule('0 2 * * *', $$REFRESH MATERIALIZED VIEW CONCURRENTLY mv_dept_performance$$).
س ما القاعدة لترتيب أعمدة الفهرس المركب؟
ج أعمدة استعلام المساواة توضع أولاً، أعمدة استعلام النطاق توضع أخيراً. على سبيل المثال، (employee_id, attend_date): التصفية أولاً حسب employee_id بالمساواة، ثم المسح النطاقي حسب attend_date. الترتيب الخاطئ قد يمنع استخدام الفهرس.

📖 ملخص


📝 تمارين

  1. ⭐ استخدم الدوال النافذية للاستعلام عن أيام حضور كل موظف في الربع الأول، وترتيب حضوره على مستوى الشركة (DENSE_RANK)، وكم يوماً أقل من الموظف الذي يرتبة مباشرة أعلاه (باستخدام دالة LAG).

  2. ⭐⭐ أنشئ عرضاً مجسّداً mv_skill_gap يعرض المهارات التي يفتقر إليها كل قسم (مهارات موجودة في أقسام أخرى لكن مفقودة من هذا القسم). أنشئ فهرساً فريداً، حدّثه بـ CONCURRENTLY، واستخدم EXPLAIN ANALYZE لمقارنة أداء الاستعلام قبل وبعد التحديث.

  3. ⭐⭐⭐ صمم حلاً كاملاً لنظام إدارة الموظفين: أنشئ جدول training_records (employee_id، course_name، completion_date، score، tags JSONB)، واكتب دالة recommend_training(p_employee_id INT) توصي بدورات تدريبية مفقودة بناءً على مهارات الموظف الحالية (employees.skills) والتدريب الموجود (training_records.tags). استخدم البحث النصي الكامل لمطابقة أوصاف الدورات، مع إرجاع التوصيات مرتبة حسب الصلة.

Web-Tutorial.com

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

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

100%