增量分区物化视图

更新时间:
复制 MD 格式

增量分区物化视图是PolarDB PostgreSQL版提供的物化视图类型,把分区物化视图的分区管理能力与增量物化视图(DIMV)的增量刷新引擎结合起来,通过polar_matview.create_partition_mv的use_dimv => TRUE参数创建。适用于按时间滚动分区、历史分区写入后不再变化的大事实表,在这类基表上以较低的维护成本提供秒级新鲜度的聚合结果。

概述

增量分区物化视图把分区物化视图的分区管理能力与增量物化视图(DIMV)的增量刷新引擎结合起来,通过polar_matview.create_partition_mv(..., use_dimv => TRUE)创建:每个源分区对应一个真正的DIMV子物化视图,源分区结构的增删由分区物化视图负责同步,分区内部的数据变更由DIMV引擎增量维护。

适用场景

最典型的场景是在按时间滚动分区的大事实表(例如订单、流水、日志、埋点)上维护聚合报表。这类场景通常同时具备以下特征,而它们正好逐条对应增量分区物化视图提供的能力:

  • 基表很大且持续增长,全量重算一次的代价高昂、资源消耗大,需要增量刷新。

  • 每次改动量相对表规模很小,增量刷新才划算。

  • 写入集中在最近的少数几个分区,历史分区写完就基本不再变化,可以被冻结、彻底退出维护。

  • 分区在不断滚动,新分区需要自动纳入维护,下线的分区需要自动从结果中移除。

  • 对数据新鲜度有要求,希望秒级而不是天级看到最新聚合结果。

反过来,如果基表没有分区,或者每次刷新本来就要重算大部分数据,或者每天全量刷新一次就足够,那么使用普通物化视图或分区物化视图更简单。

与另外两种物化视图的差异

分区物化视图和增量物化视图各自解决了一半问题:

  • 分区物化视图能把刷新范围收敛到发生变化的分区,但单个分区内部仍是全量重算。分区越大,一次改动的代价越高。

  • 增量物化视图能把刷新代价压到与改动量成正比,但它不感知分区。一方面,随着基表持续增长,历史数据虽然不再变化,仍然要留在同一个物化视图里参与维护;另一方面,分区的增删本身不产生物化视图日志(Materialized View Log,简称mlog)记录,因此DROP或DETACH掉一个源分区后,DIMV不会把该分区的贡献从结果中剔除,需要手动全量刷新,而分区物化视图能通过结构对齐自动处理。

组合之后,一次基表改动的刷新代价既被分区裁剪限制在相关分区内,又在分区内部只处理增量行;分区的增删由分区物化视图提供的结构对齐能力负责;长期没有变化的历史分区还可以被冻结,彻底退出自动刷新的调度范围,使维护开销不随历史数据累积而增长。

代价是多了DIMV的那份约束:基表需要主键,查询定义要满足DIMV对SQL的限制,写入时要承担mlog的记录开销。各类物化视图的完整对比与选型建议,请参见物化视图概览。

适用范围

  • PostgreSQL 14:内核小版本需为2.0.14.22.48.0及以上版本。

说明

您可在控制台查看内核小版本号,也可以通过SHOW polardb_version;语句查看。如未满足内核小版本要求,请升级内核小版本。

安装扩展

增量分区物化视图依赖polar_ivm扩展。使用前请在需要使用该功能的数据库中执行以下SQL安装扩展:

CREATE EXTENSION IF NOT EXISTS polar_ivm;

使用要求

在分区物化视图的使用条件(RANGE分区、单列分区键、分区键必须在GROUP BY中、分区表不能出现在子查询或JOIN可空侧等)之上,增量分区物化视图还有以下要求:

  • 查询定义必须同时满足增量物化视图对SQL的要求:子物化视图是真正的DIMV,因此DIMV支持的聚合函数、连接形式等限制在这里同样适用,详情请参见增量物化视图。

  • 基表必须能唯一标识行:这同样是DIMV带来的限制,mlog要靠这个标识把记录下来的变更行对应回物化视图中的行,详情请参见增量物化视图。

    • PostgreSQL模式下要求基表显式定义PRIMARY KEY。PostgreSQL又要求分区表的主键必须包含分区键列,因此主键通常是“分区键+业务主键”的复合主键。

  • 不支持DEFAULT分区:DEFAULT分区的边界是开放的,无法映射到一个固定范围的子物化视图,创建时会直接报错:

    ERROR: partition DIMV does not support a DEFAULT partition on "..."

创建

使用polar_matview.create_partition_mv创建增量分区物化视图,把use_dimv置为TRUE即可,其余参数的含义与分区物化视图一致:

CALL polar_matview.create_partition_mv(
  mv_schema NAME,                          -- 物化视图的 schema
  mv_name NAME,                            -- 物化视图名称
  ref_partition_table OID,                 -- 源分区表的 OID
  view_def TEXT,                           -- SELECT 查询定义
  use_imci BOOLEAN DEFAULT FALSE,          -- 是否使用列存加速
  skip_check_partitionable BOOLEAN DEFAULT FALSE,  -- 跳过可分区性检查
  create_no_data BOOLEAN DEFAULT FALSE,    -- 创建时不填充数据
  use_dimv BOOLEAN DEFAULT FALSE           -- 用 DIMV 承载增量刷新
);

参数说明:

参数

类型

默认值

说明

mv_schema

NAME

必填

物化视图的Schema。

mv_name

NAME

必填

物化视图名称。

ref_partition_table

OID

必填

源分区表的OID。

view_def

TEXT

必填

SELECT查询定义。

use_imci

BOOLEAN

FALSE

是否使用列存加速。

skip_check_partitionable

BOOLEAN

FALSE

跳过可分区性检查。

create_no_data

BOOLEAN

FALSE

创建时不填充数据。

use_dimv

BOOLEAN

FALSE

用DIMV承载增量刷新。置为TRUE时创建的是增量分区物化视图。

示例:在按季度RANGE分区的demo.sales表上创建增量分区物化视图。

CREATE SCHEMA IF NOT EXISTS demo;

-- 基表:RANGE 按季度分区,主键包含分区键 sale_date
CREATE TABLE demo.sales (
  id SERIAL,
  sale_date DATE,
  amount INT,
  PRIMARY KEY (id, sale_date)
) PARTITION BY RANGE (sale_date);

CREATE TABLE demo.sales_q1 PARTITION OF demo.sales
  FOR VALUES FROM ('2023-01-01') TO ('2023-04-01');
CREATE TABLE demo.sales_q2 PARTITION OF demo.sales
  FOR VALUES FROM ('2023-04-01') TO ('2023-07-01');

INSERT INTO demo.sales(sale_date, amount) VALUES
  ('2023-02-15', 100), ('2023-03-15', 150), ('2023-05-15', 200);

-- 创建增量分区物化视图
CALL polar_matview.create_partition_mv(
  'demo', 'mv_sales', 'demo.sales'::regclass,
  $$SELECT sale_date, SUM(amount) AS s FROM demo.sales GROUP BY sale_date$$,
  FALSE, FALSE, FALSE, TRUE /* use_dimv */
);

SELECT * FROM demo.mv_sales ORDER BY 1;

创建时系统会完成以下工作:

  • 为每个源分区对应创建一个REFRESH FAST ON DEMAND ... WITH NO DATA的子DIMV,随后执行一次全量刷新填充数据、建立增量基线。

  • 所有子DIMV共享基表上的同一个mlog,基表的写入开销不随分区数增加。

  • 自动刷新间隔初始为-1 second,即默认关闭,需要显式调用set_refresh_interval开启。

刷新

自动刷新(推荐)

在父物化视图上调用polar_matview.set_refresh_interval,间隔会记录在分区物化视图的元数据中,并下推到当前所有子DIMV;此后新增分区产生的子DIMV也会继承这个间隔。

-- 将自动刷新间隔设置为 10 秒
SELECT polar_matview.set_refresh_interval(
  'demo.mv_sales'::regclass::oid,
  '10 seconds'::interval);

之后由后台worker自动完成增量刷新,worker的部署方式与普通DIMV完全一致,请参见增量物化视图中的“配置自动刷新与清理”章节。worker无需为分区场景做任何特殊配置:子DIMV就是普通DIMV,同样出现在调度队列里。

结构对齐

源分区表增删分区后,调用polar_matview.refresh_partition_mv自动同步结构:

CALL polar_matview.refresh_partition_mv('demo', 'mv_sales');
  • 新增源分区:创建对应的子DIMV(WITH NO DATA+自动建mlog+一次全量刷新填充),并继承父物化视图上的刷新间隔。

  • 删除源分区:对应的子物化视图被卸载并删除。基表上的mlog不会被自动删除,因为它可能仍被其他物化视图共享。

冻结历史分区

时间序列场景中,老分区在窗口滑过之后就不再变化,但只要它们的子DIMV还在DIMV的接管之下,调度器每一轮都要把它们取出来、开事务、查一遍增量、再更新元数据,即使每次都发现无事可做。分区数越多,这部分与数据量无关的固定开销占比越高。冻结机制把这些分区从调度队列里摘出去,让维护成本只落在真正活跃的分区上,因此推荐开启。

开启冻结功能

在父物化视图上设置冻结时长即可开启,该功能默认关闭:

-- 源分区超过 7 天没有数据变化,其子 DIMV 被冻结
SELECT polar_matview.set_freeze_interval('demo.mv_sales'::regclass, INTERVAL '7 days');

-- 传 NULL 关闭冻结
SELECT polar_matview.set_freeze_interval('demo.mv_sales'::regclass, NULL);

使用说明

  • 判定与执行:均发生在refresh_partition_mv里,系统为每个子物化视图记录最后一次源数据变化的时间,本轮刷新中没有观察到源分区变化、且距上次变化已超过freeze_interval的子DIMV会被冻结。

  • 冻结效果:清除该子DIMV的可增量更新标记,使它不再被自动刷新的调度队列选中。物化视图本身仍然可查,数据保持冻结时刻的状态。

  • 自动恢复:如果之后某个已冻结的历史分区又发生了数据变更,在下一次执行refresh_partition_mv时会检测到版本变化,对该子物化视图执行一次全量刷新使其恢复,此后它重新回到增量维护状态。数据没有发生变化的兄弟分区不受影响,继续保持冻结。

  • 强制冻结:如需绕过时间判定直接冻结某个子物化视图,可以调用:

    SELECT polar_matview.freeze_dimv('demo.mv_sales_q1'::regclass);

最佳实践

用pg_cron定期驱动结构对齐

借助pg_cron扩展把refresh_partition_mv配成一个定期任务,让它同时承担三件事:同步源表新增或删除的分区、把新分区纳入增量维护、探测冷分区上的数据变化。

pg_cron的任务元数据集中在一个库中(由cron.database_name指定,默认为postgres),cron.*函数也只存在于该库。因此这一步要连到该库执行,用schedule_in_database的最后一个参数指定物化视图所在的业务库;执行者需要是普通角色,可以直接复用配置DIMV worker时创建的dimv_worker角色。

-- 连接到 pg_cron 的元数据库(默认 postgres),以 dimv_worker 身份执行
SELECT cron.schedule_in_database(
  'align-mv-daily-sales',
  '59 seconds',
  $$CALL polar_matview.refresh_partition_mv('demo', 'mv_daily_sales')$$,
  '<业务库>'
);

这个间隔决定两件事的延迟:新滚出来的分区多久被自动纳入维护,以及冷分区在重新发生数据变化后多久转回热分区。按业务能接受的延迟配置即可,不必追求过密,因为每一轮都要遍历所有子物化视图做版本比对,间隔过小会让这部分固定开销占比上升。

pg_cron的秒级间隔语法只接受1~59秒,上面的'59 seconds'即“约每分钟一轮”;需要更长的周期改用标准cron表达式,例如'*/5 * * * *'表示每5分钟一轮。

这个定期任务是冻结分区能自动转热的前提。冻结的子物化视图不再被DIMV的调度器刷新,但refresh_partition_mv每一轮仍会检查它对应的源分区版本;一旦历史分区上又出现数据变更,这一轮refresh_partition_mv就会检测到版本变化、对该子物化视图执行一次全量刷新把它恢复,此后重新交由DIMV增量维护,整个过程不需要人工介入。

初始即全部冻结,按需转热

对于分区很多、但活跃分区只有少数几个的场景(典型的时间序列表),可以在创建完成后把所有子物化视图一次性冻结:

DO $$
DECLARE c regclass;
BEGIN
  FOR c IN SELECT inhrelid::regclass FROM pg_inherits
           WHERE inhparent = 'demo.mv_daily_sales'::regclass LOOP
    PERFORM polar_matview.freeze_dimv(c);
  END LOOP;
END $$;

这样一开始没有任何子物化视图占用自动刷新的调度资源。之后哪个分区的数据发生了变化,上面那个定期的refresh_partition_mv就会把它恢复成热分区、交给DIMV接管;数据始终没有变化的历史分区则一直保持冻结。

这种方式不依赖set_freeze_interval:set_freeze_interval解决的是“热分区变冷后自动退出”,初始冻结解决的是“一开始就只让真正活跃的分区占用维护资源”。两者建议叠加使用:只做初始冻结的话,被恢复成热分区的子物化视图此后再也不会转冷,随着写入窗口向前滑动,热分区只增不减;配上set_freeze_interval,它们在不再变化足够久之后才会重新冻结。

列存索引加速

use_imci参数对增量分区物化视图同样有效,创建和刷新时会走列存执行路径:

CALL polar_matview.create_partition_mv(
  'demo', 'mv_sales_csi', 'demo.sales'::regclass,
  $$SELECT sale_date, SUM(amount) AS s FROM demo.sales GROUP BY sale_date$$,
  TRUE /* use_imci */, FALSE, FALSE, TRUE /* use_dimv */
);

CALL polar_matview.refresh_partition_mv('demo', 'mv_sales_csi', TRUE /* use_imci */);

use_imci会自动为建视图和全量刷新语句注入Set(polar_csi.enable_query on)和Set(polar_csi.cost_threshold 0),无需再改会话配置,也不受默认代价阈值影响。唯一的前置条件是基表上存在列存索引(IMCI),创建方法请参见普通物化视图中的列存加速刷新章节。

删除

使用polar_matview.drop_partition_mv删除增量分区物化视图:

CALL polar_matview.drop_partition_mv('demo', 'mv_sales');

该操作会删除父物化视图、所有子DIMV及相关元数据。与删除单个分区一样,基表上的mlog不会被自动删除。

完整示例

下面是一个端到端示例,覆盖从安装扩展、创建增量分区物化视图、开启自动刷新、冻结历史分区,到定期结构对齐与清理的完整流程。

-- ========== 1. 准备扩展与基表 ==========
CREATE EXTENSION IF NOT EXISTS polar_ivm;
CREATE SCHEMA IF NOT EXISTS demo;

CREATE TABLE demo.orders (
  order_id SERIAL,
  order_date DATE,
  user_id INT,
  amount NUMERIC,
  PRIMARY KEY (order_id, order_date)
) PARTITION BY RANGE (order_date);

CREATE TABLE demo.orders_202401 PARTITION OF demo.orders
  FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE demo.orders_202402 PARTITION OF demo.orders
  FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

INSERT INTO demo.orders(order_date, user_id, amount) VALUES
  ('2024-01-05', 1, 100), ('2024-01-20', 2, 200),
  ('2024-02-10', 1, 300);

-- ========== 2. 创建增量分区物化视图 ==========
CALL polar_matview.create_partition_mv(
  'demo', 'mv_daily_sales', 'demo.orders'::regclass,
  $$SELECT order_date,
           SUM(amount) AS total_amount,
           COUNT(*) AS order_count
    FROM demo.orders
    GROUP BY order_date$$,
  FALSE, FALSE, FALSE, TRUE /* use_dimv */
);

SELECT * FROM demo.mv_daily_sales ORDER BY order_date;

-- ========== 3. 开启自动刷新(间隔下推到所有子 DIMV) ==========
SELECT polar_matview.set_refresh_interval(
  'demo.mv_daily_sales'::regclass::oid, '10 seconds'::interval);

-- 增量刷新由后台 worker 执行,set_refresh_interval 本身不会触发刷新。
-- 必须先连到 pg_cron 的元数据库(默认 postgres)调度 worker,
-- 否则物化视图的数据会一直停留在创建时的快照:
--   SELECT cron.schedule_in_database(
--     'dimv-auto-refresh-worker-1', '1 second',
--     $$CALL polar_matview.auto_refresh_worker_main()$$, '<业务库>');
-- worker 的部署方式与普通 DIMV 完全一致,详见「增量物化视图」的“配置自动刷新与清理”章节。

-- ========== 4. 数据变更由 DIMV 增量应用 ==========
INSERT INTO demo.orders(order_date, user_id, amount) VALUES ('2024-01-05', 3, 50);
UPDATE demo.orders SET amount = 400 WHERE order_date = '2024-02-10';

-- worker 调度后等待一个刷新周期(本例为 10 秒)再查询,即可看到最新结果
SELECT * FROM demo.mv_daily_sales ORDER BY order_date;

-- ========== 5. 冻结不再变化的历史分区 ==========
SELECT polar_matview.set_freeze_interval(
  'demo.mv_daily_sales'::regclass, INTERVAL '30 days');

结构对齐需要配成pg_cron定期任务。这一步在pg_cron的元数据库(默认为postgres)中、以普通角色执行,schedule_in_database的最后一个参数是上面这些对象所在的业务库:

-- ========== 6. 定期结构对齐(连到 postgres 库,以 dimv_worker 身份执行) ==========
SELECT cron.schedule_in_database(
  'align-mv-daily-sales',
  '59 seconds',
  $$CALL polar_matview.refresh_partition_mv('demo', 'mv_daily_sales')$$,
  '<业务库>'
);

回到业务库,滚动一个新分区,等待一轮定期任务即可看到它被自动纳入维护:

-- ========== 7. 滚动新分区:由步骤 6 的定期任务自动纳入增量维护 ==========
CREATE TABLE demo.orders_202403 PARTITION OF demo.orders
  FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');

INSERT INTO demo.orders(order_date, user_id, amount) VALUES ('2024-03-15', 2, 500);

-- 等待一轮(约 1 分钟)后,新分区的子物化视图已建好、数据已填充,
-- 并继承了 10 秒的刷新间隔;也可以手动立即执行一次:
CALL polar_matview.refresh_partition_mv('demo', 'mv_daily_sales');

SELECT * FROM demo.mv_daily_sales ORDER BY order_date;

-- ========== 8. 清理 ==========
-- 在 postgres 库执行,取消定期任务
SELECT cron.unschedule('align-mv-daily-sales');

CALL polar_matview.drop_partition_mv('demo', 'mv_daily_sales');

工作原理

理解这个特性的关键是记住分区物化视图和DIMV引擎各管一块,两者不重叠:

变化类型

由谁处理

具体行为

源分区表新增/删除分区

分区物化视图

refresh_partition_mv同步结构,为新分区创建子DIMV并填充。

分区内的数据变更

DIMV引擎

通过mlog计算增量并合并进对应的子物化视图。

历史分区长期无变化

分区物化视图

按set_freeze_interval冻结子DIMV,退出自动刷新调度。

冻结分区重新发生变化

分区物化视图

refresh_partition_mv检测到版本变化,全量刷新该子DIMV使其解冻,重新交回DIMV引擎。

这解释了两个容易困惑的现象:

  • 对一个增量分区物化视图执行refresh_partition_mv,如果只有数据变化而结构没变,它会跳过该物化视图,数据看起来没有推进,因为数据归DIMV引擎自己维护,分区物化视图这一侧不去碰。

  • 反过来,被冻结的子物化视图不再由DIMV引擎维护,却仍然能在源分区的数据重新发生变化时自动恢复,因为版本检测和恢复动作都发生在refresh_partition_mv这一侧。

注意事项

  • 改名是安全的:元数据以OID和pg_inherits为准,不依赖名称。执行ALTER MATERIALIZED VIEW ... RENAME之后,用新名字继续管理即可,增量维护不受影响。

  • mlog不会被自动删除:删除分区或删除整个物化视图时,基表上的mlog都会保留,因为它可能被其他物化视图共享。确认不再需要时,请使用手动管理的接口清理,详情请参见增量物化视图。

  • 刷新间隔设在父物化视图上:直接对某个子DIMV调用set_refresh_interval只影响这一个子视图,父物化视图上记录的间隔不会随之改变;下次在父物化视图上设置间隔时,该子视图会被一并覆盖。