增量物化视图介绍
什么是增量物化视图
增量物化视图是一种特殊的物化视图,它也具备物化视图的以下特点:
- 都会把查询结果落盘,提升后续查询速度
- 都需要考虑刷新延迟(结果不一定和底表完全实时一致)
- 都要付出额外的存储成本和维护成本
它和物化视图最大的不同是体现在刷新机制上:当底层数据发生变化时,增量物化视图不再对全量数据重新计算,而是只处理这次变化带来的那一小部分影响(Δ,delta),并把这个影响合并到已有的物化结果里。
这里体现了两种不同的刷新方式:
- 全量刷新(Full Refresh):每次刷新都等价于“把整张结果表删了再重算一遍”
- 增量刷新(Incremental Refresh):每次只算“变更产生的增量结果”,再合并到已有结果
那么,普通的物化视图的每次刷新都是全量刷新。而增量物化视图同时具备了两种刷新能力,在可行的时候尽量使用增量刷新,但当发现做不到增量刷新的时候,自动回退成全量刷新。
增量刷新原理
-
捕获变化:知道底表发生了什么变化
- 新增/更新/删除的行,或分区级别的新增和删除
-
计算增量影响:把“底表变化”转换成“视图结果需要怎么变”
- 例如:
count需要 +1 / -1;sum需要加上差值;某个 group 需要新增或修正 - 如果视图包含 join / 去重 / 窗口等复杂逻辑,计算增量影响也会更复杂一些
- 例如:
-
合并到物化结果:将增量结果合并到已落盘的结果表中
- 对物化视图结果表进行新增/更新/删除等操作
与普通物化视图的区别
在使用上,增量物化视图和普通物化视图具备一些相同点,比如:
- 结果数据都会以表的形式存储
- 都可以像普通的物理表一样自定义 Access Method,创建二级索引等
不同点则体现在:
- 刷新成本模型不同
- 普通 MV:成本与“底表总数据量”强相关
- 增量 MV:成本与“本次变更量”以及“查询复杂度“强相关
- 对查询表达能力/可维护性的要求更高
- 增量 MV 往往要求查询是可增量维护的:例如聚合类(sum/count/min/max 等),Join 类比较常见
- 一些复杂的查询/非确定函数可能受限
- 系统内部通常需要更多“状态”
- 为了支持复杂逻辑的增量计算,可能需要维护额外中间表、索引等辅助结构
增量物化视图实践策略
在使用增量物化视图之前,我们建议从业务的角度按照下面的流程进行端到端的评估。
可行性评估
首先我们需要评估增量 MV 对目标场景是否合适。下面这些场景,增量 MV 通常收益较大:
-
底表很大、全量刷新太贵
- 全量重算的时间成本或计算成本不可接受
- 但每次写入量相对较小,且结果的变化幅度不大
-
写多读多,且希望“准实时”看到结果
- 比全量刷新更容易做到分钟级/秒级更新
- 适合数据看板、告警指标、运营分析等
而下面这些往往是不同适用的场景:
-
底表更新/删除非常频繁且影响范围大
- 可能会导致增量的结果也非常大,成本接近或超过全量刷新
-
查询不固定、偶尔才查一次
- 如果很少被查询,预计算的收益不大,反而增加定期维护开销
合理划分增量物化视图
增量物化视图的定义应兼顾两点:
- 覆盖尽可能多的下游查询(提升复用)
- 选择计算开销高、且结构稳定的子查询(提升收益/降低维护复杂度)
考虑一个多表 Join + 多维分析的业务场景,我们会有以下两种选择:
- Join 逻辑比较固定,但希望保留分析灵活性:这种建议把多表 Join 抽取为 MV 定义,下游直接读取 MV 进行 ad hoc 分析
- Join 和聚合维度都比较固定,希望做大量点查:这种建议把 Join + Agg 抽取为 MV 定义,在 MV 上利用主键或二级索引服务点查。
存储与索引选择
物化视图以物理表形式存在,表级存储与索引策略会直接影响查询性能与增量刷新成本。
-
列命名 建议显式指定 MV 的列名。显式列名可提升可读性,并便于后续索引、治理与调试。
-
Access Method 选择 根据 MV 的访问模式来确定合适的 AM:
- 中间层 MV,需要直接访问和参与下游增量计算,建议
hybrid格式 - 服务层 MV,分析为主选择
columnar,点查为主选择row,查询模式多样选择hybrid
- 索引策略
- 点查场景:按访问键构建二级索引以降低查询延迟
- 列存过滤:结合系统支持的过滤类索引(例如 split index)优化扫描效率
使用增量物化视图
创建增量物化视图
在使用增量物化视图之前,我们需要先创建 imv 这个插件:
CREATE EXTENSION imv;然后通过 CREATE MATERIALIZED VIEW 语法进行创建:
CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] table_name
[ (column_name [, ...] ) ]
[ USING method ]
[ TABLESPACE tablespace_name ]
WITH ( refresh_mode = 'incremental', storage_parameter [= value] [, ... ] )
AS query
[ WITH [ NO ] DATA ]参数说明:
refresh_mode: 刷新模式,incremental表示增量刷新模式- 其他参数含义参考全量物化视图定义
示例:
-- 订单表
CREATE TABLE orders (
order_id VARCHAR(20) PRIMARY KEY,
user_id INT NOT NULL,
order_time TIMESTAMP NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
product_list TEXT,
shipping_address TEXT,
order_status VARCHAR(20) DEFAULT 'created' -- created, paid, shipped, completed
);
-- 插入示例数据
INSERT INTO orders VALUES
('ORD10001', 101, '2023-06-01 10:00:00', 299.99, 'P1001,P1002', '北京市海淀区', 'created'),
('ORD10002', 102, '2023-06-01 11:30:00', 159.50, 'P1003', '上海市浦东新区', 'created'),
('ORD10003', 101, '2023-06-02 09:15:00', 89.99, 'P1004,P1005', '北京市海淀区', 'created'),
('ORD10004', 103, '2023-06-03 14:20:00', 450.00, 'P1006,P1007', '广州市天河区', 'created'),
('ORD10005', 102, '2023-06-03 16:45:00', 120.00, 'P1008', '上海市浦东新区', 'created'),
('ORD10006', 101, '2023-06-04 09:30:00', 75.50, 'P1009', '北京市海淀区', 'created');
-- 支付表
CREATE TABLE payments (
payment_id VARCHAR(20) PRIMARY KEY,
order_id VARCHAR(20) NOT NULL,
payment_time TIMESTAMP NOT NULL,
payment_amount DECIMAL(10,2) NOT NULL,
payment_method VARCHAR(20), -- alipay, wechat, credit_card
payment_status VARCHAR(20) -- success, failed, processing
);
-- 插入示例数据
INSERT INTO payments VALUES
('PAY20001', 'ORD10001', '2023-06-01 10:05:23', 299.99, 'alipay', 'success'),
('PAY20002', 'ORD10002', '2023-06-01 11:35:45', 159.50, 'wechat', 'success'),
('PAY20003', 'ORD10003', '2023-06-02 09:20:10', 89.99, 'credit_card', 'failed'),
('PAY20004', 'ORD10004', '2023-06-03 14:25:30', 450.00, 'alipay', 'success'),
('PAY20005', 'ORD10005', '2023-06-03 16:50:15', 120.00, 'wechat', 'failed');
-- 订单与支付关联的物化视图
CREATE MATERIALIZED VIEW order_payment_summary
USING HYBRID
WITH (refresh_mode='incremental')
AS
SELECT
o.order_id,
o.user_id,
o.order_time,
o.total_amount,
o.order_status,
p.payment_id,
p.payment_time,
p.payment_method,
p.payment_status
FROM
orders o
LEFT JOIN
payments p ON o.order_id = p.order_id;
-- 结果表条数
SELECT count(1) FROM order_payment_summary;
count
-------
6
(1 row)
-- 按用户统计订单和支付情况
SELECT
user_id,
COUNT(*) AS total_orders,
SUM(CASE WHEN payment_status = 'success' THEN 1 ELSE 0 END) AS paid_orders,
SUM(CASE WHEN payment_status = 'failed' THEN 1 ELSE 0 END) AS failed_payments,
SUM(CASE WHEN payment_status = 'processing' THEN 1 ELSE 0 END) AS processing_payments
FROM
order_payment_summary
GROUP BY
user_id;
user_id | total_orders | paid_orders | failed_payments | processing_payments
---------+--------------+-------------+-----------------+---------------------
101 | 3 | 1 | 1 | 0
102 | 2 | 1 | 1 | 0
103 | 1 | 1 | 0 | 0
(3 rows)
刷新增量物化视图
增量物化视图的刷新命令和普通的物化视图完全一致。
示例:
-- 更新 ORD10003 的支付状态为成功(之前是失败)
UPDATE payments SET payment_status = 'success', payment_time = '2023-06-02 10:15:00'
WHERE payment_id = 'PAY20003';
-- 刷新 order_payment_summary
REFRESH MATERIALIZED VIEW order_payment_summary;
-- 按用户统计订单和支付情况,我们可以看到 101 user 的 paid_orders 和 failed_payments 已经更新
SELECT
user_id,
COUNT(*) AS total_orders,
SUM(CASE WHEN payment_status = 'success' THEN 1 ELSE 0 END) AS paid_orders,
SUM(CASE WHEN payment_status = 'failed' THEN 1 ELSE 0 END) AS failed_payments,
SUM(CASE WHEN payment_status = 'processing' THEN 1 ELSE 0 END) AS processing_payments
FROM
order_payment_summary
GROUP BY
user_id;
user_id | total_orders | paid_orders | failed_payments | processing_payments
---------+--------------+-------------+-----------------+---------------------
101 | 3 | 2 | 0 | 0
102 | 2 | 1 | 1 | 0
103 | 1 | 1 | 0 | 0
-- 查询刷新历史表,可以看到上面的增量刷新只修改了一条记录
SELECT
oid::regclass, refresh_action, statistics
FROM imv.pg_stat_refresh_history
WHERE oid = 'order_payment_summary'::regclass AND refresh_action = 'incremental';
oid | refresh_action | statistics
-----------------------+----------------+---------------------
order_payment_summary | incremental | {"affected_row": 1}
(1 row)
修改增量物化视图
增量物化视图也可以像普通物化视图一样进行修改,包括修改名字,修改 Access Method 等,详见:ALTER MATERIALIZED VIEW
除此之外,增量物化视图还额外支持了修改查询定义的功能。
ALTER MATERIALIZED VIEW [ IF EXISTS ] view_name AS query;在真实的复杂场景中,多个增量物化视图往往会形成一些依赖关系,比如数仓中的分层逻辑。当我们希望对中间层 MV 做一些简单的修改时(常见的有加字段,去除一些下游不再依赖的字段等),常规手段只能先删除掉物化视图再重新创建。并且,如果这个 MV 被其他下游 MV 引用,在删除的时候我们还需要使用 DROP CASCADE,把所有下游一起删除掉重建。这个会直接影响整个数据链路的可用性。
通过修改物化视图定义的功能,可以做到不影响下游的前提下,对计算逻辑进行迭代。在修改的过程中,引擎会对新老 schema 的兼容性进行校验,并在发现不兼容的情况下进行报错。如果 schema 的修改是兼容的,ALTER 会自动触发一次全量刷新。
示例:
-- 给原来的物化视图增加一列: payment_status_category,用来替代原来的 payment_status 列
ALTER MATERIALIZED VIEW order_payment_summary AS
SELECT
o.order_id,
o.user_id,
o.order_time,
p.payment_id,
p.payment_time,
p.payment_method,
-- p.payment_status,
CASE
WHEN p.payment_status = 'success' THEN 'paid'
WHEN p.payment_status = 'failed' THEN 'payment_failed'
ELSE 'awaiting_payment'
END AS payment_status_category
FROM
orders o
LEFT JOIN
payments p ON o.order_id = p.order_id;
-- 通过 refresh history,能看到这次 alter 语句触发了一次全量刷新
SELECT
oid::regclass, refresh_action, refresh_action_reason, statistics
FROM imv.pg_stat_refresh_history
WHERE oid = 'order_payment_summary'::regclass;
oid | refresh_action | refresh_action_reason | statistics
-----------------------+----------------+-----------------------+---------------------
order_payment_summary | full | alter query | {"affected_row": 6}
删除增量物化视图
删除的命令和全量物化视图一样,详见:DROP MATERIALIZED VIEW
增量 Query 调优指南
增量物化视图虽然支持只对变更的数据进行计算,但由于增量计算本身的复杂性,不同查询语句的增量计算效率差异很大。这里我们总结了一些在真实场景观察得到的一些最佳实践,能够有效的提升增量刷新效率。
非确定性函数
非确定性函数(常见的如 NOW() / CURRENT_TIMESTAMP / CURRENT_DATE)由于结果的不确定性,会给增量计算带来一些额外的复杂度。目前,ProtonBase 只支持在 WHERE 条件里使用这些函数,比如:
SELECT * FROM t WHERE ts >= NOW() - INTERVAL '1 hour';
-- 这种写法会报错
SELECT *,NOW() FROM t;并且为了支持前者的增量计算,每次增量刷新的时候我们还需要对 t 表进行全表扫描,过滤出 在前一次刷新时还合法,但是本次刷新已经不合法的数据。如果底表比较大,建议在 ts 字段上构建一个二级索引,以便能快速筛选出这部分数据。
除了上述常见的非确定性函数,我们还可以通过 current_setting 这个函数配合 set 语句来给增量 MV 传递自定义变量:
-- set 语句除了可以设置内置的 guc 变量外,还可以设定自定义变量,需要定义成 a.b 的形式
set protonbase.watermark = '2025-10-24 14:00:00';
-- 自定义变量可以通过 show 命令来查看
show protonbase.watermark;
protonbase.watermark
----------------------
2025-10-24 14:00:00
(1 row)
-- 也可以通过 current_setting 函数来获取
select current_setting('protonbase.watermark');
current_setting
---------------------
2025-10-24 14:00:00
(1 row)
-- 在 MV 定义中使用 current_setting
CREATE MATERIALIZED VIEW imv_with_setting
WITH (refresh_mode='incremental') AS
SELECT * FROM t WHERE ts >= current_setting('protonbase.watermark')::timestamptz;
-- 在每次 REFRESH 前设置合适的值
set protonbase.watermark = '2025-10-24 14:10:00';
REFRESH MATERIALIZED VIEW imv_with_setting;
set protonbase.watermark = '2025-10-24 14:20:00';
REFRESH MATERIALIZED VIEW imv_with_setting;GROUP BY 语句
Primary Key
Group By MV 常见用途是按维度做点查(例如按某个 group key 查一行聚合结果),因此用户通常期望:Group By 的字段能自动成为 MV 的主键。
但数据库的 Primary Key 有一个硬要求:主键列必须是 NOT NULL。如果 Group By 字段可能为 NULL,系统就不能安全地自动声明成主键。
推荐按以下思路处理:
-
业务允许时,将 Group By 的字段在底表上申明成
NOT NULL
这是最清晰的做法,也能减少 “NULL维度代表什么” 的歧义。 -
不方便改源表约束,但业务上不需要
NULL作为维度值
可以在 MV 定义里过滤掉NULL,让系统推断该 key 在结果中为NOT NULL,从而自动生成主键:
SELECT ...
FROM T
WHERE key IS NOT NULL
GROUP BY key;- 业务必须保留
NULL维度值,同时又需要点查
这时主键无法包含该列(因为主键不允许 NULL),建议在 MV 上创建 secondary index(二级索引) 来提供点查访问路径。
Group By 字段顺序
Group By 字段往往会成为 MV 的主键(即使不是主键也会直接影响主键的值),因此字段顺序会决定主键排序与数据分布。在默认 range 分布 下,这会直接影响增量刷新时写入是否“集中到一段 key range”,从而影响写入的并行程度。 举例:
-- ts 为分钟级时间字段,通常随业务时间递增
SELECT SUM(a)
FROM T
GROUP BY ts, b;如果同一分钟内 b 的取值非常多(高基数),那么 MV 的主键会按 ts 先聚集:同一分钟的数据挤在一起。 当新的增量数据到来时(通常是更大的 ts),写入会倾向落在主键空间的尾部连续区间。range 分布下容易形成写入热点,导致增量刷新并发受限。
因此建议将更能打散写入的高基数字段放前面:
SELECT SUM(a)
FROM T
GROUP BY b, ts;这样增量到来时,会分散写入到不同的 b 分组区域,更容易形成多个并行写入点,通常能获得更好的刷新吞。
小结:
- 单调递增字段(时间戳、递增 ID)不要放在 Group By 的第一位
- 优先把高基数、离散度高的维度放在前面,把时间类字段放后面作为次级排序
多组 Count Distinct
一个业务上常见的需求是在聚合的时候根据不同的过滤条件来计算 Count Distinct,比如计算不同口径的 UV。一种常见的写法是:
SELECT
COUNT(DISTINCT CASE WHEN x = 1 THEN user_id ELSE NULL) AS uv1,
COUNT(DISTINCT CASE WHEN x = 2 THEN user_id ELSE NULL) AS uv2,
COUNT(DISTINCT CASE WHEN x = 3 THEN user_id ELSE NULL) AS uv3,
...上述的例子中 count distinct 的目标字段都是 user_id,只是过滤条件不同。这种情况我们建议更换一种等价的写法,可以让增量计算更加高效:
SELECT
COUNT(DISTINCT user_id) FILTER (WHERE x = 1) AS uv1,
COUNT(DISTINCT user_id) FILTER (WHERE x = 2) AS uv2,
COUNT(DISTINCT user_id) FILTER (WHERE x = 3) AS uv3,
...JOIN 语句
当增量物化视图(IMV)的定义包含 Join 时,增量刷新的成本主要取决于两点:变更数据量(Δ)有多大,以及系统是否能用高效方式把大表与 Δ 做匹配。下面给出三条常用且有效的优化建议。
优先使用 Inner Join
从功能上,我们支持 INNER / LEFT OUTER / RIGHT OUTER / FULL OUTER 的增量计算。但从效率来看,Outer Join 通常比 Inner Join 开销更大,尤其当 Outer 表(例如 LEFT OUTER JOIN 的右表)变更较为频繁时,增量计算需要处理更多“补齐/撤销/空值扩散”等逻辑,影响范围更大,刷新成本更高。
建议:
- 业务语义允许时,尽量使用
INNER JOIN - 如确需 Outer Join,建议重点关注 Outer 表的变更频率与刷新周期,避免频繁刷新导致资源抖动
使用等值条件
带等值条件的 Join(例如 S.k1 = T.k2)更容易被优化:
- 可以使用
Hash Join / Merge Join等经典算法加速 - 也为后续利用索引进行 Lookup 加速提供前提(见下一条)
如果 Join 只有非等值条件(例如范围条件或复杂表达式),通常会降低优化空间,并增加增量计算的扫描成本。
建议:
- 每个 Join 尽量至少包含一个等值条件
- 其他条件可以作为附加过滤(
AND ...)保留
利用二级索引
考虑如下 Join 场景:
S INNER JOIN T ON S.k1 = T.k2其中 S 是大表,T 是小表。增量刷新时,小表 T 的变更会产生一小批变更数据 ΔT。此时增量计算本质上是在运行:
S INNER JOIN ΔT ON S.k1 = ΔT.k2如果大表 S 在 k1 字段上构建了二级索引,系统就可以对 ΔT 的每一行通过索引进行 Lookup 来定位匹配行,避免扫描整张 S,从而显著降低增量刷新成本。
为了让索引生效,等值条件应尽量直接使用原始列。下面两类写法常见但会破坏优化,使执行计划退化为扫描或更慢的匹配方式:
- 在 Join key 上做类型转换(类型不匹配时临时
cast):
S.k1::text = T.k2- 在 Join key 上使用函数加工:
UPPER(S.k1) = T.k2推荐做法:
- 尽量保证 Join 两侧字段类型一致,避免在 Join 条件中临时
cast - 如需做规范化(
upper/lower/trim等),建议提前将结果计算好后再参与 Join(例如新增预处理列、生成列,或在 ETL 中提前加工好):
S.k1_norm = T.k2_norm小结:
- 优先使用
INNER JOIN:增量维护更简单、影响范围更小、成本更可控 - 每个 Join 至少包含一个等值条件:更容易采用高效 Join 算法,也更利于索引加速
- 大表 Join Key 建二级索引,并避免在 Join 条件里对 key 做
cast或函数加工,以确保索引可用
TOP N 语句
业务上 Top-N 的典型写法是:
SELECT a, b, c
FROM (
SELECT
a, b, c,
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) AS rn
FROM t
)
WHERE rn = 1; -- Top-1
-- 或者:WHERE rn <= 10; -- Top-10说明:
PARTITION BY定义分区(例如按用户、商品、业务 key 分组)ORDER BY定义排序规则(例如按时间倒序、版本号倒序)rn = 1表示每个分区只保留第一条;rn <= N表示保留前 N 条
其中,Top-1 最常见的目的是“按 key 去重“,尤其是在已有的离线计算场景。因为大部分大数据系统缺少数据库意义上的 Primary Key 约束,会保留所有未去重的明细数据,只有在需要唯一结果时才用 Top-1(rn = 1)取“每个 key 的一条记录”。
对于这种“Top-1 只是为了去重”的场景,通常可以用 主键表 + Upsert 直接替代 Top-1。在数据同步入 ProtonBase 前就在表上定义好 Primary Key,并采用 INSERT ON CONFLICT DO UPDATE(UPSERT) 的方式写入数据:
- 同一个主键的新数据写入时覆盖(或按规则更新)旧版本
- 表在系统内天然就是“按 PK 去重后的结果”
- 下游查询不再需要 Top-1 去重
收益:
- 降低计算成本:避免在查询中做排序/窗口/过滤
- 简化查询逻辑:去重语义由主键约束与写入规则保证
- 增量更稳定:下游的增量只需要处理 Upsert 带来的变更,不用再计算 Top-1
Optimize Notice
为了帮助你在创建阶段就获得更好的增量刷新性能,ProtonBase 会在创建增量 MV 时对查询进行分析,并在需要时通过 NOTICE 给出一些调优的建议(最常见的是建议创建二级索引)。 这些 NOTICE 不会改变执行结果,也不会自动创建索引;它们是基于查询结构与增量计算路径的建议,你可以按需采纳。
示例:
CREATE TABLE t (a int, b int);
CREATE MATERIALIZED VIEW imv_join
WITH (refresh_mode='incremental') AS
SELECT t1.a, t2.b
FROM t AS t1, t AS t2
WHERE t1.a = t2.a;系统可能输出:
NOTICE: suggested index: CREATE INDEX ON t (b ASC)
NOTICE: suggested index: CREATE INDEX ON t (a ASC)- 该查询包含 Join 条件 t.b = t1.a
- 增量刷新时,系统往往需要用变更数据(Δ)去匹配另一侧表
- 在 Join Key 上建立二级索引,可以让增量计算通过索引 Lookup 加速,避免扫描大表
总结:
- 先创建 MV,观察创建阶段输出的 NOTICE
- 评估建议是否适用
- 表是否为大表、增量是否频繁、该 MV 是否是热点
- 索引会带来额外写入与存储开销,建议结合负载选择
- 按需创建索引(可直接复制 NOTICE 给出的语句)
- 验证效果
- 对比增量刷新耗时、资源使用、以及 MV 查询延迟
自动化运维建议
- 利用调度系统(如 Airflow、crontab)定时刷新物化视图,结合业务低峰期执行。
- 监控刷新耗时、失败率、延迟等指标,及时告警。
- 利用系统表(如
pg_matviews、pg_depend)自动分析依赖关系,避免因基础表变更导致刷新失败。 - 定期清理不再使用的物化视图,节省存储和维护成本。
实践案例
案例一:用户增长分析
假如我们需要评估一些市场推广或广告投放的效果,新用户数量是一个很好的指标。判断一个用户是否是新用户,可以通过他是否在过去 30 天内在我们的网站内有任何活动来判断。我们需要类似下面这样的查询:
SELECT COUNT(DISTINCT user_id)
FROM user_activities
WHERE pt = current_date
AND user_id not in (
-- 统计过去30天出现过的所有用户
SELECT DISTINCT user_id
FROM user_activities
WHERE pt >= current_date - internal '30 day' AND pt < current_date AND pt)我们注意到"统计过去 30 天出现过的所有用户"这样的查询计算量还是比较大的,并且结果基本不变(假设不考虑 user_activities 有一些迟到的数据)。如果新用户分析的查询经常发生,我们就可以考虑把 "统计过去 30 天出现过的所有用户" 这部分结果使用物化视图的方式存储起来。考虑到这个查询只需要每天运行一次,我们可以选择全量物化视图,在每天凌晨全量刷新一下结果即可。
类似的思路,也可以扩展到用户留存分析,不同渠道新用户增长分析等场景。
案例二:数仓分层
在典型的数仓架构中,我们一般都会采取分层策略,比如:
- ODS(明细数据层):也称贴源层,这里存储最原始未加工的数据,后后续数据加工的来源。
- DWD(数据基础层):主要对 ODS 层的数据做一些清洗/过滤/规范化的加工操作,也经常涉及到宽表的加工等
- DWS / ADS(数据服务层):基于 DWD 层的数据,做不同程度的汇总操作。
离线数仓和实时数仓的主要区别,就在于每一层的时效性,即最原始的数据变化,在多少时间后会体现在这一层的内容里。从模型上来讲,除去 ODS 层之外,其他层的数据都可以看作是某种意义上的物化视图,定期根据数据的依赖关系进行刷新操作。
结合 ProtonBase 强大的存储和查询能力,我们可以构建一个离线和实时一体化的数仓架构:
- ODS
- 由外部实时写入或由工具实时同步进入 Protonbase
- 一些最灵活的业务场景(如数据分析师验证想法等)可以直接查询 ODS 数据
- DWD
- 按不同的时效性需求,基于 ODS 层数据构建物化视图。通过
pg_depend,pg_class等系统表,可以自动分析出物化视图和表之间的依赖关系 - 小时/天级时效性:构建全量物化视图,定期按照依赖关系全量刷新
- 分钟级时效性:构建增量物化视图,支持常见的过滤,多表关联等,定期按照依赖关系增量刷新
- 按不同的时效性需求,基于 ODS 层数据构建物化视图。通过
- DWS/AWS:
- 根据不同的业务场景需要,基于 DWD 和 ODS 层构建物化视图。
- 一些较为固定的查询 pattern:可以构建一些轻度聚合的物化视图,直接在上面做二次聚合分析或者点查。
- 灵活性很高的查询:可以不使用物化视图,直接查询 ODS 和 DWD 层的数据
通过这个架构,我们不需要为离线和实时各准备一套完整的数据架构,只需要根据时效性和查询灵活性等需求在 Protonbase 里创建合理的表和物化视图等,即可同时满足离线和实时的需求,极大的降低了成本,也提高了易用性。
案例三:金融风控
在金融行业中,实时风控是保障业务安全的重要环节。通过增量物化视图,可以实现对用户交易行为的实时监控和风险识别。
业务场景: 银行或支付平台需要实时监控用户的交易行为,识别异常交易模式,防止欺诈行为。例如,监控用户按小时、按天的交易频次、交易金额分布等指标,当这些指标超过预设阈值时,触发风控策略。
技术实现:
-- 创建基础增量物化视图,按时间粒度聚合用户交易数据
CREATE MATERIALIZED VIEW user_transaction_base
WITH (refresh_mode = 'incremental') AS
SELECT
user_id,
DATE_TRUNC('hour', transaction_time) as transaction_hour,
DATE_TRUNC('day', transaction_time) as transaction_day,
COUNT(*) as transaction_count,
SUM(amount) as total_amount,
AVG(amount) as avg_amount,
MAX(amount) as max_amount,
COUNT(DISTINCT merchant_id) as merchant_count
FROM transactions
GROUP BY user_id, DATE_TRUNC('hour', transaction_time), DATE_TRUNC('day', transaction_time);
-- 创建高级物化视图,基于基础物化视图进行进一步聚合
CREATE MATERIALIZED VIEW user_risk_metrics
WITH (refresh_mode = 'incremental') AS
SELECT
user_id,
transaction_day,
SUM(transaction_count) as daily_transaction_count,
SUM(total_amount) as daily_total_amount,
AVG(avg_amount) as daily_avg_amount,
MAX(max_amount) as daily_max_amount,
AVG(transaction_count) as hourly_avg_transactions,
STDDEV(transaction_count) as transaction_count_stddev
FROM user_transaction_base
GROUP BY user_id, transaction_day;优势:
- 分层聚合:通过基础物化视图和高级物化视图的分层设计,提高查询效率
- 多时间粒度:支持按小时、按天等不同时间粒度的统计分析
- 实时性:增量物化视图能够实时反映用户最新的交易行为
- 性能:避免每次风控检查时都进行全表扫描和实时计算
应用场景:
- 异常高频交易检测(如短时间内大量小额交易)
- 大额交易监控(单笔或累计大额交易)
- 交易模式异常识别(与历史模式偏差较大)
- 黑名单商户交易监控
案例四: IoT 设备监控
在物联网(IoT)场景中,通常需要处理来自数百万设备的传感器数据,通过增量物化视图可以高效地进行数据聚合和实时监控。
业务场景: 工业物联网中,需要实时监控各类设备的运行状态,包括温度、压力、振动等传感器数据。通过对这些数据进行实时聚合分析,可以及时发现设备异常,预防设备故障。
技术实现:
-- 创建基础增量物化视图,按设备和时间粒度聚合传感器数据
CREATE MATERIALIZED VIEW device_sensor_base
WITH (refresh_mode = 'incremental') AS
SELECT
device_id,
device_type,
DATE_TRUNC('hour', reading_time) as reading_hour,
DATE_TRUNC('day', reading_time) as reading_day,
AVG(temperature) as avg_temperature,
MAX(temperature) as max_temperature,
MIN(temperature) as min_temperature,
AVG(pressure) as avg_pressure,
STDDEV(vibration) as vibration_stddev,
COUNT(*) as reading_count,
COUNT(CASE WHEN temperature > 80 THEN 1 END) as high_temp_count
FROM sensor_readings
GROUP BY device_id, device_type, DATE_TRUNC('hour', reading_time), DATE_TRUNC('day', reading_time);
-- 创建高级物化视图,基于基础物化视图计算设备健康指标
CREATE MATERIALIZED VIEW device_health_metrics
WITH (refresh_mode = 'incremental') AS
SELECT
device_id,
device_type,
reading_day,
AVG(avg_temperature) as daily_avg_temperature,
MAX(max_temperature) as daily_max_temperature,
AVG(vibration_stddev) as daily_avg_vibration_stddev,
SUM(high_temp_count) as daily_high_temp_count,
COUNT(*) as hourly_readings_count,
-- 计算设备异常率
SUM(CASE WHEN high_temp_count > 0 THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as high_temp_rate
FROM device_sensor_base
GROUP BY device_id, device_type, reading_day;优势:
- 分层处理:基础物化视图处理原始数据,高级物化视图计算健康指标
- 多维度分析:支持按设备类型、时间粒度等多维度统计
- 高效处理:能够处理数百万设备的海量传感器数据
- 异常检测:快速识别异常设备,支持预防性维护
应用场景:
- 设备健康状态监控
- 异常率统计和趋势分析
- 预防性维护决策支持
- 设备性能基准比较
案例五:日志分析
在大规模分布式系统中,日志分析是保障系统稳定性和安全性的重要手段。通过增量物化视图,可以实现对日志数据的实时聚合和分析。
业务场景: 互联网公司需要实时监控系统日志,统计各类错误、告警、访问量等指标,以便及时发现系统问题并驱动自动化运维和安全响应。
技术实现:
-- 创建基础增量物化视图,按服务和时间粒度聚合日志数据
CREATE MATERIALIZED VIEW log_base_metrics
WITH (refresh_mode = 'incremental') AS
SELECT
service_name,
DATE_TRUNC('hour', log_time) as log_hour,
DATE_TRUNC('day', log_time) as log_day,
log_level,
COUNT(*) as log_count,
COUNT(CASE WHEN log_level = 'ERROR' THEN 1 END) as error_count,
COUNT(CASE WHEN log_level = 'WARN' THEN 1 END) as warn_count,
COUNT(CASE WHEN log_level = 'INFO' THEN 1 END) as info_count,
COUNT(DISTINCT host_ip) as active_hosts,
APPROX_COUNT_DISTINCT(user_id) as affected_users
FROM application_logs
GROUP BY service_name, DATE_TRUNC('hour', log_time), DATE_TRUNC('day', log_time), log_level;
-- 创建高级物化视图,基于基础物化视图计算系统健康度指标
CREATE MATERIALIZED VIEW system_health_metrics
WITH (refresh_mode = 'incremental') AS
SELECT
service_name,
log_day,
log_hour,
SUM(log_count) as total_logs,
SUM(error_count) as total_errors,
SUM(warn_count) as total_warnings,
-- 计算错误率
CASE
WHEN SUM(log_count) > 0 THEN SUM(error_count) * 100.0 / SUM(log_count)
ELSE 0
END as error_rate,
-- 计算警告率
CASE
WHEN SUM(log_count) > 0 THEN SUM(warn_count) * 100.0 / SUM(log_count)
ELSE 0
END as warning_rate,
MAX(active_hosts) as peak_active_hosts,
SUM(affected_users) as total_affected_users
FROM log_base_metrics
GROUP BY service_name, log_day, log_hour;优势:
- 分层聚合:基础物化视图处理原始日志,高级物化视图计算健康指标
- 多时间粒度:支持按小时、按天等时间粒度的统计分析
- 实时监控:实时反映系统最新状态和问题
- 性能优化:避免每次分析时都扫描大量日志数据
应用场景:
- 系统健康度监控
- 错误和告警实时统计
- 用户访问量分析
- 安全事件检测(如异常登录、恶意请求等)
- 性能瓶颈识别
常见问题与排查
- 刷新失败:检查基础表/视图权限、依赖对象是否存在、SQL 语法是否兼容增量物化。
- 数据不一致:关注基础表延迟、迟到数据、并发写入等场景,必要时采用全量重刷。
- 性能瓶颈:分析物化视图定义的 SQL 复杂度、索引覆盖情况、存储方式(行/列/混合)是否合理。
- 依赖变更:基础表结构变更后,需同步调整物化视图定义,避免刷新异常。
排查建议:
- 刷新失败优先检查依赖对象和权限。
- 数据不一致时关注迟到数据和并发写入,必要时全量重刷。
- 性能瓶颈优先分析 SQL 复杂度和索引覆盖。
- 基础表变更需同步调整物化视图定义。