StarRocks OLAP: Hành Trình Tối Ưu Query Analytics Từ 1.5 Giây Xuống 250ms
Bài này có bản tiếng Anh. Read in English →
Nguyen Hai Trinh • Senior Software Engineer @ SmileViet
Lúc còn ở Cognisian, mình phụ trách dashboard analytics cho một nền tảng y tế, khoảng 150 biểu đồ: số lượt khám theo khoa, thời gian chờ trung bình, xu hướng chẩn đoán theo thời gian. Ban đầu mọi query chạy thẳng trên Postgres, cùng con database phục vụ hệ thống HIS vận hành. Một biểu đồ load mất 1.5 giây. Nhân với 150 biểu đồ trên một trang thì gần như không dùng được.
1. Postgres (OLTP) không hợp với workload analytics
Postgres là row-store tối ưu cho OLTP: đọc ghi từng bản ghi, transaction ngắn, index theo primary key. Nhưng query analytics kiểu “tổng số lượt khám theo khoa, theo tháng, trong 2 năm qua” phải quét hàng chục triệu dòng mà chỉ cần vài cột: khoa, ngày, số lượng. Columnar storage sinh ra để giải đúng bài này.
Với row-store, mỗi query phải đọc cả row — kể cả mấy cột chẳng cần như ghi chú bác sĩ hay metadata JSON — rồi mới lọc ra cột cần. Lãng phí I/O khủng khiếp. Chưa hết: mấy query multi-join, group-by nhiều tầng chạy chung instance Postgres với traffic vận hành, tức luồng ghi hồ sơ bệnh án. Lock contention với CPU spike quay lại đập vào hệ thống chính.
2. Vì sao chọn StarRocks
Tụi mình đánh giá vài lựa chọn (ClickHouse, Druid, BigQuery) nhưng chọn StarRocks vì ba lý do cụ thể với bài toán của mình, không phải vì nó đang hot:
- MPP (Massively Parallel Processing): một query được chia nhỏ và chạy song song trên nhiều node, tận dụng hết CPU cluster thay vì chạy tuần tự.
- Columnar storage + vectorized execution: chỉ đọc đúng cột cần, xử lý theo batch (vector) thay vì từng dòng, nhanh hơn đáng kể cho aggregate query.
- Hỗ trợ real-time ingestion qua CDC (Change Data Capture) từ Postgres, nên dashboard vẫn gần real-time (độ trễ vài giây đến vài phút) mà không cần tự xây pipeline ETL phức tạp.
3. Kiến trúc: tách OLTP và OLAP, đồng bộ qua CDC
Thay vì query trực tiếp Postgres, kiến trúc mới tách hẳn hai luồng:
Postgres (OLTP, nguồn sự thật)
│ CDC (Debezium → Kafka)
▼
StarRocks (OLAP, phục vụ dashboard)
▲
Next.js Analytics Dashboard (~150 charts)
Mọi thay đổi trên Postgres, insert/update/delete gì cũng vậy, Debezium bắt qua WAL rồi đẩy sang Kafka. StarRocks’s Routine Load consume liên tục để cập nhật bảng đích. Dashboard không chạm vào Postgres nữa. Query analytics chạy hết trên StarRocks, cô lập hẳn khỏi traffic vận hành.
4. Ba kỹ thuật tối ưu ăn tiền nhất
a. Materialized View cho các aggregate lặp lại
Nhiều biểu đồ chỉ khác nhau ở khoảng thời gian lọc — ngày, tuần, tháng — nhưng logic aggregate thì y nhau: đếm, trung bình, tổng. Để mỗi query tự tính từ bảng fact chi tiết thì phí. Tụi mình tạo materialized view pre-aggregate theo ngày:
CREATE MATERIALIZED VIEW mv_visits_daily
DISTRIBUTED BY HASH(department_id)
AS
SELECT
department_id,
DATE(visit_time) AS visit_date,
COUNT(*) AS visit_count,
AVG(wait_minutes) AS avg_wait_minutes
FROM fact_visits
GROUP BY department_id, DATE(visit_time);
StarRocks tự route query vào materialized view phù hợp nếu pattern khớp, gọi là query rewrite. Phần lớn code dashboard không phải sửa gì. Có materialized view là query tự nhanh hơn.
Đường đi của một query dashboard nó như vậy:
flowchart LR
D["Query dashboard<br/>aggregate theo ngày/tuần/tháng"] --> RW{"Pattern có khớp MV?"}
RW -- "khớp" --> MV["mv_visits_daily<br/>pre-aggregate theo ngày"]
RW -- "không khớp" --> F["fact_visits"]
b. Partition và bucket theo thời gian
Bảng fact (fact_visits) partition theo tháng (PARTITION BY RANGE(visit_time)), trong mỗi partition lại bucket theo department_id (DISTRIBUTED BY HASH(department_id) BUCKETS 16). Hai lớp này giúp:
- Query lọc theo khoảng thời gian (gần như dashboard nào cũng có date-range filter) chỉ cần quét đúng partition liên quan, bỏ qua hoàn toàn dữ liệu cũ.
- Query group-by theo khoa tận dụng được bucket pruning, giảm thêm lượng dữ liệu phải đọc.
Cộng dồn lại thì lượng dữ liệu phải đọc nó rụng dần theo từng lớp:
flowchart TB Q["Query có date-range filter"] --> P["Chọn đúng partition theo tháng<br/>PARTITION BY RANGE(visit_time)"] P --> B["Bucket pruning theo department_id<br/>BUCKETS 16"] B --> RD["Chỉ còn phần dữ liệu cần đọc"]
c. Denormalize bảng fact thay vì join lúc query
Ở Postgres, mô hình chuẩn hoá với nhiều bảng join là hợp lý cho OLTP. Với OLAP thì ngược lại, join lúc query là một trong những nguyên nhân chậm nhất. Tụi mình denormalize: bảng fact_visits chứa sẵn cả department_name, doctor_name dạng string, thay vì chỉ lưu department_id, doctor_id rồi join với bảng dimension lúc query.
Đánh đổi là tốn thêm storage, vì lưu string lặp lại thay vì chỉ ID, và phải xử lý lại dữ liệu nếu tên khoa hay bác sĩ đổi. Nhưng đây là warehouse chỉ đọc, mà storage thì rẻ hơn CPU với latency nhiều. Tụi mình chấp nhận.
5. Đo lường trước và sau
| Chỉ số | Trước (Postgres) | Sau (StarRocks) |
|---|---|---|
| p50 latency/biểu đồ | ~900ms | ~120ms |
| p95 latency/biểu đồ | ~1.5s | ~250ms |
| CPU load trên DB vận hành | Spike theo giờ cao điểm dashboard | Không ảnh hưởng (tách biệt) |
| Độ trễ dữ liệu (freshness) | Real-time | ~vài giây đến vài phút (qua CDC) |
p95 ~250ms là mức người dùng gần như không cảm nhận được độ trễ khi chuyển tab giữa các biểu đồ. Trước đó là 1.5 giây, dashboard đứng hình mỗi lần filter lại khoảng thời gian.
6. Trade-off cần chấp nhận
Không có gì miễn phí. Ba đánh đổi chính tụi mình chấp nhận:
- Eventual consistency: dữ liệu trên StarRocks trễ hơn Postgres vài giây đến vài phút qua CDC pipeline. Với dashboard báo cáo (không cần real-time tuyệt đối), đây là đánh đổi chấp nhận được; nhưng phải truyền đạt rõ với stakeholder để tránh hiểu lầm “sao số liệu dashboard không khớp ngay lập tức với hệ thống vận hành”.
- Thêm một hệ cơ sở dữ liệu cần vận hành: StarRocks cluster cần được monitor, backup, và có người hiểu cách tune (partition, bucket, compaction) — đây là chi phí vận hành thật, không chỉ là chi phí hạ tầng.
- Pipeline CDC là một điểm hỏng mới: nếu Debezium hoặc Kafka gặp sự cố, dữ liệu trên StarRocks sẽ bị “đứng” lại — cần alerting riêng để phát hiện độ trễ ingestion bất thường, chứ không chỉ alerting cho chính StarRocks.
Tách OLTP/OLAP không phải lúc nào cũng cần ngay từ đầu. Dashboard vài biểu đồ thì Postgres với index tốt vẫn ổn. Nhưng vượt một ngưỡng — ở đây là ~150 biểu đồ với hàng chục triệu bản ghi — thì chi phí vận hành thêm một hệ OLAP chuyên dụng rẻ hơn nhiều so với cái giá của một dashboard không ai dùng được vì quá chậm.
Đọc tiếp
Cần người cho dự án contract hoặc freelance?
Mình nhận dự án fullstack — backend Go, frontend React/Next.js, hoặc cả hai đầu. Gửi vài dòng về phạm vi công việc và thời gian, mình phản hồi trong 48 giờ.