增量物化视图实践

增量物化视图最佳实践

概述

增量物化视图是 ProtonBase 中一种特殊的物化视图,它通过仅对变更数据进行刷新而非全量重新计算的方式来提高查询性能。与全量物化视图相比,增量物化视图在基础表数据量庞大而变更量较小的场景下能够显著提升查询性能。

适用性评估

在使用增量物化视图前,需评估查询的适用性。可通过以下方法进行验证:

设 T1 时刻刷新的物化视图为:MV@T1 = Query@T1 T2 时刻刷新的物化视图为:MV@T2 = Query@T2

MV@T1MV@T2 的内容差异较小,则增量刷新可带来显著性能提升。反之,若内容完全不同,则不适合采用增量刷新。

不适用示例

SELECT a, b, c, NOW() FROM t;

此查询每次刷新结果均不同,无法有效利用增量计算优势。

增量物化视图实践速查表

场景推荐做法避免做法
WHERE 条件- 使用固定时间范围
- 添加索引
- 使用 NOW()
- 使用 CURRENT_TIMESTAMP 等不稳定函数
GROUP BY- 物化所有分组结果
- 后续过滤
- 使用 HAVING 在物化前过滤
Count Distinct- 使用 FILTER 子句统一计算- 使用多个 CASE WHEN 分散计算
JOIN 操作- 使用 INNER JOIN
- 确保等值条件
- 创建索引
- 使用 OUTER JOIN
- 类型转换
- 无索引
Over Window- 置于查询最外层
- 完整持久化
- 嵌套使用
- 结果过滤
复杂查询- 拆分为分层物化视图- 单一复杂查询语句
函数使用- 确定性函数
  - SUM
  - COUNT
- 非确定性函数
  - RANDOM
  - CURRENT_USER

设计与实现最佳实践

1. WHERE 子句优化

问题场景:

SELECT * FROM t WHERE ts >= NOW() - INTERVAL '1 hour';

解决方案:

  • 列存储表:利用自适应扫描并发优化过滤性能
  • 行存储表:在 ts 字段创建二级索引
  • 考虑使用固定时间范围替代动态时间计算

2. GROUP BY 子句优化

基本原则: GROUP BY 语句需持久化全部计算结果

不推荐做法:

SELECT department, AVG(salary)
FROM employees
GROUP BY department
HAVING COUNT(*) > 5;

推荐方案:

-- 创建基础物化视图
CREATE MATERIALIZED VIEW dept_stats AS
SELECT department, AVG(salary) as avg_salary, COUNT(*) as emp_count
FROM employees
GROUP BY department;
 
-- 查询时过滤
SELECT department, avg_salary
FROM dept_stats
WHERE emp_count > 5;

3. 多维度 Count Distinct 优化

低效实现:

SELECT
  COUNT(DISTINCT CASE WHEN platform = 'iOS' THEN user_id END) as ios_uv,
  COUNT(DISTINCT CASE WHEN platform = 'Android' THEN user_id END) as android_uv
FROM user_events;

高效实现:

SELECT
  COUNT(DISTINCT user_id) FILTER (WHERE platform = 'iOS') as ios_uv,
  COUNT(DISTINCT user_id) FILTER (WHERE platform = 'Android') as android_uv
FROM user_events;

4. JOIN 操作优化

优化策略:

  • 优先使用 INNER JOIN
  • 确保每个 JOIN 至少包含一个等值条件
  • 保持等值条件两侧数据类型一致
  • 在大表 JOIN 键上创建二级索引

示例:

-- 创建索引优化 JOIN 性能
CREATE INDEX idx_orders_product_id ON orders(product_id);
CREATE INDEX idx_products_product_id ON products(product_id);
 
-- 执行 JOIN 查询
SELECT o.order_id, p.product_name
FROM orders o
INNER JOIN products p ON o.product_id = p.product_id;

5. Over Window 处理

支持模式:

CREATE MATERIALIZED VIEW top_employees AS
SELECT *
FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as rn
  FROM employees
) WHERE rn <= 3;

不支持模式:

-- 系统将报错:Unsupported feature: window not in query top level
SELECT department, AVG(salary) OVER (PARTITION BY department)
FROM employees
WHERE avg_salary > 10000;

6. 架构设计模式

架构设计模式速查表

设计模式适用场景优点缺点
CTE 物化- 中间结果复用
- 逻辑复杂查询
- 逻辑清晰
- 自动物化中间结果
- 无法直接查询中间结果
分层物化- 多个应用复用基础数据- 可复用
- 支持索引
- 灵活查询
- 物化视图数量增多
子查询- 简单逻辑
- 无需物化
- 语法简洁- 复杂查询性能较差

方案一:CTE 物化

CREATE MATERIALIZED VIEW imv
WITH (refresh_mode = 'incremental') AS
WITH base AS (
  SELECT department, AVG(salary) as avg_salary
  FROM employees
  GROUP BY department
)
SELECT department, avg_salary
FROM base
WHERE avg_salary > 5000;

方案二:分层物化视图

-- 基础层
CREATE MATERIALIZED VIEW base_mv
WITH (refresh_mode = 'incremental') AS
SELECT department, AVG(salary) as avg_salary
FROM employees
GROUP BY department;

-- 应用层
CREATE MATERIALIZED VIEW app_mv
WITH (refresh_mode = 'incremental') AS
SELECT department, avg_salary
FROM base_mv
WHERE avg_salary > 5000;

物化视图实践策略

1. 评估是否使用物化视图

物化视图能产生收益一般需要满足以下几个特征:

  • 查询 pattern 计算开销大。常见开销较大的计算模式比如多表关联,聚合,开窗函数等。另外一些比较复杂的函数(如正则匹配等),以及需要访问外部资源的 UDF 等,也可以被认为是计算开销较大。
  • 查询 pattern 频繁出现。如果某个查询 pattern 开销很大,但是只是很偶尔使用,考虑到物化视图本身会带来一定的成本,也没有太大必要使用。如果查询足够频繁,带来的节省超过物化视图的成本,则可以考虑。

如果一个查询 pattern 同时满足上述两个特征,那么可以考虑针对其创建物化视图。

2. 合理的定义物化视图

我们建议用户综合考虑查询的计算开销和查询频率,来抽取合适的子查询作为物化视图的定义。这样可以让这个子查询覆盖尽可能的 Query。常见的场景有:

  • 多表关联外加各种 Ad hoc 的聚合计算。一般的数仓分析中往往需要提前将事实表和维度表进行关联,然后基于关联后的表进行分析。如果我们发现这个多表关联的规则基本不变,但是后续的分析非常灵活,那么可以考虑将多表关联定义成物化视图,后续基于物化视图直接进行 Ad hoc 分析。
  • 复杂聚合后的随机查询。还有一些场景是多表关联和聚合规则都很固定,但是最终需要在聚合的结果上做大量的点查。比如在电商领域,一般会提前加工好商家和买家维度的一些聚合结果表,提供给商家和买家对自己的结果进行访问。我们建议对聚合后的结果表创建物化视图,而不是为最终每个可能的点查创建物化视图。

3. 选择合适的存储

由于物化视图最终也是以一个物理表的形式存在,一些表级的设置手段在这里也都适用,常见的有:

  • 为物化视图的每一列定义名字。如果在创建物化视图时不指定列名,引擎会自动推断和产生。但考虑到后续在物化视图上的调优手段需要,我们建议手动指定每一列的名字。
  • 根据最终访问物化视图的模式选择合理的 Access Method。比如物化视图存储的是一个预聚合的结果,后续对其查询以点查为主,那么可以考虑使用行存(row)。如果后续需要大量全表扫描物化视图做进一步的计算,可以考虑使用列存(columnar)。如果各种访问方式都有,考虑使用混合存储模式(hybrid)。
  • 创建适合的索引。常见的比如加速点查的二级索引,加速列存过滤的 split index 等,都可以创建在物化视图上。这里也需要权衡更多的索引带来的更大的写入开销。

4. 选择合适的刷新策略

选择合适的刷新策略需要结合以下因素进行综合考虑:

  1. 基础表的修改频率
  2. 对物化视图的时效性需求
  3. 待物化的查询复杂度

首先我们看下比较硬性的条件,假如:

  • 基础表基本不变或者修改很不频繁
  • 对时效性要求较低(比如小时或者天级就可以满足)
  • 查询复杂度过高,无法使用增量计算

上述任一条件满足,我们使用全量物化视图即可。

如果原始数据修改频繁,我们又对结果有一定的时效性要求,则可以考虑使用增量或者实时物化视图。这种情况下,我们一般从时效性的角度来选择:

  • 实时物化视图:这个一般用于某些对时效性要求特别高(秒级以内)的业务场景。
  • 增量物化视图:针对一些秒级和分钟级延迟的场景需求。

自动化运维建议

  • 利用调度系统(如 Airflow、crontab)定时刷新物化视图,结合业务低峰期执行。
  • 监控刷新耗时、失败率、延迟等指标,及时告警。
  • 利用系统表(如 pg_matviewspg_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 等系统表,可以自动分析出物化视图和表之间的依赖关系
    • 小时/天级时效性:构建全量物化视图,定期按照依赖关系全量刷新
    • 分钟级时效性:构建增量物化视图,支持常见的过滤,多表关联等,定期按照依赖关系增量刷新
  • DWS/AWS:
    • 根据不同的业务场景需要,基于 DWD 和 ODS 层构建物化视图。
    • 一些较为固定的查询 pattern:可以构建一些轻度聚合的物化视图,直接在上面做二次聚合分析或者点查。
    • 灵活性很高的查询:可以不使用物化视图,直接查询 ODS 和 DWD 层的数据

通过这个架构,我们不需要为离线和实时各准备一套完整的数据架构,只需要根据时效性和查询灵活性等需求在 Protonbase 里创建合理的表和物化视图等,即可同时满足离线和实时的需求,极大的降低了成本,也提高了易用性。

案例三:广告计费

在广告场景中,在投放引擎决定投放某一个广告时,一般会先检查一下该广告主的余额是不是充足。在按点击收费的模式下,用户的每一次点击都会引发对这一广告主的收费。由于用户的点击无时无刻不在发生,我们需要实时的统计每个广告主的花费情况:

SELECT SUM(cost) AS total_cost FROM user_clicks GROUP BY advertiser

如果发现某个广告主的总开销已经超过他的余额,那么就停止展现他的广告。反过来说,如果该花费统计有延迟,那就有可能导致一些超投的情况而造成资损。

目前,一些常见的手段会基于一些流计算引擎来做这部分实时统计,但是也不能避免一小段时间的延迟,同时系统在预期之外的重启也会导致延迟不可控。这时候我们就可以考虑基于 user_clicks 构建实时的物化视图。当用户点击的行为写进 user_clicks 表时,相应的物化视图也会实时的进行更新,做到真正的 0 延迟。

案例四:金融风控

在金融行业中,实时风控是保障业务安全的重要环节。通过增量物化视图,可以实现对用户交易行为的实时监控和风险识别。

业务场景: 银行或支付平台需要实时监控用户的交易行为,识别异常交易模式,防止欺诈行为。例如,监控用户按小时、按天的交易频次、交易金额分布等指标,当这些指标超过预设阈值时,触发风控策略。

技术实现:

-- 创建基础增量物化视图,按时间粒度聚合用户交易数据
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 复杂度、索引覆盖情况、存储方式(行/列/混合)是否合理。
  • 依赖变更:基础表结构变更后,需同步调整物化视图定义,避免刷新异常。

总结

物化视图是提升查询性能的有效手段,但需要遵循特定的设计原则:

  1. 确保查询结果集变化率较低
  2. 避免使用非确定性函数
  3. 保证 GROUP BYOver Window 结果的完整性
  4. 优化 JOIN 条件和索引策略
  5. 采用合适的架构分层方案