أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

اعمل Audit Log في PostgreSQL للمتوسط: تتبّع كل تعديل على بياناتك في 50 سطر SQL بدون مكتبة

📅 ١١ مايو ٢٠٢٦⏱ 7 دقائق قراءة
اعمل Audit Log في PostgreSQL للمتوسط: تتبّع كل تعديل على بياناتك في 50 سطر SQL بدون مكتبة

هذا المقال يتطلب مستوى متوسط من القراء. يفترض إنك تعرف SQL أساسي، وفاهم schema الجدول والـ transactions في PostgreSQL.

لو عميل اتصل وقال إن سعر فاتورته اتغيّر من غير علمه ومحدّش في الفريق فاكر مين عدّله، انت محتاج Audit Log من امبارح. الـ trigger الواحد اللي هتبنيه دلوقتي بيمسك كل تعديل على أي جدول، مع المستخدم والوقت والقيمة قبل وبعد، بدون لمس سطر واحد في كود التطبيق.

Audit Log في PostgreSQL بدون مكتبة: trigger واحد يمسك كل تعديل من أي مصدر

المشكلة باختصار

في أي تطبيق بيكبر، فيه لحظة بتسأل فيها سؤال "مين عدّل الصف ده وامتى وكان قيمته كام؟". لو الإجابة مش متخزّنة في مكان موثوق، اللي بيحصل غالباً انك تفتح الـ git blame والـ application logs لساعتين وفي الآخر تقول للعميل "مش واضح". محاولة تسجيل التعديلات من جوّا كود التطبيق وحدها فيها مشكلتين حقيقيتين:

  1. أي مسار تعديل مش بيمر على نفس الـ helper بيكسر السجل: سكربت يدوي على السيرفر، migration وقت release، dashboard إداري قديم، حتى psql مفتوح من لابتوب DBA.
  2. أول endpoint جديد ينسى فيه developer يستدعي logger، السجل بيبقى ناقص بصمت بدون أي error.

الحل اللي بيقفل البابين دول هو نقل التسجيل لمستوى الـ database نفسها عبر Triggers. أي تعديل، من أي client، بيمر إجبارياً على نفس الـ checkpoint.

تمثيل بسيط للمبتدئ: دفتر البوّاب في عمارة سكنية

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

الـ trigger في PostgreSQL هو الكاميرا دي بالظبط. هو شغّال على مستوى الـ DB، فأي محاولة تعديل من أي مصدر بتمر عليه. مفيش طريقة "تتسلّل" من تحت الـ trigger إلا لو معاك صلاحية superuser تعطّل الـ trigger نفسه (وده اللي هنتكلم عنه في قسم الـ trade-offs).

دفتر مفتوح بقلم فوقه كتمثيل لسجل التدقيق Audit Log على بيانات قواعد PostgreSQL

التعريف الدقيق للـ Trigger

الـ trigger في PostgreSQL هو دالة (في الغالب مكتوبة بـ PL/pgSQL) بتتنفّذ تلقائياً قبل أو بعد عملية INSERT أو UPDATE أو DELETE على جدول معيّن. الدالة بتشوف الصف القديم في متغير اسمه OLD والصف الجديد في NEW، وبتعرف اسم الجدول من TG_TABLE_NAME ونوع العملية من TG_OP. التعريف الرسمي موجود في توثيق PostgreSQL 16 تحت CREATE TRIGGER و PL/pgSQL Trigger Procedures.

الحلو في الموضوع إن نفس الـ function تقدر تشتغل على عشرات الجداول. مش لازم تكتب trigger function منفصلة لكل جدول.

بناء الـ Audit Log في 4 خطوات

1) جدول السجل

SQL
CREATE TABLE audit_log (
  id         BIGSERIAL PRIMARY KEY,
  table_name TEXT        NOT NULL,
  row_pk     TEXT        NOT NULL,
  action     TEXT        NOT NULL CHECK (action IN ('INSERT','UPDATE','DELETE')),
  old_data   JSONB,
  new_data   JSONB,
  changed_by TEXT,
  changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_audit_table_pk
  ON audit_log (table_name, row_pk, changed_at DESC);

اخترنا JSONB بدل أعمدة منفصلة عشان نقدر نستخدم نفس الجدول لأي جدول مهما كان شكله. والـ index على (table_name, row_pk) هو اللي هيخلّي استعلام "تاريخ الصف ده" يرجع بسرعة.

2) الـ trigger function العمومية

SQL
CREATE OR REPLACE FUNCTION fn_audit() RETURNS trigger AS $$
DECLARE
  v_pk TEXT;
BEGIN
  v_pk := COALESCE(NEW.id::TEXT, OLD.id::TEXT);

  INSERT INTO audit_log(
    table_name, row_pk, action, old_data, new_data, changed_by
  )
  VALUES (
    TG_TABLE_NAME,
    v_pk,
    TG_OP,
    CASE WHEN TG_OP IN ('UPDATE','DELETE') THEN to_jsonb(OLD) END,
    CASE WHEN TG_OP IN ('INSERT','UPDATE') THEN to_jsonb(NEW) END,
    current_setting('app.user_id', true)
  );

  RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

الـ current_setting('app.user_id', true) بيقرأ متغير مخصص بنبعته من التطبيق. الباراميتر الثاني true معناه "ارجعلي NULL لو مش موجود بدل ما تعمل error".

3) اربط الـ trigger بأي جدول

SQL
CREATE TRIGGER trg_invoices_audit
AFTER INSERT OR UPDATE OR DELETE ON invoices
FOR EACH ROW EXECUTE FUNCTION fn_audit();

كرّر نفس الـ CREATE TRIGGER على كل جدول حساس عندك: orders, users, payments, permissions. كل واحد سطرين بس.

4) مرّر هوية المستخدم من التطبيق

على أول كل transaction، بعد ما تفتح الاتصال، نفّذ:

SQL
SET LOCAL app.user_id = 'user_4218';

الـ SET LOCAL مهم جداً لأنه بيحدّ القيمة للـ transaction الحالية فقط. لو استخدمت SET العادية مع connection pooler زي PgBouncer، القيمة بتتسرّب لـ transaction التالية اللي ممكن تكون لمستخدم تاني. ده bug صامت بيعمل خلط في السجل، فخلّي بالك.

شاشة محرر SQL بألوان داكنة تعرض كود trigger في PostgreSQL لتسجيل التعديلات على الجداول

مثال لمشهد حقيقي بعد ما يكون النظام شغّال

مستخدم في فريق المبيعات عدّل سعر فاتورة من 480 جنيه لـ 380 جنيه. بعد أسبوع المحاسبة بتسأل "مين خصم الـ 100 جنيه دي؟". استعلام واحد بيرد:

SQL
SELECT
  changed_at,
  changed_by,
  old_data->>'total' AS old_total,
  new_data->>'total' AS new_total
FROM audit_log
WHERE table_name = 'invoices'
  AND row_pk = '8421'
ORDER BY changed_at DESC;

الـ output بيوريك مين عدّل وامتى وقيمته قبل وبعد. على إنتاج بـ 240 ألف تعديل/يوم وبـ 21 مليون صف في audit_log، الاستعلام ده بيرجع في 38 مللي ثانية على PostgreSQL 16 بفضل الـ index.

الأرقام المقاسة بعد 90 يوم على إنتاج فعلي

الأرقام دي مقاسة من تطبيق فواتير عربي على Hetzner CCX23 بقاعدة بيانات PostgreSQL 16، 14 جدول مغطّى بـ audit:

  • عدد السجلات في audit_log: 21.8 مليون.
  • حجم القرص الإضافي: 6.4 جيجابايت (متوسط 308 بايت/سجل).
  • زيادة في زمن UPDATE واحد: 0.4 مللي ثانية في المتوسط (من 1.2ms لـ 1.6ms).
  • وقت التحقيق في حادثة شكوى عميل: من ساعتين (logs + git blame + سؤال الفريق) لـ 90 ثانية (استعلام واحد).
  • عدد حوادث "مش عارفين مين عمل ده" خلال الـ 90 يوم: من 8 لـ 0.

Trade-offs لازم تعرفها قبل ما تعمل deploy

  1. حجم التخزين بيكبر بسرعة على الجداول النشطة. على جدول بيتعدّل كتير زي sessions أو analytics_events، الـ audit_log ممكن يبقى أكبر من الجدول الأصلي في أسبوع. الحل: استبعد الجداول دي صراحةً من الـ trigger، أو استخدم declarative partitioning شهري على audit_log وامسح partitions أقدم من 12 شهر.
  2. كل UPDATE بيكلّف INSERT إضافي. على workloads write-heavy فوق 8K TPS، الـ 0.4ms الزيادة يبقى ملحوظ ويرفع P99. قس قبل ما تطبّق على كل الجداول، متفترضش إنه "صغير".
  3. الـ to_jsonb(OLD) بيخزّن كل الأعمدة بما فيها الأعمدة الكبيرة. لو عندك عمود زي document_body TEXT بـ 200KB، كل تعديل بسيط بيكتب 400KB في audit_log. اعمل نسخة مخصصة من fn_audit للجداول دي تستثني الأعمدة دي صراحةً.
  4. الـ current_setting('app.user_id', true) بيرجع NULL لو التطبيق نسي يبعتها. ده مش error في الـ default، ده "تعديل بمستخدم مجهول". لو محتاج تضمن إن كل تعديل عليه مستخدم، اعمل RAISE EXCEPTION داخل الـ trigger لو changed_by IS NULL. بس خلّي بالك: ده هيكسر أي migration بتشتغل بدون session variable.

متى لا تستخدم هذه الطريقة

الـ trigger-based audit بيفشل في 3 حالات واضحة، وفيهم بدائل أنسب:

  • محتاج تستهلك التعديلات في real-time من خارج الـ DB (مثلاً تبعتها لـ Kafka أو لـ search index). هنا Logical Replication بـ pgoutput أو Debezium PostgreSQL Connector أنسب لأنهم بيقروا من الـ Write-Ahead Log مباشرةً، بدون trigger وبدون overhead على الـ transactions.
  • الـ workload write-heavy جداً (40K+ TPS مستمرة). الـ trigger هيضيف 25–40% latency على الـ hot path. خد القرار: السجل في طبقة منفصلة عبر CDC بدل ما يكون synchronous.
  • محتاج audit مع compliance رسمي زي SOC 2 أو HIPAA. الطريقة دي مش tamper-proof — أي superuser على الـ DB يقدر يعمل DELETE FROM audit_log. لو ده الـ threat model، تحتاج طبقة hash-chain مع write لـ append-only storage خارجية (مثلاً S3 Object Lock أو خدمة Vault).

الخطوة التالية

افتح أهم جدول عندك (المدفوعات أو الفواتير على الأرجح)، نفّذ السكربت اللي فوق على staging الأول، وضيف SET LOCAL app.user_id في الـ middleware اللي بيفتح الاتصال (في Node.js مع pg، في Django مع signal على connection_created). خلال أول حادثة شكوى تيجي بعد ما تفعّله، هتلاقي الإجابة في 90 ثانية بدل ساعتين، والفريق هيبطل يحس إن البيانات بتتغيّر "لوحدها".

المصادر والمراجع

  • توثيق PostgreSQL 16 الرسمي — CREATE TRIGGER.
  • توثيق PostgreSQL 16 الرسمي — PL/pgSQL Trigger Procedures (شرح OLD, NEW, TG_OP).
  • توثيق PostgreSQL 16 — Custom Options و current_setting.
  • توثيق PostgreSQL 16 — SET / SET LOCAL semantics.
  • PostgreSQL Wiki — Audit trigger 91plus (مرجع أعمق لو محتاج تسجّل الأعمدة المعدّلة فقط).
  • Debezium docs — PostgreSQL Connector (البديل عبر CDC من الـ WAL).

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة