SQL Academy · บทเรียน

การสร้างสถานะขึ้นใหม่จากเหตุการณ์

รวมเหตุการณ์เข้ากับสถานะปัจจุบัน

บทเรียน 4 จาก 413 ขั้นตอน

การสร้างสถานะขึ้นใหม่จากเหตุการณ์ เป็นบทเรียน SQL Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน SQL Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส SQL Academy มีบทเรียนทั้งหมด 4 บทเรียน

การสร้างสถานะขึ้นใหม่หมายถึงอะไร

ในการจัดเก็บที่ขับเคลื่อนด้วยเหตุการณ์ ข้อมูลจะถูกเก็บเป็นบันทึกเหตุการณ์ที่เปลี่ยนแปลงไม่ได้ แทนที่จะเก็บเป็นแถวที่แก้ไขได้ หากต้องการทราบสถานะปัจจุบันของสิ่งใดก็ตาม คุณต้องประมวลเหตุการณ์เหล่านั้นซ้ำแล้วรวมผลเป็นผลลัพธ์เดียว

กระบวนการนี้เรียกว่าการสร้างสถานะขึ้นใหม่จากเหตุการณ์ ลองนึกถึงบัญชีธนาคาร แทนที่จะเก็บยอดคงเหลือ คุณจะเก็บรายการฝากเงินและถอนเงินทุกครั้ง ยอดคงเหลือจะเท่ากับผลรวมของเหตุการณ์ทั้งหมดนั้นเสมอ

ตารางเหตุการณ์อย่างง่าย

ให้เริ่มด้วยการสร้างบันทึกเหตุการณ์พื้นฐานสำหรับระบบบัญชีธนาคาร แต่ละแถวแสดงสิ่งที่เกิดขึ้น — การฝากเงินหรือการถอนเงิน — พร้อมจำนวนเงินและเวลาประทับ

ตารางนี้จะไม่ถูกแก้ไขหรือลบ แถวใหม่จะถูกเพิ่มต่อท้ายเสมอเพื่อบันทึกข้อเท็จจริงใหม่

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;

สถานะ ณ เวลาใดเวลาหนึ่ง

หนึ่งในคุณสมบัติที่ทรงพลังที่สุดของการจัดเก็บเหตุการณ์คือความสามารถในการสร้างสถานะขึ้นใหม่ ณ เวลาใดก็ได้ เพียงเพิ่มตัวกรอง 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;

ตารางตามเวลาและ SYSTEM VERSIONING

SQL:2011 ได้เปิดตัวตารางตามเวลาที่มีรุ่นจัดการโดยระบบ ซึ่งฐานข้อมูลจะดูแลเอง ทุกแถวจะมีคอลัมน์ valid_from และ valid_to โดยอัตโนมัติ และระบบฐานข้อมูลจะเป็นผู้จัดการคอลัมน์เหล่านี้

PostgreSQL ไม่รองรับคุณสมบัตินี้โดยตรง แต่คุณสามารถจำลองการทำงานได้ ฐานข้อมูลอื่น ๆ เช่น MariaDB และเซิร์ฟเวอร์ SQL รองรับ 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';

การจัดเก็บเหตุการณ์กับหลายเอนทิตี

ระบบจริงมักติดตามเหตุการณ์ของเอนทิตีจำนวนมากพร้อมกัน บันทึกเหตุการณ์ร่วมที่มีคอลัมน์ 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 ช่วยให้คิวรีสร้างสถานะใหม่อ่านง่ายเมื่อต้องเชื่อมสถานะที่คำนวณได้เข้ากับตารางอื่น

รูปแบบเหล่านี้เป็นพื้นฐานของการออกแบบฐานข้อมูลที่ใช้การจัดเก็บเหตุการณ์และเอื้อต่อการตรวจสอบ

เริ่มต้นได้ฟรี

เรียนรู้ SQL ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
46
บทเรียน
183

คำถามที่พบบ่อย

บทเรียน “การสร้างสถานะขึ้นใหม่จากเหตุการณ์” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การสร้างสถานะขึ้นใหม่จากเหตุการณ์” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส SQL Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส SQL Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การสร้างสถานะขึ้นใหม่จากเหตุการณ์”

รวมเหตุการณ์เข้ากับสถานะปัจจุบัน คุณปฏิบัติ SQL Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน SQL Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน SQL Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “การสร้างสถานะขึ้นใหม่จากเหตุการณ์” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน SQL Academy นี้ได้ไหม

ได้ บทเรียน SQL Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. เหตุผลที่ควรเก็บประวัติ
  2. ตารางเหตุการณ์แบบเพิ่มข้อมูลอย่างเดียว
  3. แถวแบบเวลาสัมพันธ์และแบบมีรุ่น
  4. การสร้างสถานะขึ้นใหม่จากเหตุการณ์
← กลับไปที่ SQL Academy