Vấn đề hiệu năng khi tính toán Aggregate trên dữ liệu lớn
Trong môi trường sản xuất thực tế, việc chạy các câu lệnh SELECT có hàm tổng hợp như AVG(), SUM(), MAX() trên hàng triệu hoặc hàng tỷ dòng dữ liệu chuỗi thời gian sẽ gây tắc nghẽn CPU và làm tăng độ trễ (latency) của ứng dụng.
Mỗi lần bạn truy vấn để vẽ biểu đồ xu hướng trong 24 giờ qua, database phải quét toàn bộ dữ liệu gốc (Hypertable), thực hiện phép tính toán trên từng điểm dữ liệu (row) rồi mới trả về kết quả. Quá trình này tốn kém tài nguyên và không thể đáp ứng yêu cầu Real-time.
Giải pháp của TimescaleDB là sử dụng Continuous Aggregate (Tổng hợp liên tục). Cơ chế này tự động tính toán và lưu trữ sẵn các kết quả tổng hợp theo khoảng thời gian định trước (ví dụ: mỗi 15 phút, mỗi 1 giờ) ngay khi dữ liệu mới được ghi vào. Khi bạn truy vấn, hệ thống chỉ cần đọc các kết quả đã tính sẵn, giảm tải tính toán xuống mức tối thiểu.
Tạo Continuous Aggregate để tự động cập nhật dữ liệu
Trước tiên, bạn cần xác định phạm vi thời gian (bucket) cho việc tổng hợp. Ví dụ, chúng ta sẽ tạo một hàm tính trung bình nhiệt độ mỗi 15 phút từ bảng dữ liệu gốc conditions đã tạo ở các phần trước.
Thay vì viết một view thông thường, bạn sử dụng cú pháp CREATE MATERIALIZED VIEW kết hợp với từ khóa WITH (timescaledb_continuous = true). Điều này báo cho TimescaleDB biết đây là một Continuous Aggregate.
Thực thi câu lệnh sau để tạo Continuous Aggregate tính trung bình nhiệt độ (avg_temp) và độ ẩm (avg_humidity) theo từng khoảng 15 phút:
CREATE MATERIALIZED VIEW conditions_agg_15min
WITH (timescaledb_continuous = true)
AS
SELECT time_bucket('15 minutes', time) AS bucket,
sensor_id,
avg(temp) AS avg_temp,
avg(humidity) AS avg_humidity
FROM conditions
GROUP BY bucket, sensor_id;
Kết quả mong đợi: PostgreSQL trả về thông báo CREATE MATERIALIZED VIEW. Lúc này, dữ liệu lịch sử đã được tính toán ngay lập tức, nhưng dữ liệu mới phát sinh sẽ được tính toán theo chu kỳ (polling interval) mặc định.
Để kiểm tra xem Continuous Aggregate đã được tạo chưa và xem thông tin cấu hình, bạn thực hiện:
\d+ conditions_agg_15min
Kiểm tra kết quả: Trong output, bạn phải thấy dòng TimescaleDB Continuous Aggregate và các thông số như bucket interval (15 minutes) được hiển thị rõ ràng.
Tạo Hypertable Materialized View để lưu trữ kết quả
Continuous Aggregate ban đầu được lưu dưới dạng một bảng vật lý thông thường. Khi dữ liệu tăng lên hàng triệu dòng, việc quét bảng này để trả kết quả vẫn có thể chậm nếu không được phân mảnh (sharding) theo thời gian.
Bước quan trọng nhất để tối ưu hiệu năng cực đại là chuyển đổi bảng kết quả của Continuous Aggregate thành một Hypertable. Điều này cho phép dữ liệu tổng hợp được lưu trữ theo cấu trúc phân mảnh, hỗ trợ truy vấn nhanh hơn nhiều lần và dễ dàng áp dụng các chính sách Compression/Retention trong tương lai.
Thực thi lệnh sau để chuyển đổi view conditions_agg_15min thành Hypertable:
SELECT create_hypertable('conditions_agg_15min', 'bucket');
Giải thích: Hàm create_hypertable nhận tên bảng view và tên cột thời gian (ở đây là bucket). TimescaleDB sẽ tự động cấu trúc lại dữ liệu bên trong.
Kết quả mong đợi: Hàm trả về true hoặc ID của Hypertable mới. Sau khi thực thi, bạn cần kiểm tra lại cấu trúc bảng để xác nhận nó đã trở thành Hypertable.
\d+ conditions_agg_15min
Kiểm tra kết quả: Trong phần thông tin bảng, bạn sẽ thấy dòng TimescaleDB Hypertable thay vì chỉ là Continuous Aggregate như trước. Điều này xác nhận dữ liệu đã được phân mảnh theo thời gian.
Cấu hình chu kỳ cập nhật (Polling Interval)
Default, TimescaleDB cập nhật dữ liệu cho Continuous Aggregate mỗi 15 phút. Tuy nhiên, trong môi trường giám sát thời gian thực (Real-time monitoring), 15 phút là quá lâu. Bạn cần giảm chu kỳ này xuống để dữ liệu mới được tính toán và hiển thị ngay lập tức.
Bạn có thể thay đổi tham số polling_interval của Hypertable này bằng hàm alter_continuous_aggregate.
Thay đổi chu kỳ cập nhật xuống còn 5 phút để dữ liệu mới được tính toán nhanh hơn:
SELECT alter_continuous_aggregate(
'conditions_agg_15min',
'polling_interval',
'5 minutes'
);
Kết quả mong đợi: Lệnh trả về ALTER MATERIALIZED VIEW. Từ thời điểm này, worker của TimescaleDB sẽ chạy mỗi 5 phút để tính toán dữ liệu mới phát sinh trong khoảng thời gian đó.
Để kiểm tra trạng thái cập nhật và thời gian chạy gần nhất, truy vấn bảng hệ thống _timescaledb_catalog.continuous_aggs:
SELECT matview_name, last_job_run, next_job_run
FROM _timescaledb_catalog.continuous_aggs
WHERE matview_name = 'conditions_agg_15min';
Kiểm tra kết quả: Bạn sẽ thấy cột last_job_run hiển thị thời điểm tính toán gần nhất và next_job_run dự báo thời điểm chạy tiếp theo (cách 5 phút).
So sánh hiệu năng truy vấn giữa bảng gốc và View đã tính sẵn
Để chứng minh sức mạnh của Continuous Aggregate, chúng ta sẽ so sánh thời gian thực thi (Execution Time) của hai truy vấn tương đương: một truy vấn tính toán thủ công từ bảng gốc và một truy vấn đọc từ view đã tính sẵn.
Bước 1: Thực hiện truy vấn tính trung bình nhiệt độ từ bảng gốc conditions cho khoảng thời gian 1 giờ qua. Lệnh EXPLAIN (ANALYZE, COSTS OFF) sẽ cho biết thời gian thực tế.
EXPLAIN (ANALYZE, COSTS OFF)
SELECT time_bucket('15 minutes', time) AS bucket,
sensor_id,
avg(temp) AS avg_temp
FROM conditions
WHERE time > NOW() - INTERVAL '1 hour'
GROUP BY bucket, sensor_id;
Kết quả mong đợi: Bạn sẽ thấy dòng Execution Time ở cuối output. Với dữ liệu lớn, con số này có thể từ 500ms đến vài giây tùy thuộc vào lượng dữ liệu và tài nguyên CPU.
Bước 2: Thực hiện cùng một truy vấn nhưng đọc từ Hypertable Continuous Aggregate conditions_agg_15min. Lưu ý: khi truy vấn Continuous Aggregate, bạn thường dùng hàm time_bucket_gapfill để lấp đầy các khoảng trống nếu không có dữ liệu trong bucket đó.
EXPLAIN (ANALYZE, COSTS OFF)
SELECT time_bucket_gapfill('15 minutes', bucket) AS bucket,
sensor_id,
avg(avg_temp) AS avg_temp
FROM conditions_agg_15min
WHERE bucket > NOW() - INTERVAL '1 hour'
GROUP BY bucket, sensor_id;
Kết quả mong đợi: Dòng Execution Time sẽ giảm drastically, thường xuống dưới 10ms hoặc thậm chí dưới 1ms. Sự chênh lệch này có thể lên tới 100 lần hoặc hơn.
Phân tích kết quả:
- Truy vấn từ bảng gốc: Phải quét hàng triệu dòng, tính toán hàm
AVG() trên từng dòng, rồi nhóm lại. Tốn CPU và I/O.
- Truy vấn từ Continuous Aggregate: Chỉ cần quét số lượng dòng rất nhỏ (số lượng bucket, ví dụ 4 dòng cho 1 giờ với chu kỳ 15 phút). Kết quả đã được tính sẵn.
Verify kết quả tổng hợp
Cuối cùng, hãy đảm bảo rằng dữ liệu trong Continuous Aggregate khớp với dữ liệu thực tế. Thực hiện truy vấn đơn giản để xem dữ liệu đã được tính toán đầy đủ chưa.
SELECT bucket, sensor_id, avg_temp, avg_humidity
FROM conditions_agg_15min
ORDER BY bucket DESC
LIMIT 10;
Kết quả mong đợi: Bạn thấy danh sách các khoảng thời gian (bucket) đã được tính toán với giá trị trung bình nhiệt độ và độ ẩm. Các dòng dữ liệu mới nhất (nếu vừa được tạo trong 5 phút qua) sẽ xuất hiện ngay lập tức nếu worker đã chạy.
Để kiểm tra xem có bucket nào bị thiếu dữ liệu (gap) hay không, bạn có thể dùng hàm time_bucket_gapfill trong truy vấn SELECT để tự động điền giá trị NULL hoặc giá trị mặc định cho các khoảng thời gian không có dữ liệu.
SELECT time_bucket_gapfill('15 minutes', bucket) AS bucket,
sensor_id,
avg(avg_temp) AS avg_temp
FROM conditions_agg_15min
WHERE bucket > NOW() - INTERVAL '1 day'
AND sensor_id = 1
GROUP BY bucket, sensor_id
ORDER BY bucket;
Kết quả mong đợi: Output trả về một chuỗi thời gian liên tục trong 24 giờ, không bị ngắt quãng, giúp việc vẽ biểu đồ trên frontend trở nên mượt mà và chính xác.
Điều hướng series:
Mục lục: Series: Triển khai Database Time-Series với TimescaleDB và Ubuntu 24.04
« Phần 4: Tối ưu hiệu năng lưu trữ với Compression và Retention
Phần 6: Bảo mật, sao lưu và Troubleshooting nâng cao »