预聚合、物化视图、Cubes
TL;DR
聚合查询全量 scan 太贵 → 用预聚合 + 物化视图 + 数据 cube 提前算好 → 查询时查小表。代价:构建 + 维护 + 一致性。本节覆盖 ROLLUP / CUBE / GROUPING SETS、PostgreSQL / Materialize 物化视图、Apache Druid auto-rollup、Cube.js semantic layer、ClickHouse AggregateFunction、Star Schema vs Snowflake Schema。
一、Star Schema vs Snowflake Schema
1.1 Star Schema
- 中心是 fact 表(事件)
- 周围是 dim 表(用户、商品、时间)
- fact 表大(亿+ row),dim 表小(k-row)
- fact 表外键链接 dim
- 物理上 星状 → 简单 join 顺序 planner 容易优化
dim_user dim_product dim_time
| | |
+----------+-------------+
|
fact_sales
(10^9 rows)
适合:BI、报表、阳 reporting。
1.2 Snowflake Schema
- dim 表进一步 normalized → 多层 dim 表
- 节省存储,但 join 复杂
- 现代 OLAP 罕用(存储已不贵)
二、预聚合模型
2.1 Rollup Table
每次 INSERT fact + 1 row 写汇总表:
CREATE TABLE sales_by_day (
dt DATE,
product_id BIGINT,
total_amount DECIMAL, n_sales INT);
INSERT INTO sales_by_day
SELECT created_at::DATE, product_id, SUM(amount), COUNT(*)
FROM sales_raw
WHERE created_at::DATE = '2024-10-22'
GROUP BY created_at::DATE, product_id;
查询变小:
SELECT dt, SUM(total_amount)
FROM sales_by_day
WHERE dt BETWEEN '2024-10-01' AND '2024-10-31'
GROUP BY dt;
100x 加速(30 days × 1000 products = 30k row → 替代 1亿 raw row)。
2.2 Materialized View (PostgreSQL)
CREATE MATERIALIZED VIEW sales_daily AS
SELECT
date_trunc('day', created_at) AS dt,
product_id,
SUM(amount) AS total,
COUNT(*) AS n
FROM sales
GROUP BY dt, product_id
WITH DATA;
REFRESH MATERIALIZED VIEW sales_daily;
-- PG 9.4+ 支持 REFRESH CONCURRENTLY 不阻塞 SELECT
PG 14+ WITH NO DATA + lazy refresh;9.3+ 已有 incremental refresh on CONCURRENTLY。
2.3 ClickHouse AggregateFunction State
CREATE TABLE sales (
dt Date,
product_id UInt32,
amount AggregateFunction(sum, Decimal)
) ENGINE = AggregatingMergeTree PARTITION BY toYYYYMM(dt) ORDER BY (dt, product_id);
INSERT INTO sales SELECT dt, product_id, sumState(amount) FROM sales_log GROUP BY dt, product_id;
SELECT dt, sumMerge(total) FROM sales GROUP BY dt;
AggregateFunction state:聚合为 part merge 时(不重 row,是 state 合并)→ 单 INSERT 1 row/组。
三、Cube.js Semantic Layer
cube Sales {
measures: [totalAmount, count]
dimensions: [createdAt, product, user]
joins: Products ON id = product_id
preAggregations: {
salesByDay: {
type: rollup
measures: [totalAmount]
timeDimension: createdAt
granularity: day
}
}
}
Cube.js 在 query time 选 pre-aggregation rollup → 流 SQL 改写 → 后台刷新 + cache。
Cube.js 部署:
- Cube.js server (Node.js)
- Backend 查 PG / Snowflake / BigQuery
- Frontend RESTful API
- pre-aggregations 可存自己 store (e.g. PG 表 + cube refresh worker)
四、Apache Druid Rollup
Druid ingest 阶段做聚合:
- ingestion spec 中 metrics 计
aggregators(sum, count, hyperUnique etc.) - 同 timestamp bucket + dimensions group 一组 → 1 row
- 用户既不查 raw 只查 rollup
随机 ingest 1 PB raw → 1 TB rollup → 1000× storage 节省。
最终 consistency:Druid 数据源有 granularity (minute/hour/day); query 时合并 rollup grain。
五、Apache Pinot 的 Real-time / Hybrid Tables
- offline segment: 历史 batch push
- realtime segment: Kafka stream 持续 ingest → realtime 0-day data
- Hybrid: 同 table 合一来自 realtime + offline
聚合:Star-Tree Index (Pinot 1.0+) → 子集维度 + sub-set 聚合 → skip 数据,比 Druid rollup 更显增量。
六、Snowflake search optimization + materialized view
CREATE MATERIALIZED VIEW sales_daily_mv AS
SELECT date_trunc('day', created_at) AS dt, product_id,
SUM(amount) AS total, COUNT(*) AS n
FROM sales
GROUP BY dt, product_id;
SELECT * FROM sales_daily_mv WHERE dt = '2024-10-01';
Snowflake 自动改写 query 命中可视物化视图 (用 metadata 找 cover);后台 asynchronous refresh。但 $/byte 计算 → cost 主要 storage。
七、Hyperloglog / approximation
-- PG 默认 count 是 O(N), 大 OLAP 用 approximate HLL
CREATE EXTENSION hll;
SELECT hll_cardinality(hll_agg(user_id)) FROM logs;
-- ClickHouse uniq / uniqExact / uniqCombined
SELECT uniq(user_id), uniqExact(user_id) FROM logs;
HLL ~3% error,1KB filter ~10^9 distinct items → saves memory + 时间。
八、产线案例
8.1 大表 group-by 几小时 → 加 rollup 后 几秒
100亿 sales group by (day, product) → 30 分钟 → rollup 30k row → 3 sec 查询。
8.2 Druid rollup 不频繁导致 query 加爆
旧数据 1 day rollup 100% raw 仍 → ETL job 频率没达到 30 min 一夜 → query 引擎 scan 6× 数据 reduced 到自然 limit
修复:Druid indexRealtime 提高聚合 ingest 次数 + bend at hour rollup ingest 1 min batch.
8.3 物化视图未刷新误以为 query 旧
PG 9.3 没 REFRESH CONCURRENTLY,刷新锁表 → 业务误以为慢,业务 skip → 数据漂移。
修复:升 PG 9.4+ + REFRESH CONCURRENTLY + cron 30min 刷新。
8.4 Cube.js pre-aggregation 大规模膨胀
某 dashboard pre-agg 30k row × dims 5 但 D = 100 × 7 × 1000 = 700k 组 → quota 大。
修复:pre-agg dims 减 3 + 引 high cardinality 单独 cube (用户 specific 减 K use approximate)
8.5 Snowflake materialized view 全 table 1 TB 加倍钱
MV storage 双副本 fact + MV → 业务调 15+ MV → 总 cost storage 30 TB 给 10 TB raw
修复:MV 5 同时舍弃 + HLL 不是 raw count。
九、易错清单
- Rollup 后 history retention:rollup 不消耗 raw → 仅到 30 days;rollup 长期保留
- MV refresh 后 query 实时一致:MV 是 snapshot + 故业务报表万物万 now feature
- HLL approximation:3% 误差是多少 → 直播 report 通常接受;财务一定需 native
- Insert on rollup table 业务必向:业务 INSERT batch 慢慢 with raw row 一古
- Star schema joins 一定简化:均匀 planner join,uniform 接口 select 横向 输料
十、这一章带走的东西
- Star Schema + rollup + MV 让聚合查询 O(N) 变 O(N/group)
- PG 9.4+ REFRESH CONCURRENTLY,老 PG 难升
- Druid / Pinot ingestion 时聚合 → 体积 1000× 缩
- ClickHouse AggregateFunction state + AggregatingMergeTree 是 OLAP 黑 magic
- Cube.js semantic layer 让前端不害怕 rollup 设计
- HLL / T-digest 让 distinct count 内存搏 5KB → billions items
下一节 →
Lakehouse:Iceberg / Delta / Hudi — metadata server 中关键 cap, potential 优化 storage