pg_ivm
pg_ivm模块为PostgreSQL提供增量视图维护(IVM)功能。 该扩展与PostgreSQL 13、14、15、16和17版本兼容。
描述
**增量视图维护(IVM)**是一种使物化视图保持最新的方法,它只计算并应用视图的增量变化,而不像REFRESH MATERIALIZED VIEW
那样从头重新计算内容。当视图只有小部分发生变化时,IVM比重新计算更有效地更新物化视图。
关于视图维护的时机,有两种方法:即时维护和延迟维护。在即时维护中,视图在其基表被修改的同一事务中更新。在延迟维护中,视图在事务提交后更新,例如,在访问视图时,响应用户命令如REFRESH MATERIALIZED VIEW
,或定期在后台更新等。pg_ivm
提供了一种即时维护方式,即当基表被修改时,物化视图立即在AFTER触发器中更新。
我们将支持IVM的物化视图称为增量可维护物化视图(IMMV)。要创建IMMV,你需要使用关系名称和视图定义查询调用create_immv
函数。例如:
SELECT create_immv('myview', 'SELECT * FROM mytab');
这将创建一个名为'myview'的IMMV,定义为SELECT * FROM mytab
。这相当于以下创建普通物化视图的命令:
CREATE MATERIALIZED VIEW myview AS SELECT * FROM mytab;
当创建IMMV时,会自动创建一些触发器,以便在其基表被修改时立即更新视图的内容。
postgres=# SELECT create_immv('m', 'SELECT * FROM t0');
注意:无法自动在immv "m"上创建索引
详细信息:此目标列表没有包含所有主键列,或此视图不包含DISTINCT子句。
提示:在immv上创建索引以实现高效的增量维护。
create_immv
-------------
3
(1行)
postgres=# SELECT * FROM m;
i
---
1
2
3
(3行)
postgres=# INSERT INTO t0 VALUES (4);
INSERT 0 1
postgres=# SELECT * FROM m; -- 自动更新
i
---
1
2
3
4
(4行)
请注意,如果你使用PostgreSQL 17或更高版本,在IMMV的自动维护期间,search_path
会暂时更改为pg_catalog, pg_temp
。
安装
要安装pg_ivm
,请在模块目录中执行以下命令:
make install
如果您是通过rpm或deb安装的PostgreSQL,则需要开发包(例如,postgresql14-devel或postgresql-server-dev-14)。
重要提示: 如果您想在非默认或自定义构建的PostgreSQL上使用
pg_ivm
,请不要忘记设置PG_CONFIG
变量(make PG_CONFIG=...
)或将pg_config
命令的路径添加到PATH
中。更多信息请参阅这里。 然后,执行CREATE EXTENSION命令。
CREATE EXTENSION pg_ivm;
RPM包和yum仓库
pg_ivm的RPM包可以从PostgreSQL yum仓库获取。详细信息请参阅说明。请注意,我们不是该yum仓库的维护者,其中的pg_ivm RPM包可能并非总是最新版本。
对象
当安装了pg_ivm
后,将创建以下对象。
函数
create_immv
使用create_immv
函数创建IMMV。
create_immv(immv_name text, view_definition text) RETURNS bigint
create_immv
定义一个新的查询IMMV。它会创建一个名为immv_name
的表,执行由view_definition
指定的查询,并用结果填充IMMV。该查询会被存储在pg_ivm_immv
中,以便之后进行增量视图维护时刷新。create_immv
返回创建的IMMV中的行数。
创建IMMV时,系统会自动创建一些触发器,以便在基表被修改时立即更新视图内容。此外,如果可能的话,还会自动为IMMV创建一个唯一索引。如果视图定义查询有GROUP BY子句,则会在GROUP BY表达式的列上创建唯一索引。如果视图有DISTINCT子句,则会在目标列表的所有列上创建唯一索引。否则,如果IMMV的目标列表中包含其基表的所有主键属性,则会在这些属性上创建唯一索引。在其他情况下,不会创建索引。
注意,如果你使用PostgreSQL 17或更高版本,在create_immv
运行期间,search_path
会临时更改为pg_catalog, pg_temp
。
refresh_imm
使用refresh_immv
函数刷新IMMV。
refresh_immv(immv_name text, with_data bool) RETURNS bigint
refresh_immv
完全替换IMMV的内容,就像REFRESH MATERIALIZED VIEW
命令对物化视图所做的那样。要执行此函数,你必须是IMMV的所有者(在PostgreSQL 16或更早版本中)或对IMMV拥有MAINTAIN
权限(在PostgreSQL 17或更高版本中)。旧内容将被丢弃。
with_data标志对应于REFRESH MATERIALIZED VIEW
命令的WITH [NO] DATA
选项。如果with_data为true,将执行后台查询以提供新数据,如果IMMV未填充,则创建用于维护视图的触发器。此外,如果可能且视图尚未拥有,还会为IMMV创建唯一索引。如果with_data为false,则不会生成新数据,IMMV将变为未填充状态,并且会从IMMV中删除触发器。注意,未填充的IMMV仍然可以扫描,尽管结果为空。这种行为在将来可能会改变,当扫描未填充的IMMV时可能会引发错误。
注意,如果你使用PostgreSQL 17或更高版本,在refresh_immv
运行期间,search_path
会临时更改为pg_catalog, pg_temp
。
get_immv_def
get_immv_def
重构IMMV的底层SELECT命令。(这是一个反编译重构,而不是命令的原始文本。)
get_immv_def(immv regclass) RETURNS text
IMMV元数据目录
目录pg_ivm_immv
存储IMMV信息。
名称 | 类型 | 描述 |
---|---|---|
immvrelid | regclass | IMMV的OID |
viewdef | text | 视图定义的查询树(以nodeToString()表示形式) |
ispopulated | bool | 如果IMMV当前已填充则为True |
示例
总的来说,IMMV允许比REFRESH MATERIALIZED VIEW
更快的更新,但代价是对其基表的更新速度较慢。基表更新较慢是因为需要触发触发器,并且IMMV在每个修改语句的触发器中更新。
例如,假设有一个如下定义的普通物化视图:
test=# CREATE MATERIALIZED VIEW mv_normal AS
SELECT a.aid, b.bid, a.abalance, b.bbalance
FROM pgbench_accounts a JOIN pgbench_branches b USING(bid);
SELECT 10000000
更新这个物化视图的基表中的一个元组很快,但对这个视图执行REFRESH MATERIALIZED VIEW
命令需要很长时间:
test=# UPDATE pgbench_accounts SET abalance = 1000 WHERE aid = 1;
UPDATE 1
Time: 9.052 ms
test=# REFRESH MATERIALIZED VIEW mv_normal ;
REFRESH MATERIALIZED VIEW
Time: 20575.721 ms (00:20.576)
另一方面,使用相同的视图定义创建IMMV后:
test=# SELECT create_immv('immv',
'SELECT a.aid, b.bid, a.abalance, b.bbalance
FROM pgbench_accounts a JOIN pgbench_branches b USING(bid)');
NOTICE: created index "immv_index" on immv "immv"
create_immv
-------------
10000000
(1 row)
更新基表中的一个元组比普通视图需要更多时间,但其内容会自动更新,这比REFRESH MATERIALIZED VIEW
命令更快。
test=# UPDATE pgbench_accounts SET abalance = 1234 WHERE aid = 1;
UPDATE 1
Time: 15.448 ms
test=# SELECT * FROM immv WHERE aid = 1;
aid | bid | abalance | bbalance
-----+-----+----------+----------
1 | 1 | 1234 | 0
(1 row)
IMMV上的适当索引对于高效的IVM是必要的,因为我们需要在IMMV中查找要更新的元组。如果没有索引,将会花费很长时间。
因此,当通过create_immv
函数创建IMMV时,如果可能,会自动在其上创建唯一索引。如果视图定义查询有GROUP BY子句,会在GROUP BY表达式的列上创建唯一索引。此外,如果视图有DISTINCT子句,会在目标列表中的所有列上创建唯一索引。否则,如果IMMV在目标列表中包含其所有基表的主键属性,会在这些属性上创建唯一索引。在其他情况下,不会创建索引。
在前面的例子中,在"immv"的aid和bid列上创建了一个名为"immv_index"的唯一索引,这使得视图的快速更新成为可能。删除这个索引会使更新视图花费更长的时间。
test=# DROP INDEX immv_index;
DROP INDEX
test=# UPDATE pgbench_accounts SET abalance = 9876 WHERE aid = 1;
UPDATE 1
Time: 3224.741 ms (00:03.225)
支持的视图定义和限制
目前,IMMV的视图定义可以包含内连接、DISTINCT子句、一些内置聚合函数、FROM
子句中的简单子查询、EXISTS子查询和简单的CTE(WITH
查询)。支持包括自连接在内的内连接,但不支持外连接。支持的聚合函数有count、sum、avg、min和max。其他聚合函数、包含聚合或DISTINCT
子句的子查询、FROM
子句以外的子查询、窗口函数、HAVING
、ORDER BY
、LIMIT
/OFFSET
、UNION
/INTERSECT
/EXCEPT
、DISTINCT ON
、TABLESAMPLE
、VALUES
以及FOR UPDATE
/SHARE
不能在视图定义中使用。
基表必须是简单表。不能使用视图、物化视图、继承父表、分区表、分区和外部表。
视图定义查询中不能包含任何系统列。
目标列表不能包含以__ivm_
开头的列名。
视图目标列表中使用的数据类型必须具有btree访问方法的默认运算符类。例如,json
、xml
或point
类型不能出现在目标列表中。
不支持逻辑复制,也就是说,即使发布节点上的基表被修改,订阅节点上基于这些基表定义的IMMV也不会更新。
注意事项
聚合函数
支持的聚合函数包括count
、sum
、avg
、min
和max
。目前仅支持内置聚合函数,不能使用用户自定义聚合。
当创建包含聚合的IMMV时,一些以__ivm
开头的额外列会自动添加到目标列表中。__ivm_count__
包含每个分组中聚合的元组数。此外,还会为每个聚合值列添加多个额外列以维护其值。例如,为维护平均值会添加名为__ivm_count_avg__
和__ivm_sum_avg__
的列。当基表被修改时,新的聚合值会使用旧的聚合值和存储在IMMV中相关额外列的值进行增量计算。
注意,对于min
或max
,当包含当前最小或最大值的元组从基表中删除时,可能需要从基表重新计算受影响分组的新值。因此,更新包含这些函数的IMMV可能需要较长时间。
另外,在IMMV中对real
(float4
)类型或double precision
(float8
)类型使用sum
或avg
是不安全的,因为由于这些类型的精度有限,IMMV中的聚合值可能与从基表计算的结果不同。为避免此问题,请使用numeric
类型。
聚合的限制
如果有GROUP BY
子句,GROUP BY
中指定的表达式必须出现在目标列表中。这是用于识别IMMV中需要更新的元组的方式。这些属性用作搜索IMMV中元组的扫描键,因此需要在它们上建立索引以实现高效的IVM。
目标列表不能包含含有聚合的表达式。
子查询
支持FROM
子句中的简单子查询和WHERE
子句中的EXISTS子查询。
子查询的限制
支持使用EXISTS的子查询和FROM子句中的简单子查询。不支持除'AND'以外条件的EXISTS子查询和目标列表中的子查询。EXISTS子查询仅支持在WHERE中使用,不支持在目标列表中使用。
如果EXISTS包含引用外部查询表中列的列,这些列必须包含在目标列表中。
不支持包含聚合函数或DISTINCT
的子查询。
CTE
支持简单的CTE(WITH
查询)。
CTE的限制
不支持包含聚合函数或DISTINCT
的WITH
查询。
不允许递归查询(WITH RECURSIVE
)。也不允许未引用的CTE,即CTE必须在视图定义查询中至少被引用一次。
DISTINCT
IMMV的定义查询中允许使用DISTINCT
。假设在包含重复元组的基表上定义了带DISTINCT的IMMV。当从基表删除元组时,只有当元组的重复数变为零时,视图中的元组才会被删除。此外,当向基表插入元组时,只有当视图中不存在相同的元组时,才会向视图中插入元组。
从物理上讲,使用DISTINCT
定义的IMMV包含消除重复后的元组,每个元组的重复数存储在创建此类IMMV时添加的名为__ivm_count__
的额外列中。
TRUNCATE
当基表被截断时,如果视图定义查询不包含没有GROUP BY
子句的聚合,IMMV也会被截断,内容变为空。没有GROUP BY
子句的聚合视图始终有一行。因此,在这种情况下,如果基表被截断,IMMV只会被刷新而不是被截断。
并发事务
假设一个IMMV定义在两个基表上,每个表在不同的并发事务中同时被修改。在先提交的事务中,IMMV可以只考虑该事务中发生的变化进行更新。另一方面,为了在后提交的事务中正确更新IMMV,我们需要知道两个事务中发生的变化。因此,在READ COMMITTED
模式下,一旦基表被修改,就会立即在IMMV上持有ExclusiveLock
,以确保IMMV在后一个事务中在前一个事务提交后更新。在REPEATABLE READ
或SERIALIZABLE
模式下,如果锁获取失败,会立即引发错误,因为在这些模式下其他事务中发生的任何变化都不可见,在这种情况下IMMV无法正确更新。但是,作为例外,如果IMMV只有一个基表且不使用DISTINCT或GROUP BY,并且表通过INSERT
修改,那么在IMMV上持有的锁是RowExclusiveLock
。
行级安全性
如果某些基表有行级安全策略,对物化视图所有者不可见的行将从结果中排除。此外,在视图增量维护时,这些行也会被排除。然而,如果在创建物化视图后定义了新策略或更改了策略,新策略不会应用于视图内容。要应用新策略,需要重新创建IMMV。
如何禁用或启用即时维护
当我们希望保持IMMV最新,且基表的小部分不频繁修改时,IVM是有效的。由于即时维护的开销,当基表频繁修改时IVM不太有效。此外,当修改基表的大部分或向基表插入大量数据时,IVM不太有效,维护成本可能大于从头刷新。
在这种情况下,我们可以在修改基表之前使用refesh_immv
函数,并将with_data
设为false来禁用即时维护。修改基表后,调用refresh_immv
并将with_data
设为true来刷新视图数据并启用即时维护。
作者
IVM 开发团队
许可证
版权声明
- 部分版权所有 © 1996-2022,PostgreSQL 全球开发组
- 部分版权所有 © 2022,IVM 开发组