0Pricing
SQL Academy · درس

جداول الأحداث ذات الإلحاق فقط

سجّل ما حدث ولا تستبدل السجلات مطلقًا

جداول الأحداث ذات الإلحاق فقط درس مجاني في SQL Academy على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في SQL Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة SQL Academy 4 دروس في المجموع.

ما هو جدول الإضافة فقط؟

جدول الأحداث للإضافة فقط هو جدول لا تُدرج فيه الصفوف إلا مرة واحدة، ولا تُحدّث أو تُحذف مطلقًا. يمثل كل صف شيئًا حدث في نقطة زمنية محددة.

يشكل هذا النمط الأساس لـ Event Sourcing. فبدلًا من تخزين الحالة الحالية، تخزن كل تغيير على هيئة حدث غير قابل للتغيير، مما يمنحك سجلًا كاملًا وقابلًا للتدقيق.

إنشاء جدول أحداث

يلتقط جدول الأحداث المصمم جيدًا من نفّذ ماذا، وعلى أي مورد، ومتى. يسجل العمود occurred_at الطابع الزمني الدقيق، وتضمن DEFAULT NOW() تعبئته دائمًا تلقائيًا.

لاحظ عدم وجود UPDATE أو DELETE في هذا التصميم، فالصفوف تصبح دائمة بمجرد كتابتها.

CREATE TABLE account_events (
  id           BIGSERIAL PRIMARY KEY,
  account_id   BIGINT      NOT NULL,
  event_type   TEXT        NOT NULL,
  payload      JSONB,
  occurred_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

إدراج الأحداث

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

يحافظ ذلك على التسلسل الكامل لما حدث وبالترتيب الصحيح.

INSERT INTO account_events (account_id, event_type, payload)
VALUES
  (42, 'account_opened',  '{"plan": "free"}'),
  (42, 'email_verified',  '{"email": "alice@example.com"}'),
  (42, 'plan_upgraded',   '{"from": "free", "to": "pro"}');

قراءة السجل الكامل

بما أن كل تغيير في الحالة مخزن في صف، فإن الاستعلام عن السجل الكامل لحساب ما لا يتطلب سوى استخدام SELECT وترتيب النتائج حسب الوقت. يمكنك إعادة تشغيل دورة حياة السجل بأكملها، بدءًا من أول حدث ووصولًا إلى أحدث حدث.

SELECT
  id,
  event_type,
  payload,
  occurred_at
FROM account_events
WHERE account_id = 42
ORDER BY occurred_at ASC;

اشتقاق الحالة الحالية

في جدول الإضافة فقط، لا تخزن الحالة الحالية مباشرة، بل تشتقها من خلال قراءة أحدث حدث ذي صلة. في هذه الحالة، تكون الخطة الحالية للحساب 42 هي ما يحدده أحدث حدث من نوع plan_upgraded أو account_opened.

يؤدي استخدام ORDER BY occurred_at DESC LIMIT 1 إلى جلب أحدث لقطة بكفاءة.

SELECT payload->>'to'  AS current_plan
FROM   account_events
WHERE  account_id = 42
  AND  event_type IN ('account_opened', 'plan_upgraded')
ORDER BY occurred_at DESC
LIMIT 1;

فرض عدم القابلية للتغيير باستخدام القواعد

يمكن فرض ضمان الإضافة فقط على مستوى قاعدة البيانات باستخدام RULE يتجاهل بصمت أي عملية UPDATE أو DELETE على الجدول. ويمنع ذلك التعديلات غير المقصودة من أي تطبيق يملك صلاحية الكتابة.

ويُعد المشغّل الذي يرفع استثناءً بديلًا أقوى، إذ يرفض العملية فعليًا ويعيد خطأ.

CREATE RULE no_update_events AS
  ON UPDATE TO account_events
  DO INSTEAD NOTHING;

CREATE RULE no_delete_events AS
  ON DELETE TO account_events
  DO INSTEAD NOTHING;

عدم القابلية للتغيير باستخدام مشغّل

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

CREATE OR REPLACE FUNCTION deny_event_mutation()
RETURNS TRIGGER AS $$
BEGIN
  RAISE EXCEPTION 'Event table is append-only: % is not allowed', TG_OP;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_deny_event_mutation
BEFORE UPDATE OR DELETE ON account_events
FOR EACH ROW EXECUTE FUNCTION deny_event_mutation();

عدّ الأحداث عبر الزمن

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

SELECT
  DATE_TRUNC('day', occurred_at) AS day,
  event_type,
  COUNT(*)                        AS total
FROM account_events
GROUP BY 1, 2
ORDER BY 1, 2;

إعادة بناء الحالة عند نقطة زمنية

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

وهذا أمر بالغ الأهمية لتصحيح الأخطاء وعمليات التدقيق والامتثال للمتطلبات التنظيمية.

-- What plan was account 42 on at the end of last month?
SELECT payload->>'to' AS plan_at_snapshot
FROM   account_events
WHERE  account_id = 42
  AND  event_type IN ('account_opened', 'plan_upgraded')
  AND  occurred_at <= DATE_TRUNC('month', NOW()) - INTERVAL '1 second'
ORDER BY occurred_at DESC
LIMIT 1;

الأحداث التعويضية بدلًا من التصحيحات

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

يحافظ ذلك على اكتمال سجل التدقيق ويجعل أي تلاعب به قابلًا للاكتشاف.

-- A charge was applied by mistake; record a reversal
INSERT INTO account_events (account_id, event_type, payload)
VALUES (
  42,
  'charge_reversed',
  '{"reason": "billing_error", "reverses_event_id": 17}'
);

تقسيم جداول الأحداث الكبيرة

تنمو جداول الأحداث بسرعة. ويحافظ تقسيمها حسب نطاق زمني على صغر حجم الأقسام الفردية، ويسرّع استعلامات النطاق، ويسمح بأرشفة الأقسام القديمة أو حذفها من دون المساس بالبيانات الحديثة.

يجعل التقسيم التصريحي في PostgreSQL ذلك أمرًا مباشرًا: عرّف قسم RANGE على occurred_at، ودع قاعدة البيانات توجّه عمليات الإدراج تلقائيًا.

CREATE TABLE account_events_2025
  PARTITION OF account_events
  FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

CREATE TABLE account_events_2026
  PARTITION OF account_events
  FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');

اختبار المعرفة: جداول الإضافة فقط

اختبر مدى فهمك لتصميم جداول الأحداث للإضافة فقط.

مراجعة: جداول الأحداث للإضافة فقط

تعلمت في هذا الدرس كيفية تصميم جداول الأحداث للإضافة فقط واستخدامها:

  • عدم القابلية للتغيير — تُدرج الصفوف مرة واحدة ولا تُعدّل أبدًا. فالأحداث السابقة حقائق.
  • السجل الكامل — يُحفظ كل تغيير في الحالة، مما يتيح سجلات تدقيق كاملة واستعلامات عند نقطة زمنية.
  • الأحداث التعويضية — تُصحح الأخطاء بإضافة حدث عكسي جديد، لا بحذف الحدث القديم.
  • الفرض — تمنع القواعد أو المشغلات على مستوى قاعدة البيانات التعديلات غير المقصودة.
  • قابلية التوسع — يحافظ التقسيم حسب النطاق على أداء سجلات الأحداث الكبيرة بمرور الوقت.

تُعد جداول الإضافة فقط العمود الفقري لـ Event Sourcing وبنى CQRS وأي نظام تكون فيه قابلية التدقيق والدقة التاريخية أمرين بالغي الأهمية.

الأسئلة الشائعة

هل درس «جداول الأحداث ذات الإلحاق فقط» مجاني؟

نعم — نص درس «جداول الأحداث ذات الإلحاق فقط» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة SQL Academy، انتقل إلى CoddyKit PRO. تتضمن دورة SQL Academy 4 دروس في المجموع.

ماذا ستتعلم في «جداول الأحداث ذات الإلحاق فقط»؟

سجّل ما حدث ولا تستبدل السجلات مطلقًا تتمرن على SQL Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ SQL Academy؟

لا تُشترط خبرة سابقة. SQL Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.

كم من الوقت يستغرق درس «جداول الأحداث ذات الإلحاق فقط»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس SQL Academy هذا؟

نعم. كل درس في SQL Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. لماذا نحتفظ بالسجل التاريخي
  2. جداول الأحداث ذات الإلحاق فقط
  3. الصفوف الزمنية والمُدارة بالإصدارات
  4. إعادة بناء الحالة من الأحداث
← العودة إلى SQL Academy