物化视图原理与实践

举报
leapdb 发表于 2026/01/27 12:09:01 2026/01/27
【摘要】 详细讲解物化视图的原理和相关实践

物化视图(Materialized View)是通过预先计算来存储查询结果的数据库对象,物化视图将结果集物理化存储在实体表中,查询时直接读取预存数据,大幅提升复杂查询的响应速度,适用于读多写少的场景。

01.PNG

物化视图既在pg_matviews存储视图定义信息,也在pg_class存储实体表定义信息,对应一张物理表,存储物化视图数据。

查询重写时,根据pg_matviews中的视图定义,检查是否可以从物化视图中直接读取预计算数据。

基表数据写入后,可手动或者定时刷新物化视图数据。

 

名词解释

基表:物化视图中SQL查询的表。

失效:基表发生数据变化后,物化视图会被标记成失效状态。可直查,不可重写。

刷新:将物化视图的SQL重新全量或增量执行,更新物化视图数据。

重写:数据库优化器对SQL的查询重写阶段,将用户SQL等价改写成更高效的形式。

原理介绍

基本原理

1. 数据预计算:基于指定的基础查询语句(可包含关联、聚合、过滤等操作),在物化视图创建/刷新时,将查询结果一次性计算并存储为物理表,数据与基础表解耦。

2. 数据一致性:基础表数据发生变更(增删改)时,物化视图的预存数据会过期,需通过刷新机制同步基础表变化,平衡查询性能与数据实时性。

3. 透明加速:当用户SQL与物化视图SQL匹配时,查询重写会自动选择从物化视图读取数据。无需修改业务SQL,即可实现复杂查询的响应时间,降低系统负载。

简单来说,物化视图是“空间换时间”的典型优化方案,以额外的存储开销和少量的刷新性能损耗,换取复杂查询的毫秒级响应。

功能特点

  • 声明式编程:通过物化视图的SQL完成数据加工,不必再编写复杂的存储过程。
  • 增量计算:除了表级全量刷新,分区全量刷新,还支持表级增量刷新,分区级增量刷新。对于增量很少的场景,会比全量刷新性能更好。
  • pipleline流式加工:将物化试图嵌套使用,即可数据的层层向上流转,完成不同主题域的数据自动加工。
  • 跨源查询:物化视图可以从多个不同数据源,将数据定时汇总到视图中。
  • 数仓分层:将明细层(DWD),基础层(DWB),汇总层(DWS)和应用服务层,可借用物化视图进行分层管理。

技术优势

  • 失效轻量化:基表数据变化后触发物化视图失效,不影响基表并发写入,不重复失效。
  • 锁轻量化:基表写入不堵塞刷新,刷新也不堵塞业务查询。
  • 可靠失效:CN故障不影响物化视图的失效和刷新。
  • 合理级联锁:对失效、刷新和基表DDL,进行统一设计,任意交叉依赖和嵌套,无死锁。
  • 自动级联刷新:自底向上级联刷新每一个物化视图,全链路保障数据实时一致。

功能约束

  全量刷新 增量刷新
基表类型 内表,外表,普通视图,物化视图 内表,物化视图
查询约束 无约束

支持agg, join, where, group by, having等

不支持subquery, union all, window agg, cte

存储格式 行列存都可以 行列存都可以
表分区 支持 支持
查询重写 支持 支持
刷新间隔 天、小时级 分钟、秒级
刷新代价 基表数据量 增量大小,计算复杂度
弹性计算 支持 支持

全量刷新

刷新物化视图时,重新执行视图定义中的SQL,将根据最新数据计算的全量结果,替换物化视图中的旧数据。

CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] materialized_view_name
[ ( column_name [, ...] ) ] --可选,自定义各列别名
[ BUILD { DEFERRED | IMMEDIATE } ] --可选,创建后再刷新或创建时立即刷新数据
[ REFRESH [ [DEFAULT] | [ RESTRICT ] | [ CASCADE FORWARD ] | [ CASCADE BACKWARD ] | [ CASCADE ALL ] ] [ COMPLETE | FAST ] [ ON DEMAND ] 
[ [ START WITH (timestamptz) ] | [ EVERY (interval) ] ] ] --定义数据的定时或级联刷新方式
[ { ENABLE | DISABLE } QUERY REWRITE ] --可选,是否用于查询重写
[ WITH ( {storage_parameter = value} [, ... ] ) ] --表格式相关参数,推荐WITH(orientation=COLUMN, enable_hstore_opt=ON)
[ TABLESPACE tablespace_name ]
[ DISTRIBUTE BY { REPLICATION | ROUNDROBIN | { HASH ( column_name [,...] ) } } ]
[PARTITION BY { 
        {RANGE (partition_key) ( partition_less_than_item [, ... ] )} |
        {RANGE (partition_key) ( partition_start_end_item [, ... ] )}]
AS query;

创建样例:

CREATE MATERIALIZED VIEW test_mv1
REFRESH COMPLETE EVERY (interval '00:01:00') --每分钟刷新一次
ENABLE QUERY REWRITE --开启查询重写
WITH(orientation=COLUMN, enable_hstore_opt=ON) --使用列存hstore_opt存储格式
AS Your_Select;

全量刷新对业务SQL没有限制,刷新时内部通过insert overwrite完成数据写入,仅在最后交换数据文件的瞬间加锁与查询互斥。

数据量本身不大,秒级完成的SQL,建议优先使用全量刷新。

增量刷新

刷新物化视图时,从基表的binlog中抽取增量数据,并根据增量算法计算出增量结果,替换物化视图中的部分数据。

2.png

创建样例:

CREATE MATERIALIZED VIEW test_mv1
REFRESH FAST EVERY (interval '00:00:30') --每30秒刷新一次
ENABLE QUERY REWRITE --开启查询重写
WITH(orientation=COLUMN, enable_hstore_opt=ON) --使用列存hstore_opt存储格式
AS Your_Select;

不适用增量刷新的场景

subquery, union all, window agg, cte。

 

增量刷新会自动转全量的场景

  • 基表发生DDL
  • 无group by的全表聚合
  • 手动强制restrict刷新
  • 集群扩容重分布期间的刷新
  • 开启了GTM Free模式

 

增量算法

3.png


根据多表JOIN计算出增量合并的部分,不限制JOIN表的数量。

查询重写

单表的点查,可以创建索引进行查询加速;

多表的复杂查询,还可以创建物化视图,实现透明加速,无需改造业务。

相当于省去了SQL的执行时间,直接从物化视图中取数据,大大加速业务。

 

1. 支持全文匹配的查询重写。

2. 支持查询条件的代数逻辑推理,如果物化视图中的数据能满足查询,则优先使用物化视图的数据。

3. 支持对子查询的物化视图重写。

 

查询重写原理

4.png

选型策略

选择适合物化视图的场景

物化视图本质上,通过“相似归一”的思想,减少重复计算。


一般我们可以从TOPSQL中分析历史SQL,总结归纳相似计算。

  • 通过unique_sql_id统计同类语句使用频率,将同类语句中的公共部分提取为物化视图。
  • 通过具体大表,按query字段like该表上的SQL,分析近似计算。

往往把物化视图提取的更基础,更简单,复用频率更高,越能带来更大的收益。

设计合理,减少了重复计算,可以降本增效;反之,可能增加系统负担。

 

通过pv_matview_detail视图,可以评估物化视图使用效率。

选择合理的物化视图定义

系统会根据物化视图SQL查询的表头,自动推导物化视图的列名,但还是建议给每列指定列名,避免后面调优需要修改列名。

 

物化视图表级属性

  • bitmap_columns 指定bitmap列,可以给物化视图创建bitmap索引,加速使用视图的查询。
  • excluded_inactive_tables 指定哪些表的修改,不触发物化视图失效。例如一些维度表的变化,如果对视图结果无影响,可以进行设置。
  • mv_analyze_mode; 控制物化视图刷新后自动analyze方式,一般自动做,且不堵塞查询
  • mv_computing_resource 给物化视图刷新指定的弹性资源池 
  • mv_incremental_mode; 指定增量物化视图的格式
  • mv_pck_column; --指定pck列,给物化视图创建PCK,加速使用视图的查询。
  • mv_refresh_guc; --刷新使用的GUC参数,可以设定各种GUC参数,确保刷新的计划稳定,例如禁止nestloop等。
  • mv_support_function_type; --开启物化视图创建时查询语句中可以使用的函数属性

5.png


如果对物化视图上的查询性能要求较高,可以利用mv_pck_column,bitmap_columns进行优化。


如果需要使用不稳定函数,可以设置mv_support_function_type

如果需要给物化视图单独设置GUC参数,可以使用mv_refresh_guc

选择合适的存储格式

存储格式

根据业务使用场景,选择行存或列存。

高频点查的场景 建议使用行存,并用索引进行加速。
批量分析的场景 检视使用列存。

 

分布方式

根据物化视图的结果选择合理的分布方式。

物化结果10w条以下的 建议选择复制表方式
物化结果10w条以上的 建议选择hash分布的方式
没有能够均匀分布的hash键 可以使用roundrobin分布方式

设计合理的刷新方式

刷新方式

功能约束 全量刷新 增量刷新
基表类型 内表,外表,普通视图,物化视图 内表,物化视图
查询约束 无约束

支持agg, join, where, group by, having等

不支持subquery, union all, window agg, cte

刷新间隔 天、小时级 分钟、秒级
刷新代价 基表数据量 增量大小,计算复杂度

 

选全量刷新,还是增量刷新?

参考维度 推荐全量的场景 推荐增量的场景
基本约束 无法支持增量的SQL 增量全部支持的SQL
基表变化 频率很低,经常批量更新或发生DDL 频率很高且都是少量修改(30%以下)
时效要求 天或小时级即可满足 要求分钟级或秒级读到最新数据
SQL复杂度 较复杂增量会慢 较简单不影响增量

 

自动刷新间隔

可根据业务实际的实时性要求,按天,小时,30min,分钟,秒的粒度安排物化视图的自动刷新间隔。

  • 跑批业务可以手动刷新,或者按小时级刷新。
  • 实时业务可以按分钟或秒级刷新。

    6.png


最好能错峰刷新,防止集中刷新导致CPU过高。可以通过START WITH和EVERY进行控制。

实时性需要与业务需求相匹配,过度频繁的刷新会消耗太多系统资源。

 

刷新级联

嵌套使用的物化视图,建议级联刷新的方式,自底向上一次完成整个链路的刷新。减少各层非必要的重复刷新。

底层物化视图设置cascade和erery定时,上层物化视图不设置定时即可。

7.png


刷新稳定性

如果SQL比较复杂,存在计划不稳定的可能性,建议通过hint将计划固定住。防止刷新的计划跳变导致刷新时间超过刷新间隔,出现积压。

 

注意事项总结

  • 防CPU突高:刷新任务错峰编排,避免集中刷新。由START WITH和EVERY进行设定。
  • 防计划跳变:执行计划尽量hint固定,将查询计划调至最优,减少计划跳变对轮询刷新的影响。 
  • 防重复刷新:按数据流管道级联刷新,减少重复刷新。CASCADE BACKWARD控制。
  • 最佳的性能:建议使用hstore_opt格式
  • 合理的间隔:顶层视图刷新间隔,必须大于下层最大间隔,避免数据快照不一致。

8.png

下层mv1每5分钟刷新一次,上层mv2每2分钟刷新一次,上层刷新间隔小于下层,上层即使刷新看到的也是下层旧的数据。


实践案例

业务场景

按不同的业务场景,可以分为以下几类物化视图。

聚合物化视图

主要解决聚合加速问题。

CREATE MATERIALIZED VIEW mv_order_region_stats
AS
SELECT
    region,                          -- 聚合维度:地区
    COUNT(*) AS order_count,         -- 指标1:该地区订单总数
    SUM(order_amount) AS total_amount -- 指标2:该地区订单总金额
FROM t_order
GROUP BY region; -- 单表聚合,无多表关联

JOIN物化视图

主要解决多表关联加速问题。

CREATE MATERIALIZED VIEW mv_user_order_stats
AS
SELECT
    u.user_level,
    u.region,
    COUNT(o.order_id) AS order_count,
    SUM(o.order_amount) AS total_amount
FROM t_order o
INNER JOIN t_user u ON o.user_id = u.user_id
GROUP BY u.user_level, u.region;

分区物化视图

主要解决超大事实表的查询加速问题。

9.png

--创建事实表
CREATE TABLE fact_table (start_time TIMESTAMPTZ NOT NULL, id int)
PARTITION BY RANGE(start_time) (
    PARTITION p1 VALUES LESS THAN ('2024-01-01'),
    PARTITION p2 VALUES LESS THAN ('2024-02-01'),
    PARTITION p3 VALUES LESS THAN ('2024-03-01'),
    PARTITION p4 VALUES LESS THAN ('2024-04-01'),
    PARTITION p5 VALUES LESS THAN ('2024-05-01'),
    PARTITION p6 VALUES LESS THAN ('2024-06-01')
);

--创建维度表
CREATE TABLE dimen_table (start_time TIMESTAMPTZ NOT NULL, id INT);

--等比例方式,创建与事实表分区完全一样的分区物化视图
CREATE MATERIALIZED VIEW level_normal enable query rewrite DISTRIBUTE BY HASH(start_time) PARTITION BY start_time
AS SELECT fact_table.start_time, dimen_table.id FROM fact_table JOIN dimen_table ON fact_table.id = dimen_table.id;

--按天上卷方式,根据事实表start_time分区键,创建分区物化视图
CREATE MATERIALIZED VIEW level_day enable query rewrite DISTRIBUTE BY HASH(start_time) PARTITION BY date_trunc('day', start_time)
AS SELECT fact_table.start_time, dimen_table.id FROM fact_table JOIN dimen_table ON fact_table.id = dimen_table.id;

--按月上卷方式,根据事实表start_time分区键,创建分区物化视图
CREATE MATERIALIZED VIEW level_month enable query rewrite DISTRIBUTE BY HASH(start_time) PARTITION BY date_trunc('month', start_time)
AS SELECT fact_table.start_time, dimen_table.id FROM fact_table JOIN dimen_table ON fact_table.id = dimen_table.id;

--按年上卷方式,根据事实表start_time分区键,创建分区物化视图
CREATE MATERIALIZED VIEW level_year enable query rewrite DISTRIBUTE BY HASH(start_time) PARTITION BY date_trunc('year', start_time)
AS SELECT fact_table.start_time, dimen_table.id FROM fact_table JOIN dimen_table ON fact_table.id = dimen_table.id;

刷新时无需指定分区,内核会自动识别需要刷新的失效分区。

为了解决分区刷新实时性问题,可以设置 mv_refresh_parts_per_trans 由近及远分批刷新提交,快速完成最近分区的刷新。

如果需要手动快速干预,也可以直接手动指定分区刷新。手动与自动不冲突,一方在进行另一方会自动跳过,一方完成另一方拿到锁后也会自动跳过,防止重复浪费资源。

嵌套物化视图

主要解决实时数据流或相似查询统一问题。

CREATE TABLE t_user (
    user_id BIGSERIAL PRIMARY KEY, 
    user_name VARCHAR(32) NOT NULL,
    user_level VARCHAR(10) NOT NULL,
    region VARCHAR(20) NOT NULL,
    create_time TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE TABLE t_order (
    order_id BIGSERIAL PRIMARY KEY,
    order_no VARCHAR(32) NOT NULL,
    user_id BIGINT NOT NULL,
    order_amount NUMERIC(10,2) NOT NULL,
    create_time TIMESTAMP NOT NULL DEFAULT NOW(),
);

第一层物化视图,按照用户等级和地区,计算订单总数和总金额

CREATE MATERIALIZED VIEW mv_user_order_stats
AS
SELECT
    u.user_level,
    u.region,
    COUNT(o.order_id) AS order_count,
    SUM(o.order_amount) AS total_amount
FROM t_order o
INNER JOIN t_user u ON o.user_id = u.user_id
GROUP BY u.user_level, u.region;

第二层物化视图,按照用户等级全局聚合

CREATE MATERIALIZED VIEW mv_user_level_global_stats
AS
SELECT
    user_level,
    SUM(order_count) AS total_order_count,
    SUM(total_amount) AS total_order_amount
FROM mv_user_order_stats
GROUP BY user_level;

第二层物化视图,按照地区全局聚合

CREATE MATERIALIZED VIEW mv_region_global_stats
AS
SELECT
    region,
    SUM(order_count) AS total_order_count,
    SUM(total_amount) AS total_order_amount
FROM mv_user_order_stats
GROUP BY region;

10.png

既需要按地区统计订单总数和总金额,又需要按用户统计订单总数和总金额。

先创建一个按照“地区和用户”聚合的中间层物化视图,再分别按地区和用户聚合,这样省去了基表的重复关联计算。

行业案例

游戏行业战力排名

业务诉求:游戏行业中,玩家战力 / 积分 / 等级排名是高频核心功能(如全服战力榜、单服职业榜、日积分 TOP 榜),直接基于百万级玩家单表执行 ORDER BY + LIMIT 实时排序,会触发全表扫描 + 排序,响应时间长达 秒级,严重影响玩家体验。

解决方案:基于玩家战力单表创建物化视图,提前存储「可排序的核心字段」并创建复合索引,将排名查询的响应时间降至 毫秒级;结合高频增量刷新(1~5 分钟一次),平衡「实时性」与「性能」,完美适配游戏准实时排名需求。

方案 实时性 查询性能 数据库压力 适用场景
直接查基表排序 实时 差(秒级) 高(全表扫描) 测试服 / 小服(玩家 < 1 万)
物化视图 + 索引 准实时(延迟 1~5 分钟) 优(毫秒级) 低(索引扫描) 正式服 / 大服(玩家 > 10 万)
-- 游戏玩家战力核心表(500万级数据,优化字段+基础索引)
CREATE TABLE game_player_combat (
    player_id BIGSERIAL PRIMARY KEY,       -- 玩家唯一ID(主键,自增)
    player_name VARCHAR(32) NOT NULL,      -- 玩家角色名(不可重复)
    server_id INT NOT NULL,                -- 服务器ID(1~20服,区分不同服务器)
    profession VARCHAR(20) NOT NULL,       -- 职业(战士/法师/刺客/牧师/弓手)
    combat_power BIGINT NOT NULL,          -- 战力值(排名核心,1万~200万)
    title VARCHAR(20) NOT NULL DEFAULT '见习', -- 头衔(按战力梯度分配)
    level INT NOT NULL,                    -- 玩家等级(1~150级)
    update_source VARCHAR(30) NOT NULL,    -- 战力更新来源(装备强化/技能升级/宠物进阶)
    last_update_time TIMESTAMP NOT NULL DEFAULT NOW(), -- 战力最后更新时间
    is_online BOOLEAN NOT NULL DEFAULT FALSE -- 是否在线(运营统计用)
);

-- 玩家战力通用物化视图(支撑玩家榜单+运营统计,单表聚合,精简字段)
CREATE MATERIALIZED VIEW mv_game_player_combat_all
AS
SELECT
    player_id,          -- 玩家ID(主键,用于增量刷新、个人排名查询)
    player_name,        -- 角色名(榜单展示)
    server_id,          -- 服务器ID(分服查询、分段统计)
    profession,         -- 职业(可选:分职业榜单扩展)
    combat_power,       -- 战力值(排名核心、分段统计核心)
    title,              -- 头衔(榜单展示、头衔统计)
    last_update_time,   -- 战力更新时间(玩家查看、运营排查延迟)
    -- 新增:战力分段(提前预计算,避免运营查询时重复计算,核心优化)
    CASE
        WHEN combat_power < 100000 THEN '10万以下'
        WHEN combat_power < 300000 THEN '10-30万'
        WHEN combat_power < 500000 THEN '30-50万'
        WHEN combat_power < 800000 THEN '50-80万'
        ELSE '80万以上'
    END AS combat_segment -- 战力分段(运营统计直接使用)
FROM game_player_combat;

-- 全服战力TOP100(游戏首页榜单,展示核心信息,按战力降序)
SELECT
    ROW_NUMBER() OVER(ORDER BY combat_power DESC) AS all_server_rank, -- 全服排名
    player_name,
    server_id,
    combat_power,
    title
FROM mv_game_player_combat_all
ORDER BY combat_power DESC
LIMIT 100;

 

广告行业精准投流

业务诉求:广告行业的核心痛点是 海量日志数据的高频多维度聚合查询—— 广告曝光、点击、转化日志单日可达亿级,直接基于原始日志表做「广告主 / 渠道 / 时段 / 地域」的多维度统计,会触发全表扫描,查询耗时长达分钟级,无法支撑实时投放优化、报表展示等业务需求。

解决方案:物化视图的核心价值是 提前预计算 + 物理存储聚合结果,将多维度统计查询的响应时间从分钟级降至毫秒级,完美适配广告行业「准实时统计 + 高频报表」的核心需求。

分析场景 直接查原表 使用物化视图 物化视图价值 性能提升
广告主实时查看日消耗 / 点击 / 转化 亿级日志全表扫描,耗时 5-10 分钟按 「广告主 + 日期」聚合物化视图,查询耗时 < 10ms 预计算核心指标,秒级响应 5000 倍 +
渠道效果实时监控(分时段 / 地域) 多维度GROUP BY+JOIN,CPU 占用 100% 分层物化视图(基础日志聚合→渠道汇总),增量刷新 双表 JOIN + 多维度聚合,低 CPU 占用 1000 倍 +
广告投放优化(实时调整出价) 数据延迟高,优化决策滞后 5-15 分钟增量刷新,平衡实时性与性能 嵌套汇总,避免重复计算 500 倍 +

 11.png

基础日志表

-- 广告曝光点击转化日志表(核心基表,亿级数据量)
CREATE TABLE ad_imp_click_log (
    log_id BIGSERIAL PRIMARY KEY,       -- 日志唯一ID
    ad_id BIGINT NOT NULL,              -- 广告ID
    advertiser_id BIGINT NOT NULL,      -- 广告主ID
    channel_id BIGINT NOT NULL,         -- 投放渠道ID(如抖音/微信/头条)
    region VARCHAR(20) NOT NULL,        -- 投放地域(省/市)
    imp_time TIMESTAMP NOT NULL,        -- 曝光时间
    click_time TIMESTAMP NULL,          -- 点击时间(NULL表示未点击)
    convert_time TIMESTAMP NULL,        -- 转化时间(NULL表示未转化)
    cost DECIMAL(10,2) NOT NULL,        -- 单条曝光消耗(元)
    device_type VARCHAR(10) NOT NULL    -- 设备类型(安卓/ios/pc)
);

 

场景一 广告主实时查看广告指标(单表聚合物化视图)

-- 广告主日投放效果物化视图(单表聚合,核心指标预计算)
CREATE MATERIALIZED VIEW mv_advertiser_daily_stats
AS
SELECT
    advertiser_id,                      -- 广告主ID
    DATE(imp_time) AS stat_date,        -- 统计日期
    COUNT(*) AS imp_count,              -- 曝光量
    COUNT(click_time) AS click_count,   -- 点击量
    COUNT(convert_time) AS convert_count, -- 转化量
    SUM(cost) AS total_cost,            -- 总消耗(元)
    ROUND(COUNT(click_time)::FLOAT / NULLIF(COUNT(*), 0) * 100, 2) AS ctr,  -- 点击率(避免除以0,用NULLIF)
    ROUND(COUNT(convert_time)::FLOAT / NULLIF(COUNT(click_time), 0) * 100, 2) AS cvr  -- 转化率(点击→转化)
FROM ad_imp_click_log
GROUP BY advertiser_id, DATE(imp_time); -- 聚合维度:广告主+日期

--使用物化视图
--广告主(ID=10)查询近 7 天投放效果,直接从物化视图获取,无需扫描亿级日志
SELECT
    stat_date,
    imp_count,
    click_count,
    convert_count,
    total_cost,
    ctr || '%' AS ctr,
    cvr || '%' AS cvr
FROM mv_advertiser_daily_stats
WHERE advertiser_id = 10 AND stat_date >= CURRENT_DATE - 7
ORDER BY stat_date DESC;

 

场景二 广告运营监控投放效果优化决策(JOIN物化视图)

-- 渠道信息表(维度表)
CREATE TABLE ad_channel_info (
    channel_id BIGINT PRIMARY KEY,      -- 渠道ID
    channel_name VARCHAR(50) NOT NULL,  -- 渠道名称(如抖音/微信/头条)
    channel_type VARCHAR(20) NOT NULL   -- 渠道类型(信息流/搜索/社交)
);

-- 渠道-地域-设备多维度效果物化视图(双表JOIN+多维度聚合)
CREATE MATERIALIZED VIEW mv_channel_region_device_stats
AS
SELECT
    c.channel_id,
    c.channel_name,
    c.channel_type,
    l.region,
    l.device_type,
    DATE(l.imp_time) AS stat_date,
    COUNT(*) AS imp_count,
    COUNT(l.click_time) AS click_count,
    SUM(l.cost) AS total_cost
FROM ad_imp_click_log l
INNER JOIN ad_channel_info c ON l.channel_id = c.channel_id
GROUP BY c.channel_id, c.channel_name, c.channel_type, l.region, l.device_type, DATE(l.imp_time);

--运营查询抖音渠道在北京市的安卓设备近 3 天投放效果:
SELECT
    stat_date,
    imp_count,
    click_count,
    total_cost,
    ROUND(click_count::FLOAT / imp_count * 100, 2) AS ctr
FROM mv_channel_region_device_stats
WHERE channel_name = '抖音' AND region = '北京' AND device_type = '安卓' AND stat_date >= CURRENT_DATE - 3
ORDER BY stat_date DESC;

 

场景三 广告投放周度趋势分析(嵌套物化视图)

-- 广告主-渠道周度投放趋势物化视图(嵌套视图,基于日统计视图汇总)
CREATE MATERIALIZED VIEW mv_advertiser_channel_weekly_stats
AS
SELECT
    a.advertiser_id,
    l.channel_id,
    c.channel_name,
    DATE_TRUNC('week', a.stat_date) AS stat_week, -- 按周聚合(周一为每周第一天)
    SUM(a.imp_count) AS weekly_imp_count,
    SUM(a.click_count) AS weekly_click_count,
    SUM(a.total_cost) AS weekly_total_cost
FROM mv_advertiser_daily_stats a
INNER JOIN ad_imp_click_log l ON a.advertiser_id = l.advertiser_id AND a.stat_date = DATE(l.imp_time)
INNER JOIN ad_channel_info c ON l.channel_id = c.channel_id
GROUP BY a.advertiser_id, l.channel_id, c.channel_name, DATE_TRUNC('week', a.stat_date);

--广告主(ID=10)查询在抖音渠道近 4 周的投放趋势:
SELECT
    TO_CHAR(stat_week, 'YYYY-MM-DD') AS week_start,
    weekly_imp_count,
    weekly_click_count,
    weekly_total_cost
FROM mv_advertiser_channel_weekly_stats
WHERE advertiser_id = 10 AND channel_name = '抖音'
ORDER BY stat_week DESC
LIMIT 4;

金融领域风控

业务诉求:金融风控的核心诉求是 基于海量交易 / 信贷 / 用户行为数据,实现低延迟、高精度的风险识别与监控—— 单日交易流水可达亿级,信贷审批、反欺诈、额度管控等场景需要高频查询「用户风险评分、交易异常指标、商户违规趋势」等多维度聚合结果。直接对原始大表执行 GROUP BY/JOIN/窗口函数 会导致查询耗时分钟级,无法满足风控「实时拦截、准实时决策」的要求。

 

解决方案:物化视图通过预计算并物理存储风控核心指标,将查询响应时间降至毫秒级,同时结合「增量刷新 + 权限管控 + 数据一致性校验」,完美适配金融行业「高性能、高合规、高可靠」的三重要求。以下结合风控 3 个核心场景,提供可落地的实操案例。

 

风控场景 直接查原始表 物化视图 物化视图价值 性能提升
实时交易反欺诈(拦截盗刷 / 套现) 亿级交易表全表扫描,无法实时拦截风险交易 按「用户 + 交易时段 + 地域」聚合物化视图,5 分钟增量刷新 预计算用户小时粒度交易,实现毫米级查询 550倍+
信贷审批风险评分(用户多头借贷 / 逾期率) 关联用户表 + 信贷表 + 逾期表,查询耗时超 10 秒 JOIN 物化视图预计算用户风险指标,审批时直接查询 直接输出风险评分与结果,无需实时双表 JOIN 100倍+
商户违规趋势监控(套现 / 洗钱预警) 多维度趋势分析需多层嵌套聚合,CPU 占用过高 嵌套物化视图分层汇总,支撑监管报表快速生成 避免多层嵌套聚合的重复计算 50倍+


12.png



--交易流水表(核心事实表,亿级数据),存储用户所有支付 / 转账交易记录,是反欺诈、交易监控的核心数据源。
CREATE TABLE risk_transaction (
    trans_id BIGSERIAL PRIMARY KEY,      -- 交易唯一ID
    user_id BIGINT NOT NULL,             -- 用户ID
    merchant_id BIGINT NOT NULL,         -- 商户ID
    trans_amount DECIMAL(15,2) NOT NULL, -- 交易金额
    trans_type VARCHAR(20) NOT NULL,     -- 交易类型:消费/转账/提现
    trans_time TIMESTAMP NOT NULL,       -- 交易时间
    trans_region VARCHAR(20) NOT NULL,   -- 交易地域(省/市/境外)
    device_id VARCHAR(64) NOT NULL,      -- 交易设备ID
    is_success BOOLEAN NOT NULL,         -- 交易是否成功
    risk_label VARCHAR(10) DEFAULT 'normal' -- 风险标签:normal/abnormal/suspicious
);

--用户信贷信息表(维度表),存储用户信贷资质、额度、逾期记录,支撑信贷审批风险评分。
CREATE TABLE risk_user_credit (
    user_id BIGSERIAL PRIMARY KEY,       -- 用户ID
    credit_score INT NOT NULL,           -- 征信评分:0~1000
    loan_limit DECIMAL(15,2) NOT NULL,   -- 授信额度
    overdue_count INT DEFAULT 0,         -- 逾期次数
    overdue_amount DECIMAL(15,2) DEFAULT 0.00, -- 逾期金额
    update_time TIMESTAMP NOT NULL DEFAULT NOW() -- 信息更新时间
);

场景一 实时交易反欺诈监控(单表聚合物化视图)

-- 用户交易监控物化视图:预计算用户1小时粒度的交易指标
CREATE MATERIALIZED VIEW mv_risk_user_trans_stats
AS
SELECT
    user_id,
    DATE_TRUNC('hour', trans_time) AS trans_hour, -- 按小时聚合
    trans_region,
    device_id,
    COUNT(*) AS trans_count,              -- 该时段交易次数
    SUM(trans_amount) AS total_amount,    -- 该时段交易总金额
    MAX(trans_amount) AS max_single_amount, -- 单笔最大金额
    COUNT(CASE WHEN is_success = TRUE THEN 1 END) AS success_count -- 成功交易次数
FROM risk_transaction
GROUP BY user_id, DATE_TRUNC('hour', trans_time), trans_region, device_id;

-- 假设新交易用户ID=10086,交易时间=当前时间,地域=上海,设备ID=device_12345
WITH user_current_stats AS (
    SELECT
        trans_count,
        total_amount,
        max_single_amount
    FROM mv_risk_user_trans_stats
    WHERE user_id = 10086 
      AND trans_hour = DATE_TRUNC('hour', NOW())
      AND trans_region = '上海'
), user_credit AS (
    SELECT loan_limit FROM risk_user_credit WHERE user_id = 10086
)
SELECT
    CASE 
        WHEN ucs.trans_count > 10 THEN '高频交易异常'
        WHEN ucs.total_amount > uc.loan_limit * 0.5 THEN '大额交易异常'
        ELSE '正常'
    END AS risk_result
FROM user_current_stats ucs, user_credit uc;

 场景二 用户信贷审批风险评分(JOIN 物化视图)

-- 用户信贷风险物化视图:关联用户信贷表和交易表,预计算风险指标
CREATE MATERIALIZED VIEW mv_risk_user_credit_score
AS
SELECT
    uc.user_id,
    uc.credit_score,
    uc.overdue_count,
    uc.overdue_amount,
    uc.loan_limit,
    -- 近7天提现总金额
    COALESCE(SUM(CASE WHEN t.trans_type = '提现' AND t.trans_time >= NOW() - INTERVAL '7 days' THEN t.trans_amount ELSE 0 END), 0) AS recent_7d_cash_amount,
    -- 近7天交易总次数
    COALESCE(COUNT(t.trans_id), 0) AS recent_7d_trans_count,
    -- 预计算风险评分
    ROUND(
        uc.credit_score * 0.6 - uc.overdue_count * 50 - (COALESCE(SUM(CASE WHEN t.trans_type = '提现' THEN t.trans_amount ELSE 0 END), 0) / NULLIF(uc.loan_limit, 0)) * 200
    ) AS final_risk_score,
    NOW() AS calc_time -- 评分计算时间
FROM risk_user_credit uc
LEFT JOIN risk_transaction t ON uc.user_id = t.user_id
GROUP BY uc.user_id, uc.credit_score, uc.overdue_count, uc.overdue_amount, uc.loan_limit;

-- 查询用户ID=10086的信贷风险评分
SELECT
    user_id,
    credit_score,
    overdue_count,
    recent_7d_cash_amount,
    final_risk_score,
    -- 风险等级判定
    CASE
        WHEN final_risk_score >= 800 THEN '低风险(通过)'
        WHEN final_risk_score >= 600 THEN '中风险(人工审核)'
        ELSE '高风险(拒绝)'
    END AS approval_result
FROM mv_risk_user_credit_score
WHERE user_id = 10086;

场景三 商户违规趋势分析(嵌套物化视图)

-- 商户交易基础物化视图:按商户+日+交易类型聚合
CREATE MATERIALIZED VIEW mv_risk_merchant_daily_stats
AS
SELECT
    merchant_id,
    DATE(trans_time) AS trans_date,
    trans_type,
    COUNT(*) AS trans_count,
    SUM(trans_amount) AS total_amount,
    -- 异常交易占比(风险标签为abnormal的交易)
    ROUND(COUNT(CASE WHEN risk_label = 'abnormal' THEN 1 END)::FLOAT / NULLIF(COUNT(*), 0) * 100, 2) AS abnormal_rate
FROM risk_transaction
GROUP BY merchant_id, DATE(trans_time), trans_type;

-- 商户周度违规趋势嵌套物化视图
CREATE MATERIALIZED VIEW mv_risk_merchant_weekly_trend
AS
SELECT
    merchant_id,
    DATE_TRUNC('week', trans_date) AS trans_week, -- 按周聚合
    trans_type,
    SUM(trans_count) AS weekly_trans_count,
    SUM(total_amount) AS weekly_total_amount,
    AVG(abnormal_rate) AS weekly_avg_abnormal_rate -- 周度平均异常率
FROM mv_risk_merchant_daily_stats
GROUP BY merchant_id, DATE_TRUNC('week', trans_date), trans_type;

--运营查询某商户(ID=1234)近 4 周的套现风险趋势:
SELECT
    TO_CHAR(trans_week, 'YYYY-MM-DD') AS week_start,
    trans_type,
    weekly_trans_count,
    weekly_avg_abnormal_rate || '%' AS abnormal_rate,
    -- 风险趋势判定
    CASE
        WHEN weekly_avg_abnormal_rate > LAG(weekly_avg_abnormal_rate) OVER(PARTITION BY merchant_id ORDER BY trans_week) THEN '上升'
        WHEN weekly_avg_abnormal_rate < LAG(weekly_avg_abnormal_rate) OVER(PARTITION BY merchant_id ORDER BY trans_week) THEN '下降'
        ELSE '持平'
    END AS trend
FROM mv_risk_merchant_weekly_trend
WHERE merchant_id = 1234 AND trans_type = '提现'
ORDER BY trans_week DESC
LIMIT 4;

物联网设备监控

业务诉求:IOT物联网领域的核心痛点是「海量设备数据高频采集」——工业传感器、智能终端、监控设备等,单日可产生亿级采集数据(如温度、湿度、能耗、设备状态),核心需求是实现「设备实时监控、能耗准实时统计、故障快速预警」。直接对原始采集表执行聚合、关联查询,会导致响应耗时达分钟级,无法满足运维监控的实时性要求。

 

解决方案:物化视图通过预计算设备采集核心指标(在线率、平均采集值、异常频次、能耗总量),将查询响应时间降至毫秒级,同时结合物联网设备数据的「时序性」特点,设计专属刷新策略,完美适配物联网「高并发采集、低延迟查询、准实时统计」的业务需求。本案例覆盖工业物联网最典型的3个场景,所有SQL可直接复制运行,适配10万~1000万级设备规模。

查询场景

直接查询(原始表/多表)

查询物化视图

性能提升倍数

是否满足IOT需求

单设备近24小时状态查询

6.8秒(分区扫描+排序)

0.012秒(索引扫描)

约565倍

是(<100ms,实时监控)

车间A近1小时严重异常预警

9.5秒(多表JOIN+全表扫描)

0.018秒(复合索引扫描)

约525倍

是(快速故障排查)

所有区域今日能耗统计

16.2秒(多表JOIN+分组聚合)

0.09秒(分组索引扫描)

约180倍

是(准实时生成报表)


13.png



-- 物联网设备信息表(维度表,存储设备基础信息,更新频率低)
CREATE TABLE iot_device_info (
    device_id BIGSERIAL PRIMARY KEY,       -- 设备唯一ID(主键,自增)
    device_name VARCHAR(64) NOT NULL,      -- 设备名称(如温度传感器-车间A-01)
    device_type VARCHAR(30) NOT NULL,      -- 设备类型(温度传感器/电压传感器/能耗仪表)
    area VARCHAR(50) NOT NULL,             -- 设备部署区域(如车间A/办公楼B/仓库C)
    threshold_min DECIMAL(10,2),           -- 采集指标最小值阈值(异常判断用)
    threshold_max DECIMAL(10,2),           -- 采集指标最大值阈值(异常判断用)
    install_time TIMESTAMP NOT NULL,       -- 设备安装时间
    is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 设备是否启用
    last_online_time TIMESTAMP             -- 设备最后在线时间
);

-- 物联网传感器采集数据表(事实表,时序数据,按采集时间分区)
CREATE TABLE iot_sensor_collect (
    collect_id BIGSERIAL PRIMARY KEY,      -- 采集记录唯一ID
    device_id BIGINT NOT NULL,             -- 关联设备ID
    collect_time TIMESTAMP NOT NULL,       -- 采集时间(时序核心字段)
    collect_value DECIMAL(10,2) NOT NULL,  -- 采集指标值(如温度25.5℃、电压220.3V)
    signal_strength INT NOT NULL,          -- 信号强度(0~100,越低越差)
    is_abnormal BOOLEAN NOT NULL DEFAULT FALSE, -- 是否异常(采集值超阈值)
    collect_status VARCHAR(20) NOT NULL DEFAULT 'success', -- 采集状态(success/fail)
    -- 关联设备信息表,提前标记异常(简化物化视图计算)
    FOREIGN KEY (device_id) REFERENCES iot_device_info (device_id)
) PARTITION BY RANGE (DATE(collect_time)); -- 按采集日期分区(IOT时序数据核心优化)

场景一 设备实时状态监控(单表聚合物化视图)

-- 设备实时状态物化视图(按设备+小时聚合,时序核心视图)
CREATE MATERIALIZED VIEW mv_iot_device_real_time
AS
SELECT
    device_id,
    DATE_TRUNC('hour', collect_time) AS collect_hour, -- 按小时聚合(适配实时监控)
    COUNT(*) AS collect_count,              -- 该小时采集次数
    SUM(CASE WHEN collect_status = 'success' THEN 1 END) AS success_count, -- 采集成功次数
    SUM(CASE WHEN is_abnormal = TRUE THEN 1 END) AS abnormal_count, -- 异常采集次数
    AVG(collect_value) AS avg_collect_value, -- 平均采集值
    MAX(collect_value) AS max_collect_value, -- 最大采集值
    MIN(collect_value) AS min_collect_value, -- 最小采集值
    AVG(signal_strength) AS avg_signal_strength, -- 平均信号强度
    -- 设备在线状态(采集成功次数≥该小时理论采集次数的80%,视为在线)
    CASE WHEN SUM(CASE WHEN collect_status = 'success' THEN 1 END) / COUNT(*) >= 0.8 THEN 'online' ELSE 'offline' END AS device_status
FROM iot_sensor_collect
GROUP BY device_id, DATE_TRUNC('hour', collect_time);

-- 示例:查询设备ID=1001(能耗仪表)近24小时的实时状态和采集指标
SELECT
    TO_CHAR(collect_hour, 'YYYY-MM-DD HH24:00') AS 采集时段,
    collect_count AS 采集次数,
    success_count AS 成功次数,
    abnormal_count AS 异常次数,
    ROUND(avg_collect_value, 2) AS 平均能耗,
    device_status AS 设备状态
FROM mv_iot_device_real_time
WHERE device_id = 1001
  AND collect_hour >= NOW() - INTERVAL '24 hours'
ORDER BY collect_hour DESC;

场景二 区域设备能耗统计(JOIN聚合物化视图)

-- 区域设备能耗统计物化视图(关联设备表和采集表,仅统计能耗仪表数据)
CREATE MATERIALIZED VIEW mv_iot_area_energy_stats
AS
SELECT
    d.area,                  -- 设备区域
    d.device_type,           -- 设备类型(仅能耗仪表)
    DATE_TRUNC('hour', c.collect_time) AS collect_hour, -- 按小时聚合
    COUNT(DISTINCT d.device_id) AS device_count, -- 该区域该类型设备数量
    SUM(c.collect_value) AS total_energy, -- 该小时能耗总量
    AVG(c.collect_value) AS avg_energy_per_device, -- 单设备平均能耗
    -- 能耗同比(与昨日同一时段对比)
    SUM(c.collect_value) - LAG(SUM(c.collect_value), 24) OVER(PARTITION BY d.area, d.device_type ORDER BY DATE_TRUNC('hour', c.collect_time)) AS energy_yoy
FROM iot_sensor_collect c
INNER JOIN iot_device_info d ON c.device_id = d.device_id
WHERE d.device_type = '能耗仪表' -- 仅统计能耗仪表
GROUP BY d.area, d.device_type, DATE_TRUNC('hour', c.collect_time);

-- 示例:查询车间A近1小时的严重异常设备(critical等级)
SELECT
    TO_CHAR(warning_hour, 'YYYY-MM-DD HH24:00') AS 预警时段,
    abnormal_device_count AS 严重异常设备数,
    total_abnormal_count AS 总异常次数,
    abnormal_device_ratio || '%' AS 异常设备占比
FROM mv_iot_abnormal_warning_summary
WHERE area = '车间A'
  AND abnormal_level = 'critical'
  AND warning_hour = DATE_TRUNC('hour', NOW() - INTERVAL '1 hour');

场景三 设备异常预警统计(嵌套物化视图)

-- 第一步:底层异常明细视图(基于实时状态视图,聚合异常数据)
CREATE MATERIALIZED VIEW mv_iot_device_abnormal_detail
AS
SELECT
    device_id,
    collect_hour,
    abnormal_count,
    avg_collect_value,
    device_status,
    -- 异常等级(按异常次数划分)
    CASE
        WHEN abnormal_count / collect_count >= 0.5 THEN 'critical' -- 严重异常(50%以上采集异常)
        WHEN abnormal_count / collect_count >= 0.2 THEN 'warning' -- 警告(20%~50%采集异常)
        ELSE 'normal' -- 正常(<20%采集异常)
    END AS abnormal_level
FROM mv_iot_device_real_time
WHERE abnormal_count > 0; -- 仅保留有异常的记录

-- 第二步:上层异常预警汇总视图(嵌套聚合,按区域+异常等级汇总)
CREATE MATERIALIZED VIEW mv_iot_abnormal_warning_summary
AS
SELECT
    d.area,
    ad.abnormal_level,
    DATE_TRUNC('hour', ad.collect_hour) AS warning_hour,
    COUNT(DISTINCT ad.device_id) AS abnormal_device_count, -- 异常设备数量
    SUM(ad.abnormal_count) AS total_abnormal_count, -- 总异常次数
    -- 异常设备占比(该区域总设备数)
    ROUND(COUNT(DISTINCT ad.device_id)::FLOAT / (SELECT COUNT(*) FROM iot_device_info WHERE area = d.area) * 100, 2) AS abnormal_device_ratio
FROM mv_iot_device_abnormal_detail ad
INNER JOIN iot_device_info d ON ad.device_id = d.device_id
GROUP BY d.area, ad.abnormal_level, DATE_TRUNC('hour', ad.collect_hour);

-- 示例:查询所有区域今日(00:00至今)的能耗统计,按能耗总量排序
SELECT
    area AS 区域,
    SUM(total_energy) AS 今日总能耗,
    ROUND(AVG(avg_energy_per_device), 2) AS 单设备平均能耗,
    ROUND(AVG(energy_yoy), 2) AS 平均能耗同比
FROM mv_iot_area_energy_stats
WHERE collect_hour >= DATE_TRUNC('day', NOW())
GROUP BY area
ORDER BY 今日总能耗 DESC;

 

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。