1 điểm bởi GN⁺ 3 giờ trước | Chưa có bình luận nào. | Chia sẻ qua WhatsApp
  • Để trả lời câu hỏi dữ liệu trông như thế nào tại một thời điểm cụ thể (thứ Ba tuần trước), Postgres 19 giới thiệu hỗ trợ bảng lịch sử theo thời điểm gốc (temporal table), cho phép theo dõi dữ liệu trước và sau thay đổi mà không cần hệ thống trigger audit riêng
  • Chuẩn SQL:2011 đã định nghĩa bảng temporal hơn 10 năm trước, và Postgres nay mới đưa vào lõi, tham gia muộn hơn các engine cơ sở dữ liệu khác
  • Thay vì dùng hai cột valid_from/valid_toràng buộc loại trừ (exclusion constraint) dựa trên extension btree_gist, giờ đây có cách biểu đạt trực quan hơn với một cột kiểu khoảng (range type) duy nhất cùng ràng buộc WITHOUT OVERLAPS
  • Với cú pháp FOR PORTION OF, tự động tách hàng khi UPDATE·DELETE, engine tự xử lý việc tránh khoảng trống và chồng lấn trên trục thời gian
  • Việc bổ sung lần này tương ứng với thời gian hiệu lực (application time), tức một nửa của hệ thống song thời gian (bi-temporal); chưa hỗ trợ thời gian hệ thống (system time) nhưng đã đặt nền móng cho các bản phát hành sau

Cách cũ - The Old-Fashioned Way

  • Nỗ lực đầu tiên để theo dõi giá sản phẩm theo thời gian thường gồm hai cột ngày valid_from, valid_to và ràng buộc CHECK valid_from < valid_to
    • Nhưng cách này không ngăn được việc chèn hai hàng có khoảng ngày chồng lấn cho cùng một sản phẩm (ví dụ: sản phẩm số 42 vừa có giá $9.99 vừa có giá $14.99 trong cùng một ngày thứ Ba)
  • Giải pháp truyền thống là dùng extension btree_gist và ràng buộc loại trừ (exclusion constraint)
    • Dạng EXCLUDE USING gist (product_id WITH =, daterange(valid_from, valid_to) WITH &&) sẽ gây lỗi khi chèn hàng có khoảng chồng lấn
  • Vấn đề của cách này
    • GiST là kiểu chỉ mục đặc thù của Postgres, cần kinh nghiệm để dùng, và vì là extension tùy chọn nên tạo rào cản tiếp cận
    • Cú pháp ràng buộc loại trừ không trực quan, khó nghĩ đến như một cách tiếp cận chuẩn
    • Bản thân bảng không được tích hợp nhận thức về thời điểm, nên khi thay đổi khoảng thời gian phải tự tay tách hoặc gộp hàng, đẩy gánh nặng đảm bảo tính nhất quán thời gian sang ứng dụng

Lược sử ngắn về thời gian - A Brief History of Time

  • Chuẩn SQL:2011 đưa vào khoảng thời gian hiệu lực (APPLICATION TIME), ràng buộc WITHOUT OVERLAPS, và cú pháp FOR PORTION OF để thao tác dữ liệu theo thời điểm
  • Henrietta Dombrovskaya (Hetti) cùng Chad Slaughter đã phát triển extension pg_bitemporal, một framework quản lý bảng song thời gian trong Postgres bằng PL/pgSQL
    • Từ năm 2015, khái niệm này đã được trình bày tại nhiều hội nghị, minh họa cách theo dõi đồng thời thời gian hiệu lực (valid time) (thời điểm một sự kiện đúng trong thế giới thực) và thời gian giao dịch (transaction time) (thời điểm DB ghi nhận sự kiện đó)
  • Phân biệt hai chiều thời gian
    • Thời gian hiệu lực mang nghĩa như "mức giá này có hiệu lực từ tháng 1 đến tháng 6"
    • Thời gian giao dịch là góc nhìn của DB như "hàng này được chèn lúc 3:47 chiều ngày 12 tháng 3, và bị thay thế lúc 9:01 sáng ngày 3 tháng 4"
    • Kết hợp cả hai cho phép xây dựng bảng song thời gian để trả lời câu hỏi như "dựa trên thông tin ta biết vào thời điểm đó, ta nghĩ giá của thứ Ba tuần trước là bao nhiêu"
  • pg_bitemporal dùng EXCLUDE USING gist kép, áp dụng một lần cho khoảng effective (thời gian hiệu lực) và một lần cho khoảng asserted (thời gian giao dịch)
    • Nó cung cấp các hàm cho chèn, cập nhật, hiệu chỉnh, vô hiệu hóa, xóa trong mô hình song thời gian và triển khai quan hệ khoảng Allen (Allen's interval relationships) để suy luận theo thời điểm
  • Giới hạn của extension
    • Không thể thay đổi query planner để nhận biết điều kiện thời điểm, không thể tích hợp với hệ thống ràng buộc ở cấp engine, cũng không thể cung cấp cú pháp thao tác gốc → cần được đưa vào lõi
    • Postgres 19 tiếp nhận một nửa thời gian hiệu lực của hệ thống song thời gian; chưa trọn vẹn nhưng là bước tiến lớn

Khoảng thời gian giải cứu - Ranges to the Rescue

  • Cách làm trong Postgres 19 dùng một cột kiểu khoảng duy nhất valid_at DATERANGE thay cho valid_from/valid_to riêng biệt
    • PRIMARY KEY (product_id, valid_at WITHOUT OVERLAPS) giúp không còn cần extension btree_gist và ràng buộc loại trừ
    • WITHOUT OVERLAPS đảm bảo product_id duy nhất tại bất kỳ thời điểm nào, nhưng vẫn cho phép nhiều hàng cho cùng một sản phẩm miễn là các khoảng không chồng lấn
  • Về bên trong, hệ thống vẫn dùng chỉ mục GiST và cần btree_gist cho các cột không phải thời điểm trong khóa, nhưng Postgres sẽ tự xử lý phụ thuộc khi khởi tạo ràng buộc
  • Ký hiệu khoảng: [ là gồm, ) là loại trừ → [2025-01-01, 2025-07-01) gồm ngày 1 tháng 1 nhưng không gồm ngày 1 tháng 7
    • Hàng Gadget cuối cùng [2026-01-01,) là khoảng mở ở ngày kết thúc, tức giá hiện tại chưa có ngày kết thúc xác định
  • Cách bảo vệ khỏi chồng lấn hoạt động như sau
    • Nếu chèn khoảng sai [2025-03-01, 2025-01-01), sẽ nhận lỗi kiểu "cận dưới của khoảng phải nhỏ hơn hoặc bằng cận trên"
    • Nếu chèn khoảng chồng lấn [2025-03-01, 2025-09-01), sẽ gặp lỗi vi phạm ràng buộc loại trừ products_pkey
    • Chỉ với việc dùng khoảng, ta có được đồng thời hai lớp kiểm tra

Cắt lát và chia nhỏ - Slicing and Dicing

  • Nếu muốn đổi giá sản phẩm thành $10.99 chỉ trong giai đoạn từ tháng 3 đến tháng 9 năm 2025, cách cũ buộc phải tự tay tách và chèn hàng, dễ tạo ra khoảng trống hoặc chồng lấn nếu sai sót
  • Với bảng lịch sử theo thời điểm, có thể biểu đạt trực tiếp đúng ý định
    • UPDATE products FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-09-01' SET price = 10.99 WHERE product_id = 1
  • Kết quả là số hàng của Widget tăng từ 3 lên 5 hàng
    • Thu nhỏ hàng $9.99 cũ thành [2025-01-01, 2025-03-01)
    • Thêm hàng $10.99 mới cho phần khoảng còn lại
    • Thu nhỏ hàng $12.99 cũ thành [2025-09-01, 2026-01-01)
    • Thêm hàng $10.99 mới cho khoảng còn lại [2025-07-01, 2025-09-01)
  • Lý do $10.99 bị tách thành hai hàng là vì FOR PORTION OF xử lý độc lập từng hàng khớp điều kiện, chứ không tự hợp nhất (coalesce) các khoảng liền kề sau đó
    • Kết quả cuối cùng không có khoảng trống hay chồng lấn, một lợi ích mà logic loại trừ thuần túy trước đây không có
  • Một số trường hợp biên
    • Nếu khoảng FOR PORTION OF nằm hoàn toàn bên trong một hàng hiện có, có thể sinh ra tối đa 2 hàng dư (phía trước và phía sau)
    • Nếu trùng khớp chính xác với ranh giới hiện có thì không cần tạo hàng dư
  • Các hàng dư theo thời điểm mới sinh ra không cần quyền INSERT, nhưng trigger INSERT hiện có vẫn sẽ kích hoạt → cần lưu ý với audit logging hoặc các hàm trigger SECURITY DEFINER

Xóa lịch sử - Erasing History

  • FOR PORTION OF cũng hoạt động với DELETE; ví dụ tạm thời gỡ một sản phẩm khỏi catalog trong giai đoạn từ tháng 6 đến tháng 10 năm 2025
    • DELETE FROM products FOR PORTION OF valid_at FROM '2025-06-01' TO '2025-10-01' WHERE product_id = 2
  • Kết quả
    • Đoạn từ tháng 6 đến tháng 10 bị cắt bỏ, và hàng $22.99 vốn bao phủ [2025-04-01, 2026-01-01) được tách thành hai hàng dư: một hàng kết thúc ở tháng 6 và một hàng bắt đầu từ tháng 10
    • Dữ liệu giá trước và sau khoảng trống được giữ nguyên giá trị ban đầu, nên DELETE có thể làm tăng số lượng hàng
  • Toàn bộ cơ chế quản lý bảng lịch sử theo thời điểm đều được xử lý tự động, loại bỏ nguy cơ xóa quá mức hoặc tạo ra các mảnh rời rạc (orphaned fragment) ở cấp ứng dụng

Sự thật trong quảng cáo - Truth in Advertising

  • Bảng lịch sử theo thời điểm sẽ chưa hoàn chỉnh nếu thiếu khóa ngoại theo thời điểm (temporal foreign key), và Postgres 19 hỗ trợ điều này bằng từ khóa PERIOD
    • Có thể viết dưới dạng FOREIGN KEY (product_id, PERIOD valid_at) REFERENCES products (product_id, PERIOD valid_at)
  • Từ khóa PERIOD cho biết bản thân khóa ngoại là dựa trên thời điểm
    • Product được tham chiếu phải tồn tại trong toàn bộ khoảng valid_at của variant
    • Tổ hợp tất cả các hàng khớp trong bảng được tham chiếu phải phủ kín hoàn toàn khoảng thời gian của hàng đang tham chiếu
  • Nếu cố tạo variant vượt ra ngoài khoảng thời gian của product, chẳng hạn [2025-01-01, 2027-01-01), thao tác sẽ bị từ chối
    • Vì giá của Widget chỉ được định nghĩa đến giữa năm 2026, nên variant tuyên bố có hiệu lực đến năm 2027 sẽ bị từ chối do vi phạm ràng buộc khóa ngoại
  • Có một hạn chế quan trọng
    • Khóa ngoại theo thời điểm chỉ hỗ trợ hành động tham chiếu NO ACTION, không hỗ trợ CASCADE, SET NULL, SET DEFAULT
    • Nếu xóa một hàng product mà variant đang phụ thuộc vào, thao tác luôn lỗi; đây là do độ phức tạp của phép toán thời điểm dây chuyền và ứng dụng phải xử lý tường minh

Những bước nhỏ - Baby Steps

  • Các tính năng hiện có: bảng lịch sử theo thời gian hiệu lực với chống chồng lấn, thao tác dữ liệu theo thời điểm, và khóa ngoại theo thời điểm
  • Thiếu sót lớn nhất là thời gian hệ thống (system time), còn gọi là thời gian giao dịch
    • Thời gian hiệu lực theo dõi khi nào sự thật đúng trong thế giới thực, còn thời gian hệ thống theo dõi khi nào DB biết đến sự thật đó; nhiều hệ thống sử dụng cả hai
    • Đây chính là phần mà extension pg_bitemporal đã bù đắp từ năm 2015
    • Có thể mô phỏng thời gian hệ thống bằng trigger, nhưng điều đó khác với việc được engine quản lý minh bạch như các tính năng thời điểm mới khác
  • Tài liệu về bảng lịch sử theo thời điểm cũng nêu rõ thời gian hệ thống chưa được hỗ trợ gốc và chỉ có thể mô phỏng; chưa rõ Postgres 20 trở đi có đưa vào hay không, nhưng nền móng đã có sẵn

Kết luận - Final Thoughts

  • Cách làm với EXCLUDE USING gist vẫn hoạt động, nhưng là một lối vòng khá thô; các extension như pg_bitemporal đã chứng minh khái niệm và duy trì thảo luận trong thời gian dài
  • Đây là cách tiếp cận trực quan hơn rất nhiều so với ràng buộc loại trừ GiST
    • WITHOUT OVERLAPS trong khóa chính đọc lên như tiếng Anh thông thường, còn FOR PORTION OF mô tả đúng chính xác hành vi
    • Tự động tách hàng khi cập nhật hoặc xóa theo thời điểm giúp loại bỏ cả một nhóm lỗi tiềm ẩn
  • Hành trình từ SQL:2011 đến Postgres 19 là một chặng đường dài; Hetti và cộng đồng đã mất nhiều năm để chứng minh nhu cầu cũng như tính khả thi của mô hình này, và giờ nó đã có trong lõi
  • Đáng chờ xem các bản phát hành tương lai có bổ sung thời gian hệ thống hay không; khi Postgres có đủ cả hai nửa của song thời gian, khả năng ứng dụng sẽ mở rộng đáng kể

Chưa có bình luận nào.

Chưa có bình luận nào.