PostgreSQL: مشروع شامل

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

1. ما ستتعلمه


2. القصة

يطلق Bob و Alice منصة تجارة إلكترونية عبر الحدود لمنطقة الشرق الأوسط. المتطلبات معقدة: بحث نصي كامل بالعربية، سمات منتجات ديناميكية بـ JSONB، طلبات مجزأة شهرياً، أمان على مستوى الصفوف لعزل المستأجرين، وتوصيات منتجات بالذكاء الاصطناعي. يحلون كل ذلك بـ PostgreSQL في مكان واحد — tsvector للبحث العربي، jsonb للـ SKUs المرنة، التجزئة التعريفية لمئات الملايين من الطلبات، pgvector للتوصيات الدلالية، و postgres_fdw لنقل البيانات من PG القديم. قاعدة بيانات PG واحدة تستبدل MySQL + Elasticsearch + Redis + محرك توصيات.


3. المفهوم: تحليل المتطلبات وتصميم البنية

(1) تفصيل وحدات الأعمال

الوحدة الجداول الأساسية الميزات الرئيسية
نظام المستخدمين users, user_addresses أمان RLS على مستوى الصفوف، كلمات مرور bcrypt
الفئات والمنتجات categories, products سمات ديناميكية JSONB، بحث نصي كامل
الطلبات وعناصر الطلبات orders, order_items تجزئة RANGE شهرية
سلة التسوق cart_items UPSERT عالي التزامن
المدفوعات payments أنواع Enum، سجل تدقيق
تتبع الشحن shipments, shipment_events بيانات سلاسل زمنية، أحداث JSONB
التقييمات والمراجعات reviews تجميع النجوم، فهرس GIN
تقارير البيانات mv_daily_sales، إلخ طرق عرض مادية، تحديث مجدول

(2) مخطط ER الكامل

100%
erDiagram
    USERS ||--o{ USER_ADDRESSES : "لديه"
    USERS ||--o{ ORDERS : "يقدم"
    USERS ||--o{ CART_ITEMS : "لديه"
    USERS ||--o{ REVIEWS : "يكتب"
    CATEGORIES ||--o{ CATEGORIES : "أصل"
    CATEGORIES ||--o{ PRODUCTS : "يحتوي"
    PRODUCTS ||--o{ ORDER_ITEMS : "مضمن_في"
    PRODUCTS ||--o{ CART_ITEMS : "مضاف_إلى"
    PRODUCTS ||--o{ REVIEWS : "مُراجع_في"
    ORDERS ||--o{ ORDER_ITEMS : "يحتوي"
    ORDERS ||--o{ PAYMENTS : "مدفوع_بواسطة"
    ORDERS ||--o{ SHIPMENTS : "مشحون_عبر"
    SHIPMENTS ||--o{ SHIPMENT_EVENTS : "مُتَتَبَّع_بواسطة"

    USERS {
        bigint id PK
        text email UK
        text password_hash
        text role
        timestamptz created_at
    }
    USER_ADDRESSES {
        bigint id PK
        bigint user_id FK
        text address_line
        text city
        text country
    }
    CATEGORIES {
        int id PK
        text name
        int parent_id FK
        int sort_order
    }
    PRODUCTS {
        bigint id PK
        text name
        text name_ar
        int category_id FK
        numeric price
        jsonb attributes
        tsvector search_vector
        vector embedding
    }
    ORDERS {
        bigint id PK
        bigint user_id FK
        date order_date
        numeric total_amount
        text status
    }
    ORDER_ITEMS {
        bigint id PK
        bigint order_id FK
        bigint product_id FK
        int quantity
        numeric unit_price
    }
    CART_ITEMS {
        bigint id PK
        bigint user_id FK
        bigint product_id FK
        int quantity
    }
    PAYMENTS {
        bigint id PK
        bigint order_id FK
        text method
        numeric amount
        text status
        timestamptz paid_at
    }
    SHIPMENTS {
        bigint id PK
        bigint order_id FK
        text carrier
        text tracking_code
        text status
    }
    SHIPMENT_EVENTS {
        bigint id PK
        bigint shipment_id FK
        text event_type
        jsonb metadata
        timestamptz event_time
    }
    REVIEWS {
        bigint id PK
        bigint user_id FK
        bigint product_id FK
        int rating
        text comment
    }

(3) تقرير مقارنة PostgreSQL مقابل MySQL

البعد PostgreSQL MySQL
سمات JSONB الديناميكية jsonb أصلي + فهرس GIN + معاملات نوع JSON، لكن الفهرسة ضعيفة
البحث النصي الكامل tsvector/tsquery مدمج، متعدد اللغات لا دعم أصلي، يحتاج Elasticsearch
البحث المتجهي امتداد pgvector أصلي يحتاج خدمة خارجية
التجزئة RANGE/LIST/HASH تعريفية 8.0+ تدعم، لكن أضعف
أمان مستو�� الصفوف سياسات RLS غير مدعوم
طرق العرض المادية دعم أصلي + تحديث مجدول غير مدعوم
منظومة الامتدادات غنية (PostGIS/pgcrypto/FDW) ملحقات أقل
الاستعلامات المعقدة دوال النوافذ/CTE/LATERAL 8.0+ تدعم تدريجياً
النضج التشغيلي عالي، autovacuum/PITR عالي، نسخ متماثل رئيسي-تابع ناضج
نشاط المجتمع الأسرع نمواً أكبر قاعدة مستخدمين

الخلاصة: هذا المشروع يحتاج سمات JSONB ديناميكية، بحث نصي كامل، بحث متجهي، RLS، تجزئة، وطرق عرض مادية — PostgreSQL تدعمها جميعاً بشكل أصلي، بينما MySQL ستحتاج 4+ مكونات وسيطة خارجية، لذا PostgreSQL هو الاختيار.


4. التنفيذ: الوحدة 1 — نظام المستخدمين

(1) جداول المستخدمين والعناوين

SQL
CREATE TABLE users (
    id            BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email         TEXT NOT NULL UNIQUE,
    password_hash TEXT NOT NULL,
    role          TEXT NOT NULL DEFAULT 'customer'
                    CHECK (role IN ('customer','vendor','admin')),
    tenant_id     BIGINT DEFAULT 1,
    created_at    TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE user_addresses (
    id           BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id      BIGINT NOT NULL REFERENCES users(id),
    address_line TEXT NOT NULL,
    city         TEXT NOT NULL,
    country      TEXT NOT NULL,
    is_default   BOOLEAN DEFAULT false
);

(1) ▶ مثال

SQL
-- تفعيل RLS على جدول users
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

-- عزل المستأجرين: المستخدمون يرون فقط نفس المستأجر
CREATE POLICY tenant_isolation ON users
    USING (tenant_id = current_setting('app.tenant_id')::bigint);

-- المسؤول يمكنه رؤية الكل
CREATE POLICY admin_all_access ON users
    USING (role = 'admin');

-- تعيين سياق المستأجر لكل جلسة
SET app.tenant_id = '1';
SELECT * FROM users;

Output:

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

(2) ▶ مثال

SQL
CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- تسجيل بتشفير bcrypt
INSERT INTO users (email, password_hash, role, tenant_id)
VALUES (
    'alice@example.com',
    crypt('SecurePass123', gen_salt('bf')),
    'customer',
    1
);

-- التحقق من تسجيل الدخول
SELECT id, role FROM users
WHERE email = 'alice@example.com'
  AND password_hash = crypt('SecurePass123', password_hash);

Output:

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

5. التنفيذ: الوحدة 2 — الفئات والمنتجات

(3) ▶ مثال

SQL
CREATE TABLE categories (
    id          SERIAL PRIMARY KEY,
    name        TEXT NOT NULL,
    name_ar     TEXT,
    parent_id   INT REFERENCES categories(id),
    sort_order  INT DEFAULT 0
);

INSERT INTO categories (name, name_ar, parent_id, sort_order) VALUES
    ('Electronics', 'إلكترونيات', NULL, 1),
    ('Phones', 'هواتف', 1, 1),
    ('Laptops', 'حاسبات', 1, 2),
    ('Clothing', 'ملابس', NULL, 2);

-- استعلام تكراري: شجرة الفئات
WITH RECURSIVE cat_tree AS (
    SELECT id, name, name_ar, parent_id, 0 AS level
    FROM categories WHERE parent_id IS NULL
    UNION ALL
    SELECT c.id, c.name, c.name_ar, c.parent_id, ct.level + 1
    FROM categories c JOIN cat_tree ct ON c.parent_id = ct.id
)
SELECT repeat('  ', level) || name AS tree, name_ar
FROM cat_tree ORDER BY level, sort_order;

Output:

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

(4) ▶ مثال

SQL
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE products (
    id            BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name          TEXT NOT NULL,
    name_ar       TEXT,
    category_id   INT NOT NULL REFERENCES categories(id),
    price         NUMERIC(12,2) NOT NULL,
    attributes    JSONB DEFAULT '{}',
    search_vector TSVECTOR GENERATED ALWAYS AS (
        setweight(to_tsvector('simple', coalesce(name, '')), 'A') ||
        setweight(to_tsvector('simple', coalesce(name_ar, '')), 'B')
    ) STORED,
    embedding     vector(1536)
);

Output:

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

(5) ▶ مثال

SQL
-- إدراج بسمات ديناميكية
INSERT INTO products (name, name_ar, category_id, price, attributes) VALUES
    ('iPhone 15 Pro', 'آيفون 15 برو', 2, 1199.00,
     '{"color": "titanium", "storage": "256GB", "5g": true}'::jsonb),
    ('MacBook Air M3', 'ماك بوك إير', 3, 1299.00,
     '{"color": "midnight", "ram": "16GB", "screen": "15 inch"}'::jsonb);

-- البحث عن هواتف 5G تحت 1200 دولار
SELECT name, price, attributes->>'storage' AS storage
FROM products
WHERE attributes @> '{"5g": true}'::jsonb
  AND price < 1200;

-- فهرس GIN لاستعلامات احتواء JSONB
CREATE INDEX idx_products_attrs ON products USING GIN (attributes);

Output:

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

(6) ▶ مثال

SQL
-- فهرس GIN للبحث النصي الكامل
CREATE INDEX idx_products_search ON products USING GIN (search_vector);

-- البحث بالإنجليزية أو العربية
SELECT name, name_ar, ts_rank(search_vector, q) AS rank
FROM products, plainto_tsquery('simple', 'iphone') q
WHERE search_vector @@ q
ORDER BY rank DESC;

-- البحث بالعربية
SELECT name, name_ar
FROM products, plainto_tsquery('simple', 'آيفون') q
WHERE search_vector @@ q;

Output:

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

(7) ▶ مثال

SQL
-- فهرس HNSW للبحث المتجهي
CREATE INDEX idx_products_embedding ON products
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

-- البحث عن منتجات متشابهة
SELECT p2.name, p2.price,
       p2.embedding <=> p1.embedding AS distance
FROM products p1
CROSS JOIN LATERAL (
    SELECT * FROM products
    WHERE id != p1.id
    ORDER BY embedding <=> p1.embedding
    LIMIT 3
) p2
WHERE p1.name = 'iPhone 15 Pro';

Output:

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

6. التنفيذ: الوحدة 3 — الطلبات وعناصر الطلبات

(8) ▶ مثال

SQL
CREATE TABLE orders (
    id           BIGINT GENERATED ALWAYS AS IDENTITY,
    user_id      BIGINT NOT NULL REFERENCES users(id),
    order_date   DATE NOT NULL DEFAULT current_date,
    total_amount NUMERIC(12,2) DEFAULT 0,
    status       TEXT DEFAULT 'pending'
                  CHECK (status IN ('pending','paid','shipped','completed','cancelled')),
    created_at   TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (id, order_date)
) PARTITION BY RANGE (order_date);

CREATE TABLE orders_2024_01 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE orders_2024_02 PARTITION OF orders
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
CREATE TABLE orders_2024_03 PARTITION OF orders
    FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
CREATE TABLE orders_default PARTITION OF orders DEFAULT;

CREATE TABLE order_items (
    id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    order_id    BIGINT NOT NULL,
    order_date  DATE NOT NULL,
    product_id  BIGINT NOT NULL REFERENCES products(id),
    quantity    INT NOT NULL CHECK (quantity > 0),
    unit_price  NUMERIC(12,2) NOT NULL,
    FOREIGN KEY (order_id, order_date) REFERENCES orders(id, order_date)
);

CREATE INDEX idx_orders_user ON orders (user_id, order_date);
CREATE INDEX idx_order_items_order ON order_items (order_id, order_date);

Output:

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

(9) ▶ مثال

SQL
-- إنشاء طلب
WITH new_order AS (
    INSERT INTO orders (user_id, order_date, status)
    VALUES (1, '2024-01-15', 'pending')
    RETURNING id, order_date
)
INSERT INTO order_items (order_id, order_date, product_id, quantity, unit_price)
SELECT new_order.id, new_order.order_date, p.id, 2, p.price
FROM new_order, products p
WHERE p.name = 'iPhone 15 Pro';

-- تحديث إجمالي الطلب
UPDATE orders o SET total_amount = (
    SELECT SUM(quantity * unit_price)
    FROM order_items oi
    WHERE oi.order_id = o.id AND oi.order_date = o.order_date
)
WHERE o.id = 1 AND o.order_date = '2024-01-15';

Output:

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

7. التنفيذ: الوحدة 4 — سلة التسوق

(10) ▶ مثال

SQL
CREATE TABLE cart_items (
    id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id    BIGINT NOT NULL REFERENCES users(id),
    product_id BIGINT NOT NULL REFERENCES products(id),
    quantity   INT NOT NULL DEFAULT 1 CHECK (quantity > 0),
    added_at   TIMESTAMPTZ DEFAULT now(),
    UNIQUE (user_id, product_id)
);

-- إضافة أو تحديث عنصر السلة (UPSERT)
INSERT INTO cart_items (user_id, product_id, quantity)
VALUES (1, 1, 1)
ON CONFLICT (user_id, product_id)
DO UPDATE SET quantity = cart_items.quantity + EXCLUDED.quantity;

-- عرض السلة مع تفاصيل المنتج
SELECT p.name, p.price, ci.quantity,
       p.price * ci.quantity AS line_total
FROM cart_items ci
JOIN products p ON ci.product_id = p.id
WHERE ci.user_id = 1;

Output:

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

8. التنفيذ: الوحدة 5 — المدفوعات

(11) ▶ مثال

SQL
CREATE TYPE payment_method AS ENUM ('credit_card', 'paypal', 'bank_transfer', 'cod');
CREATE TYPE payment_status AS ENUM ('pending', 'completed', 'failed', 'refunded');

CREATE TABLE payments (
    id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    order_id   BIGINT NOT NULL,
    order_date DATE NOT NULL,
    method     payment_method NOT NULL,
    amount     NUMERIC(12,2) NOT NULL,
    status     payment_status DEFAULT 'pending',
    paid_at    TIMESTAMPTZ,
    FOREIGN KEY (order_id, order_date) REFERENCES orders(id, order_date)
);

-- تسجيل دفعة ناجحة
INSERT INTO payments (order_id, order_date, method, amount, status, paid_at)
VALUES (1, '2024-01-15', 'credit_card', 2398.00, 'completed', now());

-- تحديث حالة الطلب بعد الدفع
UPDATE orders SET status = 'paid'
WHERE id = 1 AND order_date = '2024-01-15';

Output:

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

9. التنفيذ: الوحدة 6 — تتبع الشحن

(12) ▶ مثال

SQL
CREATE TABLE shipments (
    id            BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    order_id      BIGINT NOT NULL,
    order_date    DATE NOT NULL,
    carrier       TEXT NOT NULL,
    tracking_code TEXT NOT NULL UNIQUE,
    status        TEXT DEFAULT 'created'
                   CHECK (status IN ('created','in_transit','delivered','failed')),
    FOREIGN KEY (order_id, order_date) REFERENCES orders(id, order_date)
);

CREATE TABLE shipment_events (
    id           BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    shipment_id  BIGINT NOT NULL REFERENCES shipments(id),
    event_type   TEXT NOT NULL,
    metadata     JSONB DEFAULT '{}',
    event_time   TIMESTAMPTZ DEFAULT now()
);

-- تتبع الشحنة بالأحداث
INSERT INTO shipments (order_id, order_date, carrier, tracking_code)
VALUES (1, '2024-01-15', 'DHL Express', 'DHL123456789');

INSERT INTO shipment_events (shipment_id, event_type, metadata) VALUES
    (1, 'picked_up',     '{"location": "Dubai Warehouse"}'::jsonb),
    (1, 'in_transit',    '{"location": "Bahrain Hub", "eta": "2024-01-18"}'::jsonb),
    (1, 'out_for_delivery', '{"location": "Riyadh"}'::jsonb);

-- تسلسل زمني للشحنة
SELECT se.event_time, se.event_type, se.metadata->>'location' AS location
FROM shipment_events se
WHERE se.shipment_id = 1
ORDER BY se.event_time;

Output:

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

10. التنفيذ: الوحدة 7 — التقييمات والمراجعات

(13) ▶ مثال

SQL
CREATE TABLE reviews (
    id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id    BIGINT NOT NULL REFERENCES users(id),
    product_id BIGINT NOT NULL REFERENCES products(id),
    rating     INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
    comment    TEXT,
    created_at TIMESTAMPTZ DEFAULT now(),
    UNIQUE (user_id, product_id)
);

INSERT INTO reviews (user_id, product_id, rating, comment) VALUES
    (1, 1, 5, 'Excellent phone, fast delivery'),
    (2, 1, 4, 'Good but expensive'),
    (3, 2, 5, 'Best laptop ever');

-- ملخص تقييم المنتج باستخدام دالة النوافذ
SELECT p.name,
       COUNT(r.id) AS review_count,
       AVG(r.rating)::numeric(3,2) AS avg_rating,
       COUNT(r.id) FILTER (WHERE r.rating = 5) AS five_star,
       COUNT(r.id) FILTER (WHERE r.rating = 4) AS four_star
FROM products p
LEFT JOIN reviews r ON r.product_id = p.id
GROUP BY p.id, p.name;

Output:

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

11. التنفيذ: الوحدة 8 — تقارير البيانات

(14) ▶ مثال

SQL
CREATE MATERIALIZED VIEW mv_daily_sales AS
SELECT order_date,
       COUNT(DISTINCT user_id) AS unique_buyers,
       COUNT(*) AS order_count,
       SUM(total_amount) AS daily_revenue,
       AVG(total_amount)::numeric(12,2) AS avg_order_value
FROM orders
WHERE status = 'completed'
GROUP BY order_date
ORDER BY order_date;

CREATE UNIQUE INDEX idx_mv_daily_sales_date ON mv_daily_sales (order_date);

-- تحديث يومي (يمكن جدولته مع pg_cron)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_sales;

-- أفضل أيام الإيرادات
SELECT order_date, daily_revenue, order_count
FROM mv_daily_sales
ORDER BY daily_revenue DESC
LIMIT 10;

Output:

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

(15) ▶ مثال

SQL
CREATE MATERIALIZED VIEW mv_category_sales AS
SELECT c.name AS category,
       p.name AS product_name,
       SUM(oi.quantity) AS total_sold,
       SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.id
JOIN categories c ON p.category_id = c.id
JOIN orders o ON oi.order_id = o.id AND oi.order_date = o.order_date
WHERE o.status = 'completed'
GROUP BY c.name, p.name
ORDER BY revenue DESC;

REFRESH MATERIALIZED VIEW mv_category_sales;

Output:

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

12. التنفيذ: الإجراءات المخزنة والأتمتة

(16) ▶ مثال

SQL
CREATE OR REPLACE PROCEDURE create_order(
    p_user_id    BIGINT,
    p_items      JSONB
)
LANGUAGE plpgsql AS 
DECLARE
    v_order_id   BIGINT;
    v_order_date DATE := current_date;
    v_item       JSONB;
BEGIN
    INSERT INTO orders (user_id, order_date, status)
    VALUES (p_user_id, v_order_date, 'pending')
    RETURNING id INTO v_order_id;

    FOR v_item IN SELECT * FROM jsonb_array_elements(p_items)
    LOOP
        INSERT INTO order_items (order_id, order_date, product_id, quantity, unit_price)
        VALUES (v_order_id, v_order_date,
                (v_item->>'product_id')::bigint,
                (v_item->>'quantity')::int,
                (SELECT price FROM products WHERE id = (v_item->>'product_id')::bigint));
    END LOOP;

    UPDATE orders SET total_amount = (
        SELECT SUM(quantity * unit_price) FROM order_items
        WHERE order_id = v_order_id AND order_date = v_order_date
    ) WHERE id = v_order_id AND order_date = v_order_date;

    COMMIT;
END;
;

-- استدعاء الإجراء
CALL create_order(1, '[
    {"product_id": 1, "quantity": 1},
    {"product_id": 2, "quantity": 2}
]'::jsonb);

Output:

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

(17) ▶ مثال

SQL
CREATE OR REPLACE FUNCTION maintain_order_partitions()
RETURNS VOID AS 
DECLARE
    v_next_month DATE;
    v_part_name  TEXT;
BEGIN
    v_next_month := date_trunc('month', current_date + interval '1 month')::date;
    v_part_name  := 'orders_' || to_char(v_next_month, 'YYYY_MM');

    IF NOT EXISTS (
        SELECT 1 FROM pg_class WHERE relname = v_part_name
    ) THEN
        EXECUTE format(
            'CREATE TABLE %I PARTITION OF orders
             FOR VALUES FROM (%L) TO (%L)',
            v_part_name,
            v_next_month,
            (v_next_month + interval '1 month')::date
        );
    END IF;

    -- فصل الأجزاء الأقدم من سنتين
    FOR v_part_name IN
        SELECT relname FROM pg_class
        WHERE relname LIKE 'orders_20__%'
          AND relkind = 'r'
          AND relname < 'orders_' || to_char(current_date - interval '2 years', 'YYYY_MM')
    LOOP
        EXECUTE format('ALTER TABLE orders DETACH PARTITION %I', v_part_name);
    END LOOP;
END;
 LANGUAGE plpgsql;

Output:

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

13. التنفيذ: نقل بيانات FDW والنسخ الاحتياطي PITR

(18) ▶ مثال

SQL
CREATE EXTENSION IF NOT EXISTS postgres_fdw;

CREATE SERVER legacy_pg FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host '10.0.1.50', port '5432', dbname 'legacy_shop');

CREATE USER MAPPING FOR current_user SERVER legacy_pg
    OPTIONS (user 'migrate_user', password 'secure_pass');

IMPORT FOREIGN SCHEMA public LIMIT TO (old_users, old_products)
    FROM SERVER legacy_pg INTO legacy;

-- نقل المستخدمين مع إعادة تشفير كلمة المرور
INSERT INTO users (email, password_hash, role, tenant_id, created_at)
SELECT email, crypt(raw_password, gen_salt('bf')), 'customer', 1, created_at
FROM legacy.old_users
ON CONFLICT (email) DO NOTHING;

Output:

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

(19) ▶ مثال

BASH
# نسخ احتياطي أساسي
pg_basebackup -D /backup/base -Ft -z -P

# أرشفة WAL (postgresql.conf)
wal_level = replica
archive_mode = on
archive_command = 'cp %p /backup/wal/%f'

# استعادة إلى نقطة زمنية محددة
pg_restore --target-time='2024-03-15 14:30:00' -d shop_db /backup/base

Output:

TEXT 📖 للعرض فقط
# command executed successfully
الاستراتيجية التكرار الاحتفاظ وقت الاستعادة
pg_basebackup كامل يومي 7 أيام 30-60 دقيقة
أرشفة WAL مستمر 7 أيام أي نقطة زمنية
نسخ احتياطي منطقي pg_dump أسبوعي 4 أسابيع 1-4 ساعات

14. مثال شامل

SQL
-- سكريبت تهيئة كامل لقاعدة بيانات التجارة الإلكترونية
-- الوحدة 1: نظام المستخدمين
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE users (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email TEXT NOT NULL UNIQUE,
    password_hash TEXT NOT NULL,
    role TEXT NOT NULL DEFAULT 'customer'
         CHECK (role IN ('customer','vendor','admin')),
    tenant_id BIGINT DEFAULT 1,
    created_at TIMESTAMPTZ DEFAULT now()
);
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON users
    USING (tenant_id = current_setting('app.tenant_id')::bigint);

CREATE TABLE user_addresses (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    address_line TEXT NOT NULL, city TEXT NOT NULL, country TEXT NOT NULL,
    is_default BOOLEAN DEFAULT false
);

-- الوحدة 2: الفئات والمنتجات
CREATE TABLE categories (
    id SERIAL PRIMARY KEY, name TEXT NOT NULL,
    name_ar TEXT, parent_id INT REFERENCES categories(id),
    sort_order INT DEFAULT 0
);
CREATE TABLE products (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name TEXT NOT NULL, name_ar TEXT,
    category_id INT NOT NULL REFERENCES categories(id),
    price NUMERIC(12,2) NOT NULL,
    attributes JSONB DEFAULT '{}',
    search_vector TSVECTOR GENERATED ALWAYS AS (
        setweight(to_tsvector('simple', coalesce(name,'')), 'A') ||
        setweight(to_tsvector('simple', coalesce(name_ar,'')), 'B')
    ) STORED,
    embedding vector(1536)
);
CREATE INDEX idx_products_attrs ON products USING GIN (attributes);
CREATE INDEX idx_products_search ON products USING GIN (search_vector);
CREATE INDEX idx_products_embedding ON products
    USING hnsw (embedding vector_cosine_ops) WITH (m=16, ef_construction=64);

-- الوحدة 3: الطلبات (مجزأة شهرياً)
CREATE TABLE orders (
    id BIGINT GENERATED ALWAYS AS IDENTITY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    order_date DATE NOT NULL DEFAULT current_date,
    total_amount NUMERIC(12,2) DEFAULT 0,
    status TEXT DEFAULT 'pending'
           CHECK (status IN ('pending','paid','shipped','completed','cancelled')),
    created_at TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (id, order_date)
) PARTITION BY RANGE (order_date);
CREATE TABLE orders_2024_q1 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE orders_default PARTITION OF orders DEFAULT;

CREATE TABLE order_items (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    order_id BIGINT NOT NULL, order_date DATE NOT NULL,
    product_id BIGINT NOT NULL REFERENCES products(id),
    quantity INT NOT NULL CHECK (quantity > 0),
    unit_price NUMERIC(12,2) NOT NULL,
    FOREIGN KEY (order_id, order_date) REFERENCES orders(id, order_date)
);

-- الوحدة 4: سلة التسوق
CREATE TABLE cart_items (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    product_id BIGINT NOT NULL REFERENCES products(id),
    quantity INT NOT NULL DEFAULT 1, added_at TIMESTAMPTZ DEFAULT now(),
    UNIQUE (user_id, product_id)
);

-- الوحدة 5: المدفوعات
CREATE TYPE payment_method AS ENUM ('credit_card','paypal','bank_transfer','cod');
CREATE TYPE payment_status AS ENUM ('pending','completed','failed','refunded');
CREATE TABLE payments (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    order_id BIGINT NOT NULL, order_date DATE NOT NULL,
    method payment_method NOT NULL, amount NUMERIC(12,2) NOT NULL,
    status payment_status DEFAULT 'pending', paid_at TIMESTAMPTZ,
    FOREIGN KEY (order_id, order_date) REFERENCES orders(id, order_date)
);

-- الوحدة 6: الشحن
CREATE TABLE shipments (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    order_id BIGINT NOT NULL, order_date DATE NOT NULL,
    carrier TEXT NOT NULL, tracking_code TEXT NOT NULL UNIQUE,
    status TEXT DEFAULT 'created'
           CHECK (status IN ('created','in_transit','delivered','failed')),
    FOREIGN KEY (order_id, order_date) REFERENCES orders(id, order_date)
);
CREATE TABLE shipment_events (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    shipment_id BIGINT NOT NULL REFERENCES shipments(id),
    event_type TEXT NOT NULL, metadata JSONB DEFAULT '{}',
    event_time TIMESTAMPTZ DEFAULT now()
);

-- الوحدة 7: المراجعات
CREATE TABLE reviews (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    product_id BIGINT NOT NULL REFERENCES products(id),
    rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
    comment TEXT, created_at TIMESTAMPTZ DEFAULT now(),
    UNIQUE (user_id, product_id)
);

-- الوحدة 8: طرق العرض المادية للتقارير
CREATE MATERIALIZED VIEW mv_daily_sales AS
SELECT order_date, COUNT(DISTINCT user_id) AS unique_buyers,
       COUNT(*) AS order_count, SUM(total_amount) AS daily_revenue
FROM orders WHERE status = 'completed'
GROUP BY order_date;
CREATE UNIQUE INDEX idx_mv_daily ON mv_daily_sales (order_date);

❓ أسئلة شائعة

س من أي وحدة يجب أن أبدأ بناء الجداول في المشروع الشامل؟
ج ابدأ بالجداول الأساسية التي ليس لها تبعيات مفاتيح خارجية (users, categories)، ثم ابنِ الجداول التي تشير إليها (products, orders)، وأخيراً الجداول التي تعتمد على الطلبات (payments, shipments). الترتيب الخاطئ سيسبب خطأ بسبب قيود المفاتيح الخارجية.
س كيف أتعامل مع مراجع المفاتيح الخارجية على جدول م��زأ؟
ج المفتاح الخارجي للجدول المجزأ يجب أن يتضمن مفتاح التجزئة. عندما يشير order_items إلى orders، استخدم المفتاح الخارجي المركب (order_id, order_date)، وليس order_id فقط.
س ما الفرق بين RLS وعبارة WHERE العادية؟
ج RLS هي سياسة مفروضة على مستوى قاعدة البيانات وتعمل حتى لو نسي التطبيق التصفية. عبارة WHERE العاد��ة تعتمد على كود التطبيق ويسهل نسيانها. لسيناريوهات تعدد المستأجرين، RLS إلزامية.
س هل كثرة سمات JSONB تؤثر على أداء الاستعلام؟
ج JSONB نفسه لا يؤثر على حد حجم الصف، لكن المزيد من السمات يعني صفاً أكبر و I/O أعلى. ابنِ فهرس GIN على السمات المستعلم عنها بكثرة، أو استخدم فهرس تعبيري لاستخراج الحقول الساخنة.
س كم مرة يجب تحديث طريقة العرض المادية؟
ج يعتمد على مدى حداثة البيانات المطلوبة. تقرير يومي يمكن تحديثه مرة في اليوم؛ لوحة معلومات فورية يمكنها استخدام REFRESH CONCURRENTLY كل 5-15 دقيقة لتجنب قفل الجدول.
س كيف أملأ عمود embedding بـ pgvector بالبيانات؟
ج PostgreSQL نفسها لا تولد التضمينات. طبقة التطبيق تستدعي نموذج AI (مثلاً OpenAI embeddings API) للحصول على المتجه، ثم تكتبه في عمود vector في PG. يمكن مزامنة ذلك تلقائياً بمشغل أو كود تطبيقي.
س هل يمكن لهذا المشروع استبدال Elasticsearch؟
ج للمقياس المتوسط (ملايين الوثائق)، نعم. البحث النصي الكامل في PG مع pg_trgm يغطي حوالي 80% من السيناريوهات. لكن للمقياس الكبير جدا (مئات الملايين) أو عند الحاجة لتحليلات تجميعية، يبقى Elasticsearch محرك البحث المتخصص.

📖 ملخص


📝 تمارين

  1. ⭐ باتباع سكريبت المثال الشامل، أنشئ قاعدة بيانات التجارة الإلكترونية الكاملة على مثيل PG المحلي لديك، وأدرج 10 صفوف اختبارية، وتحقق من الاستعلام الأساسي لكل وحدة (تسجيل دخول المستخدم، بحث المنتجات، إنشاء الطلب، UPSERT السلة).

  2. ⭐⭐ وسّع المشروع الشامل: (1) أضف وحدة كوبونات (جدول coupons + قواعد JSONB + إجراء مخزن للتحقق منها)؛ (2) اكتب دالة search_products(keyword TEXT, min_price NUMERIC, max_price NUMERIC, category_id INT) تجمع البحث النصي الكامل + تصفية JSONB + نطاق السعر؛ (3) أنشئ مهمة مجدولة pg_cron للتجزئة التلقائية الشهرية.

  3. ⭐⭐⭐ أنتج تقرير تحسين بمستوى إنتاجي: (1) استخدم pg_stat_statements لجمع أعلى 10 استعلامات بطيئة واقترح خطط تحسين؛ (2) صمم استراتيجية autovacuum لجميع الجداول المجزأة؛ (3) اضبط النسخ الاحتياطي PITR واختبر الاستعادة لنقطة زمنية؛ (4) اكتب سكريبت postgres_fdw كامل لنقل البيانات من مثيل PG إلى آخر؛ (5) استخدم EXPLAIN ANALYZE للتحقق من أن جميع الاستعلامات الرئيسية تأخذ الخطة المثلى.

Web-Tutorial.com

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

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

100%