PostgreSQL: ممارسة شاملة للميزات المتقدمة في PostgreSQL
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- استخدام الدوال النافذية لإحصائيات الحضور والترتيب
- استخدام العرض والعرض المجسّد لتغليف الاستعلامات المعقدة
- تصميم استراتيجيات الفهرسة لتحسين أداء الاستعلامات
- استخدام JSONB لتخزين علامات مهارات الموظفين بشكل مرن
- دمج البحث النصي الكامل للبحث عن الموظفين
- إكمال مشروع قاعدة بيانات كامل لنظام إدارة الموظفين
2. القصة
أليس مهندسة قواعد بيانات في شركة SaaS. يحتاج قسم الموارد البشرية منها بناء قاعدة البيانات لنظام إدارة الموظفين. المتطلبات هي:
- إحصائيات الحضور: أيام الحضور الشهرية وعدد مرات التأخير وساعات العمل الإضافية لكل موظف، مرتبة باستخدام الدوال النافذية
- ترتيب الأداء: درجة مركبة من تقييمات متعددة الأبعاد، مخزنة مؤقتاً في عرض مجسّد
- علامات المهارات: علامات مهارات كل موظف غير ثابتة، لذا يتم تخزينها بشكل مرن في JSONB
- بحث الموظفين: البحث السريع عن الموظفين بالاسم أو المهارة أو القسم وغيرها باستخدام البحث النصي الكامل
تحتاج أليس إلى الجمع بين الدوال النافذية والعرض والفهارس وJSONB والبحث النصي الكامل لإنجاز هذا المشروع.
3. المفهوم: تصميم قاعدة بيانات المشروع
(1) مخطط ER
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) ▶ مثال
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)
);
النتيجة:
CREATE TABLE
(2) إدراج بيانات الاختبار
(2) ▶ مثال
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"]');
النتيجة:
INSERT 0 1
(3) ▶ مثال
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);
النتيجة:
INSERT 0 1
(4) ▶ مثال
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);
النتيجة:
INSERT 0 1
4. المفهوم: الدوال النافذية — إحصائيات الحضور والترتيب
(1) ملخص الحضور الشهري
(5) ▶ مثال
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;
النتيجة:
count
-------
5
(1 row)
| الدالة | الوصف |
|---|---|
COUNT(*) FILTER (WHERE ...) |
عدّ مشروط (خاص بـ PostgreSQL، أوضح من SUM(CASE)) |
TO_CHAR(date, 'YYYY-MM') |
التجميع حسب الشهر |
(2) ترتيب الحضور داخل القسم
(6) ▶ مثال
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;
النتيجة:
count
-------
5
(1 row)
| الدالة النافذية | معالجة القيم المتساوية | حالة الاستخدام |
|---|---|---|
RANK() |
التساوي يتخطى الرتب (1,1,3) | ترتيب يسمح بالتساوي |
DENSE_RANK() |
التساوي لا يتخطى الرتب (1,1,2) | ترتيب متصل |
ROW_NUMBER() |
لا يسمح بالتساوي أبداً (1,2,3) | ترتيب فريد |
(7) ▶ مثال
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;
النتيجة:
result
----------
42.50
(1 row)
(8) ▶ مثال
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;
النتيجة:
count
-------
5
(1 row)
5. المفهوم: العرض والعرض المجسّد — تغليف ترتيب الأداء
(1) العرض مقابل العرض المجسّد
| البُعد | العرض (View) | العرض المجسّد (Materialized View) |
|---|---|---|
| يخزن البيانات | لا؛ يُحسب مباشرة عند كل استعلام | نعم؛ يقرأ النتيجة المخزنة مباشرة |
| حداثة البيانات | في الوقت الفعلي | يتطلب REFRESH يدوي |
| أداء الاستعلام | نفس أداء الاستعلام الأساسي | سريع (مُحسب مسبقاً) |
| حالة الاستخدام | بيانات تتحدث بشكل متكرر | التقارير/الإحصائيات، بيانات تتغير نادراً |
(9) ▶ مثال
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;
النتيجة:
CREATE TABLE
(10) ▶ مثال
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;
النتيجة:
CREATE TABLE
| طريقة REFRESH | القفل | الوصف |
|---|---|---|
REFRESH MATERIALIZED VIEW |
ACCESS EXCLUSIVE | يحجب القراءة والكتابة، لكن لا يحتاج فهرس فريد |
REFRESH ... CONCURRENTLY |
SHARE | لا يحجب القراءة، يتطلب فهرس فريد |
(11) ▶ مثال
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;
النتيجة:
count
-------
5
(1 row)
6. المفهوم: تحسين استراتيجية الفهرسة
(1) مبادئ تصميم الفهارس
| المبدأ | الوصف |
|---|---|
| فهرسة الأعمدة المستخدمة في شروط WHERE المتكررة | حالة الاستخدام الأساسية |
| فهرسة أعمدة JOIN | أعمدة المفاتيح الأجنبية لا تحتوي على فهرس بشكل افتراضي؛ أنشئها يدوياً |
| تجنب الفهرسة المفرطة | كل فهرس يضيف عبئاً على عمليات الكتابة |
| مراقبة ترتيب أعمدة الفهرس المركب | أعمدة شرط المساواة أولاً، أعمدة شرط النطاق أخيراً |
| التحقق بـ EXPLAIN ANALYZE | خطة التنفيذ الفعلية هي الحكم النهائي |
(12) ▶ مثال
-- فهرس لاستعلامات الحضور حسب الموظف ونطاق التاريخ
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';
النتيجة:
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) ▶ مثال
-- قبل الفهرس: 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';
النتيجة:
CREATE TABLE
(14) ▶ مثال
-- تضمين أعمدة لتجنب الوصول إلى الجدول
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';
النتيجة:
CREATE TABLE
7. المفهوم: JSONB لتخزين علامات المهارات
(1) تصميم JSONB لعلامات المهارات
(15) ▶ مثال
-- الاستعلام عن مهارة محددة
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"]';
النتيجة:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(16) ▶ مثال
SELECT
e.name,
jsonb_array_elements_text(e.skills) AS skill
FROM employees e
ORDER BY e.name;
النتيجة:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(17) ▶ مثال
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;
النتيجة:
count
-------
5
(1 row)
(2) فهرس JSONB لتسريع استعلامات المهارات
(18) ▶ مثال
CREATE INDEX idx_employees_skills ON employees USING gin (skills);
-- فهرس GIN يدعم استعلامات @> و ?|
EXPLAIN ANALYZE
SELECT name FROM employees WHERE skills @> '["PostgreSQL"]';
النتيجة:
CREATE TABLE
(19) ▶ مثال
-- الترقية من مصفوفة بسيطة إلى مهارات مهيكلة
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;
النتيجة:
UPDATE 3
8. المفهوم: البحث النصي الكامل للبحث عن الموظفين
(1) بناء مستند البحث tsvector
(20) ▶ مثال
-- إضافة عمود مستند البحث
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
النتيجة:
INSERT 0 1
(21) ▶ مثال
CREATE INDEX idx_employees_search ON employees USING gin (search_doc);
النتيجة:
CREATE TABLE
(2) دالة البحث
(22) ▶ مثال
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');
النتيجة:
CREATE TABLE
9. المفهوم: الاستعلام الشامل في التطبيق العملي
(1) تقرير الموظفين متعدد الأبعاد
(23) ▶ مثال
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;
النتيجة:
count
-------
5
(1 row)
(24) ▶ مثال
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;
النتيجة:
count
-------
5
(1 row)
(2) تحليل فجوة المهارات
(25) ▶ مثال
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;
النتيجة:
count
-------
5
(1 row)
10. في التطبيق العملي: قاعدة بيانات كاملة لنظام إدارة الموظفين
تدمج أليس جميع الوحدات في مشروع واحد متكامل.
-- ============================================
-- نظام إدارة الموظفين - المخطط الكامل
-- ============================================
-- الخطوة 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;
❓ أسئلة شائعة
SELECT cron.schedule('0 2 * * *', $$REFRESH MATERIALIZED VIEW CONCURRENTLY mv_dept_performance$$).📖 ملخص
- الدوال النافذية تدعم إحصائيات الحضور والترتيب والمقارنة بين الفترات (LAG/LEAD) والمجاميع المتجمعة
- العرض يغلف الاستعلامات المعقدة؛ العرض المجسّد يخزن النتائج مؤقتاً؛ التحديث بـ CONCURRENTLY لا يحجب القراءة
- استراتيجية الفهرسة: فهرسة أعمدة WHERE/JOIN المتكررة، مراقبة ترتيب أعمدة الفهرس المركب، الفهارس الجزئية تقلل عبء الصيانة
- JSONB يخزن علامات المهارات بشكل مرن؛ فهرس GIN يسرّع استعلامات الاحتواء؛
@>هو الاستعلام الأكثر شيوعاً - البحث النصي الكامل يستخدم
setweightلترجيح الحقول والمُحفّزات للمزامنة التلقائية لـ tsvector - المشروع الشامل يدمج ميزات متعددة: الدوال النافذية + العروض المجسّدة + JSONB + البحث النصي الكامل + تحسين الفهارس
📝 تمارين
-
⭐ استخدم الدوال النافذية للاستعلام عن أيام حضور كل موظف في الربع الأول، وترتيب حضوره على مستوى الشركة (DENSE_RANK)، وكم يوماً أقل من الموظف الذي يرتبة مباشرة أعلاه (باستخدام دالة LAG).
-
⭐⭐ أنشئ عرضاً مجسّداً
mv_skill_gapيعرض المهارات التي يفتقر إليها كل قسم (مهارات موجودة في أقسام أخرى لكن مفقودة من هذا القسم). أنشئ فهرساً فريداً، حدّثه بـ CONCURRENTLY، واستخدم EXPLAIN ANALYZE لمقارنة أداء الاستعلام قبل وبعد التحديث. -
⭐⭐⭐ صمم حلاً كاملاً لنظام إدارة الموظفين: أنشئ جدول
training_records(employee_id، course_name، completion_date، score، tags JSONB)، واكتب دالةrecommend_training(p_employee_id INT)توصي بدورات تدريبية مفقودة بناءً على مهارات الموظف الحالية (employees.skills) والتدريب الموجود (training_records.tags). استخدم البحث النصي الكامل لمطابقة أوصاف الدورات، مع إرجاع التوصيات مرتبة حسب الصلة.