SQL Academy · درس

إعادة بناء الحالة من الأحداث

ادمج الأحداث للوصول إلى الحالة الحالية

الدرس 4 من 413 خطوة

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

ما المقصود بإعادة بناء الحالة؟

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

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

جدول أحداث بسيط

لنبدأ بإنشاء سجل أحداث بسيط لنظام حساب مصرفي. يمثل كل صف شيئًا حدث، مثل إيداع أو سحب، مع المبلغ والطابع الزمني.

لا يُحدّث هذا الجدول ولا تُحذف منه صفوف. وتُضاف الحقائق الجديدة دائمًا على هيئة صفوف جديدة.

CREATE TABLE account_events (
  event_id   SERIAL PRIMARY KEY,
  account_id INT NOT NULL,
  event_type VARCHAR(20) NOT NULL,  -- 'deposit' or 'withdrawal'
  amount     NUMERIC(12, 2) NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

INSERT INTO account_events (account_id, event_type, amount, created_at) VALUES
  (1, 'deposit',    1000.00, '2024-01-01 09:00:00+00'),
  (1, 'deposit',     500.00, '2024-01-03 14:00:00+00'),
  (1, 'withdrawal',  200.00, '2024-01-05 10:00:00+00'),
  (1, 'deposit',     300.00, '2024-01-07 11:00:00+00'),
  (1, 'withdrawal',  150.00, '2024-01-09 16:00:00+00');

تجميع الأحداث في رصيد

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

يمنحنا هذا الاستعلام الواحد الحالة الحالية المستخلصة بالكامل من سجل الأحداث التاريخي.

SELECT
  account_id,
  SUM(
    CASE event_type
      WHEN 'deposit'    THEN  amount
      WHEN 'withdrawal' THEN -amount
      ELSE 0
    END
  ) AS current_balance
FROM account_events
WHERE account_id = 1
GROUP BY account_id;

الحالة في نقطة زمنية

من أقوى جوانب Event Sourcing إمكانية إعادة بناء الحالة في أي نقطة زمنية. يكفي إضافة عامل التصفية WHERE created_at <= :target_time قبل التجميع.

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

-- What was the balance at the end of January 5th?
SELECT
  account_id,
  SUM(
    CASE event_type
      WHEN 'deposit'    THEN  amount
      WHEN 'withdrawal' THEN -amount
      ELSE 0
    END
  ) AS balance_at_snapshot
FROM account_events
WHERE account_id = 1
  AND created_at <= '2024-01-05 23:59:59+00'
GROUP BY account_id;

الرصيد الجاري باستخدام دوال النوافذ

بدلًا من حساب إجمالي واحد، يمكننا حساب الرصيد الجاري — أي الرصيد بعد كل حدث. تحسب دالة النافذة SUM(...) OVER (ORDER BY ...) المجموع التراكمي مع تراكم الأحداث بترتيب زمني.

وهذا مفيد جدًا لسجلات التدقيق وتصحيح أخطاء انتقالات الحالة.

SELECT
  event_id,
  created_at,
  event_type,
  amount,
  SUM(
    CASE event_type
      WHEN 'deposit'    THEN  amount
      WHEN 'withdrawal' THEN -amount
      ELSE 0
    END
  ) OVER (PARTITION BY account_id ORDER BY created_at, event_id)
    AS running_balance
FROM account_events
WHERE account_id = 1
ORDER BY created_at, event_id;

تجسيد الحالة في جدول لقطات

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

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

CREATE TABLE account_snapshots (
  account_id      INT PRIMARY KEY,
  current_balance NUMERIC(12, 2) NOT NULL,
  as_of_event_id  INT NOT NULL,
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Populate / refresh the snapshot from the event log
INSERT INTO account_snapshots (account_id, current_balance, as_of_event_id, updated_at)
SELECT
  account_id,
  SUM(CASE event_type WHEN 'deposit' THEN amount WHEN 'withdrawal' THEN -amount ELSE 0 END),
  MAX(event_id),
  NOW()
FROM account_events
GROUP BY account_id
ON CONFLICT (account_id) DO UPDATE
  SET current_balance = EXCLUDED.current_balance,
      as_of_event_id  = EXCLUDED.as_of_event_id,
      updated_at      = EXCLUDED.updated_at;

تحديثات اللقطات التزايدية

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

يحافظ هذا النمط التزايدي على سرعة تحديث اللقطات حتى مع السجلات الكبيرة.

-- Apply only new events since the last snapshot
UPDATE account_snapshots AS snap
SET
  current_balance = snap.current_balance + delta.net,
  as_of_event_id  = delta.max_event_id,
  updated_at      = NOW()
FROM (
  SELECT
    ae.account_id,
    SUM(CASE ae.event_type WHEN 'deposit' THEN ae.amount WHEN 'withdrawal' THEN -ae.amount ELSE 0 END) AS net,
    MAX(ae.event_id) AS max_event_id
  FROM account_events ae
  JOIN account_snapshots s ON s.account_id = ae.account_id
  WHERE ae.event_id > s.as_of_event_id
  GROUP BY ae.account_id
) AS delta
WHERE snap.account_id = delta.account_id;

الجداول الزمنية وإصدار النظام

قدّم SQL:2011 الجداول الزمنية ذات الإصدار النظامي، التي تتولى قاعدة البيانات نفسها صيانتها. ويحصل كل صف تلقائيًا على عمودي valid_from وvalid_to اللذين يديرهما المحرك.

لا يدعم PostgreSQL ذلك أصلاً، لكن يمكنك محاكاته. وتدعم قواعد بيانات أخرى مثل MariaDB وSQL Server العبارة WITH SYSTEM VERSIONING مباشرة.

-- Emulating a temporal table in PostgreSQL
CREATE TABLE account_state_history (
  account_id      INT NOT NULL,
  current_balance NUMERIC(12, 2) NOT NULL,
  valid_from      TIMESTAMPTZ NOT NULL,
  valid_to        TIMESTAMPTZ NOT NULL DEFAULT 'infinity'
);

-- Insert initial state
INSERT INTO account_state_history (account_id, current_balance, valid_from)
VALUES (1, 1000.00, '2024-01-01 09:00:00+00');

-- On update: close old row, insert new row
UPDATE account_state_history
  SET valid_to = '2024-01-03 14:00:00+00'
WHERE account_id = 1 AND valid_to = 'infinity';

INSERT INTO account_state_history (account_id, current_balance, valid_from)
VALUES (1, 1500.00, '2024-01-03 14:00:00+00');

الاستعلام عن السجل الزمني

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

يفصل هذا النمط منطق الاستعلام عن إعادة تشغيل الأحداث — فجدول سجل الحالة مجمّع مسبقًا.

-- What was the account balance on January 4th?
SELECT
  account_id,
  current_balance,
  valid_from,
  valid_to
FROM account_state_history
WHERE account_id = 1
  AND valid_from <= '2024-01-04 00:00:00+00'
  AND valid_to   >  '2024-01-04 00:00:00+00';

Event Sourcing مع كيانات متعددة

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

نتتبع هنا حركات المخزون عبر منتجات متعددة. وإعادة بناء المخزون الحالي لكل منتج ليست سوى عملية تجميع حسب المنتج مرة أخرى.

CREATE TABLE inventory_events (
  event_id    SERIAL PRIMARY KEY,
  product_id  INT NOT NULL,
  event_type  VARCHAR(20) NOT NULL,  -- 'received', 'shipped', 'adjusted'
  quantity    INT NOT NULL,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

INSERT INTO inventory_events (product_id, event_type, quantity, created_at) VALUES
  (101, 'received',  200, '2024-03-01 08:00:00+00'),
  (101, 'shipped',    50, '2024-03-02 12:00:00+00'),
  (101, 'shipped',    30, '2024-03-04 15:00:00+00'),
  (102, 'received',  150, '2024-03-01 08:00:00+00'),
  (102, 'adjusted',  -10, '2024-03-03 09:00:00+00');

-- Rebuild current stock for all products
SELECT
  product_id,
  SUM(CASE event_type WHEN 'received' THEN quantity WHEN 'shipped' THEN -quantity ELSE quantity END) AS stock_on_hand
FROM inventory_events
GROUP BY product_id
ORDER BY product_id;

استخدام CTE لتوضيح الاستعلامات

قد تصبح استعلامات إعادة بناء الحالة معقدة. يؤدي تغليف خطوة الطي داخل CTE إلى تحسين سهولة القراءة، كما يتيح ضم الحالة المعاد بناؤها إلى جداول أخرى بطريقة واضحة.

نعيد هنا بناء أرصدة الحسابات، ثم نضمها إلى جدول مرجعي للحسابات لإضافة أسماء المالكين إلى النتائج.

CREATE TABLE accounts (
  account_id INT PRIMARY KEY,
  owner_name VARCHAR(100) NOT NULL
);

INSERT INTO accounts (account_id, owner_name) VALUES
  (1, 'Alice'),
  (2, 'Bob');

INSERT INTO account_events (account_id, event_type, amount, created_at) VALUES
  (2, 'deposit',   2000.00, '2024-01-02 10:00:00+00'),
  (2, 'withdrawal', 400.00, '2024-01-06 11:00:00+00');

WITH rebuilt_balances AS (
  SELECT
    account_id,
    SUM(CASE event_type WHEN 'deposit' THEN amount WHEN 'withdrawal' THEN -amount ELSE 0 END) AS balance
  FROM account_events
  GROUP BY account_id
)
SELECT
  a.account_id,
  a.owner_name,
  rb.balance
FROM accounts a
JOIN rebuilt_balances rb USING (account_id)
ORDER BY a.account_id;

اختبار الفهم

اختبر فهمك لإعادة بناء الحالة من الأحداث باستخدام SQL.

مراجعة الدرس

تعلمت في هذا الدرس كيفية إعادة بناء الحالة الحالية والتاريخية من سجل أحداث غير قابل للتغيير باستخدام SQL.

أهم النقاط:

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

تشكل هذه الأنماط أساس تصاميم قواعد البيانات التي تعتمد على Event Sourcing وتراعي سهولة التدقيق.

البدء مجانًا

تعلم SQL مع معلم ذكاء اصطناعي — مجانًا

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

الدورات
46
الدروس
183

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

هل درس «إعادة بناء الحالة من الأحداث» مجاني؟

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

ماذا ستتعلم في «إعادة بناء الحالة من الأحداث»؟

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

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

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

كم من الوقت يستغرق درس «إعادة بناء الحالة من الأحداث»؟

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

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

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

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

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