Khái niệm và vai trò của View trong kiến trúc Stream-native
Trong môi trường streaming, việc phân biệt giữa View và Materialized View là yếu tố cốt lõi để tối ưu hiệu năng.
View hoạt động như một truy vấn ảo (virtual query). Nó không lưu trữ dữ liệu vật lý. Mỗi khi bạn thực hiện truy vấn vào View, Materialize sẽ tái tính toán lại toàn bộ logic dựa trên dữ liệu hiện tại của các Source gốc. Điều này phù hợp cho các báo cáo không cần độ trễ thấp hoặc dữ liệu có tần suất thay đổi thấp.
Materialized View (MV) khác biệt hoàn toàn. Nó lưu trữ kết quả tính toán dưới dạng một bảng vật lý (persisted table). Khi dữ liệu đầu vào thay đổi, Materialize chỉ tính toán lại phần dữ liệu bị ảnh hưởng và cập nhật MV ngay lập tức. Đây là cơ chế "chuyển đổi dữ liệu thời gian thực" (real-time transformation) mà chúng ta cần.
Tạo View tạm thời cho truy vấn đơn giản
Bước đầu tiên là xác định cách tạo View để lọc và định dạng dữ liệu từ Source Kafka đã được thiết lập ở phần trước.
Giả định chúng ta có một Source tên là sales_source chứa dữ liệu giao dịch với các trường: transaction_id, amount, currency, status.
Chúng ta sẽ tạo một View để chỉ hiển thị các giao dịch có trạng thái "COMPLETED" và đổi đơn vị tiền sang USD (giả định tỷ giá cố định 24000).
Truy cập vào CLI của Materialize (thông qua mz hoặc psql đã cấu hình ở Phần 2).
CREATE VIEW completed_sales_usd AS
SELECT
transaction_id,
amount * 24000.0 AS amount_usd,
currency,
status
FROM sales_source
WHERE status = 'COMPLETED';
Kết quả mong đợi: Không có dữ liệu nào được lưu trữ trên disk. View được tạo thành công trong catalog của Materialize. Khi chạy SELECT * FROM completed_sales_usd;, hệ thống sẽ thực thi query WHERE và phép nhân * 24000 trên toàn bộ tập dữ liệu hiện có của sales_source tại thời điểm đó.
Để verify, chạy lệnh sau để kiểm tra xem View có trong danh sách bảng không:
\d completed_sales_usd
Bạn sẽ thấy thông tin định nghĩa của View, nhưng không có số lượng dòng (row count) cụ thể cho đến khi bạn thực thi query SELECT.
Xây dựng Materialized View cho tính toán liên tục
Khi cần dữ liệu luôn sẵn sàng để join hoặc aggregate mà không gây tải cho Source gốc, chúng ta chuyển sang Materialized View.
Tạo MV để tính tổng doanh thu theo từng loại tiền tệ (currency) từ Source sales_source. Dữ liệu này sẽ được cập nhật ngay lập tức khi một dòng giao dịch mới vào Kafka.
Khác với View thông thường, MV yêu cầu hệ thống phải lưu trữ trạng thái (state) của phép aggregate.
CREATE MATERIALIZED VIEW sales_by_currency AS
SELECT
currency,
SUM(amount) AS total_revenue,
COUNT(*) AS transaction_count
FROM sales_source
GROUP BY currency;
Kết quả mong đợi: Materialize sẽ bắt đầu quá trình "chốt" (materialize) dữ liệu. Ban đầu nó sẽ quét toàn bộ lịch sử của sales_source để tính toán kết quả ban đầu, sau đó lắng nghe các thay đổi (upsert/delete) để cập nhật total_revenue và transaction_count cho từng currency tương ứng.
Verify kết quả bằng cách chọn toàn bộ dữ liệu từ MV:
SELECT * FROM sales_by_currency;
Bạn sẽ thấy các dòng dữ liệu vật lý đã được lưu. Khi bạn inject thêm một dòng dữ liệu mới vào Kafka Source, chạy lại lệnh SELECT trên MV này, kết quả sẽ thay đổi ngay lập tức mà không cần quét lại toàn bộ bảng Source.
Xử lý Join và Aggregate phức tạp trên Stream
Trong thực tế, dữ liệu thường nằm rời rạc trên nhiều Kafka Topic khác nhau. Ví dụ: Topic A chứa đơn hàng (Order), Topic B chứa thông tin khách hàng (Customer).
Chúng ta sẽ tạo một Materialized View thực hiện JOIN giữa hai Source và Aggregate để tính tổng giá trị đơn hàng theo từng khu vực (region) của khách hàng.
Giả định Source orders_source (từ Topic Orders) và Source customers_source (từ Topic Customers). Cả hai đều đã được tạo ở Phần 4.
CREATE MATERIALIZED VIEW regional_sales_summary AS
SELECT
c.region,
COUNT(o.order_id) AS total_orders,
SUM(o.amount) AS total_value
FROM orders_source o
JOIN customers_source c
ON o.customer_id = c.customer_id
WHERE o.status = 'SHIPPED'
GROUP BY c.region;
Logic hoạt động: Materialize sẽ duy trì hai bảng tạm (intermediate tables) để lưu trạng thái của orders_source và customers_source. Khi có sự thay đổi ở một trong hai Source, hệ thống sẽ tự động tính toán lại các dòng bị ảnh hưởng trong phép JOIN và cập nhật lại tổng số cho regional_sales_summary.
Điều quan trọng cần lưu ý: Phép JOIN trong streaming chỉ hiệu quả khi có một khóa chính (primary key) rõ ràng để hệ thống có thể loại bỏ các bản ghi cũ (tombstone) khi dữ liệu cập nhật. Nếu Source không có Envelope (UPSERT), phép JOIN có thể sinh ra dữ liệu dư thừa (duplicate).
Verify kết quả:
SELECT * FROM regional_sales_summary;
Để kiểm tra tính "streaming" thực sự, hãy gửi một thông báo đơn hàng mới vào Kafka Topic Orders với customer_id thuộc vùng "North". Chạy lại lệnh SELECT trên regional_sales_summary ngay sau đó. Bạn sẽ thấy số liệu của vùng "North" tăng lên ngay lập tức.
Quản lý và tối ưu hóa Materialized View
Việc tạo quá nhiều Materialized View trên cùng một Source có thể gây tải cho hệ thống. Cần hiểu cách Materialize quản lý các MV này.
Sử dụng lệnh \dm (describe materialized view) để xem chi tiết cấu trúc và trạng thái của MV.
\dm regional_sales_summary
Kết quả sẽ hiển thị định nghĩa SQL, các dependency (các source/table nào được dùng để tạo ra MV này), và các chỉ số (indexes) tự động được tạo để hỗ trợ hiệu năng.
Nếu cần xóa một MV không còn sử dụng để giải phóng tài nguyên:
DROP MATERIALIZED VIEW regional_sales_summary;
Khi xóa MV, toàn bộ dữ liệu vật lý và trạng thái tính toán của nó sẽ bị xóa. Tuy nhiên, các Source gốc không bị ảnh hưởng.
Một lưu ý kỹ thuật quan trọng: Materialized View trong Materialize hỗ trợ Real-time Updates. Nếu bạn thay đổi định nghĩa của MV (ví dụ: thêm một cột mới hoặc thay đổi logic aggregate), bạn có thể dùng lệnh ALTER MATERIALIZED VIEW trong một số trường hợp, nhưng thường an toàn và hiệu quả hơn là tạo mới và xóa cái cũ để tránh bị "re-materialize" toàn bộ lịch sử dữ liệu gây nghẽn hệ thống.
SELECT name, type FROM mz_materialized_views WHERE name = 'sales_by_currency';
Lệnh này giúp bạn kiểm tra nhanh xem đối tượng có thực sự là Materialized View hay không (để phân biệt với View thường).
Điều hướng series:
Mục lục: Series: Triển khai Database Stream-native với Materialize trên Ubuntu 24.04
« Phần 4: Xây dựng các Source và Sink đầu tiên với SQL
Phần 6: Quản lý phiên bản và kiểm soát dữ liệu với Envelope »