如何确保物化视图始终是最新的?
How can I ensure that a materialized view is always up to date?
我需要在对涉及的表进行每次更改时调用 REFRESH MATERIALIZED VIEW
,对吧?我很惊讶在网络上没有找到太多关于这个的讨论。
我应该怎么做?
我想这里答案的上半部分就是我要找的:
这样做有什么危险吗?如果更新视图失败,是否会回滚调用更新、插入等的事务? (这就是我想要的……我觉得)
I'll need to invoke REFRESH MATERIALIZED VIEW
on each change to the tables involved, right?
是的,PostgreSQL 本身永远不会自动调用它,您需要以某种方式调用它。
How should I go about doing this?
实现这一点的方法有很多。在给出一些示例之前,请记住 REFRESH MATERIALIZED VIEW
command 确实会在 AccessExclusive 模式下阻止视图,因此在它工作时,您甚至不能在 table.[=49 上执行 SELECT
=]
尽管如此,如果您使用的是 9.4 或更高版本,您可以为其提供 CONCURRENTLY
选项:
REFRESH MATERIALIZED VIEW CONCURRENTLY my_mv;
这将获取一个 ExclusiveLock,并且不会阻止 SELECT
查询,但可能会有更大的开销(取决于更改的数据量,如果更改的行很少,那么它可能会更快)。虽然你仍然不能 运行 同时执行两个 REFRESH
命令。
手动刷新
这是一个可以考虑的选项。特别是在数据加载或批量更新的情况下(例如,系统在很长一段时间后只加载大量 information/data),通常在最后进行操作以修改或处理数据,因此您可以简单地包含一个REFRESH
运行到此结束。
正在安排刷新操作
第一个也是广泛使用的选项是使用一些调度系统来调用刷新,例如,您可以在 cron 作业中配置类似的:
*/30 * * * * psql -d your_database -c "REFRESH MATERIALIZED VIEW CONCURRENTLY my_mv"
然后您的实体化视图将每 30 分钟刷新一次。
注意事项
这个选项非常好,特别是 CONCURRENTLY
选项,但前提是您可以接受数据并非始终 100% 最新。请记住,即使有或没有 CONCURRENTLY
,REFRESH
命令确实需要 运行 整个查询,因此您必须花时间 运行 内部在考虑安排 REFRESH
.
的时间之前查询
使用触发器刷新
另一种选择是在触发器函数中调用 REFRESH MATERIALIZED VIEW
,如下所示:
CREATE OR REPLACE FUNCTION tg_refresh_my_mv()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY my_mv;
RETURN NULL;
END;
$$;
然后,在涉及视图更改的任何 table 中,您可以:
CREATE TRIGGER tg_refresh_my_mv AFTER INSERT OR UPDATE OR DELETE
ON table_name
FOR EACH STATEMENT EXECUTE PROCEDURE tg_refresh_my_mv();
注意事项
它在性能和并发性方面存在一些严重缺陷:
- 任何 INSERT/UPDATE/DELETE 操作都必须执行查询(如果您正在考虑 MV,这可能会很慢);
- 即使使用
CONCURRENTLY
,一个 REFRESH
仍然会阻塞另一个,因此所涉及的 table 上的任何 INSERT/UPDATE/DELETE 都将被序列化。
我认为唯一的情况是更改真的很少见。
刷新使用LISTEN/NOTIFY
前一个选项的问题在于它是同步的,并且在每个操作中都会产生很大的开销。为了改善这一点,您可以像以前一样使用触发器,但这只会调用 NOTIFY
operation:
CREATE OR REPLACE FUNCTION tg_refresh_my_mv()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NOTIFY refresh_mv, 'my_mv';
RETURN NULL;
END;
$$;
因此您可以构建一个保持连接并使用 LISTEN
operation to identify the need to call REFRESH
. One nice project that you can use to test this is pgsidekick 的应用程序,在这个项目中您可以使用 shell 脚本来执行 LISTEN
,因此您可以安排 REFRESH
为:
pglisten --listen=refresh_mv --print0 | xargs -0 -n1 -I? psql -d your_database -c "REFRESH MATERIALIZED VIEW CONCURRENTLY ?;"
或使用 pglater
(也在 pgsidekick
内)以确保您不会经常调用 REFRESH
。例如,你可以使用下面的触发器使它成为 REFRESH
,但在 1 分钟(60 秒)内:
CREATE OR REPLACE FUNCTION tg_refresh_my_mv()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NOTIFY refresh_mv, '60 REFRESH MATERIALIZED VIEW CONCURRENLTY my_mv';
RETURN NULL;
END;
$$;
所以它不会在 60 秒内调用 REFRESH
,而且如果你在 60 秒内多次 NOTIFY
,REFRESH
只会被触发一次.
注意事项
作为 cron 选项,只有当您可以使用一些过时的数据时,这个选项才好用,但这的优点是 REFRESH
仅在真正需要时才调用,因此您的开销更少, 并且数据更新更接近需要的时候。
OBS:我还没有真正尝试过这些代码和示例,所以如果有人发现错误、打字错误或尝试过但有效(或无效),请告诉我。
让我在 MatheusOl 之前的回答中指出三点 - pglater 技术。
作为 long_options 数组的最后一个元素,它应该包含短语 [=45] 指向 https://linux.die.net/man/3/getopt_long 的“{0, 0, 0, 0}”元素=] 所以,它应该是 -
static struct option long_options[] = {
//......
{"help", no_argument, NULL, '?'},
{0, 0, 0, 0}
};
关于 malloc/free 的事情——一个免费的(对于 char listen = malloc(...);)不见了。无论如何,malloc 导致 pglater 进程在 CentOS 上崩溃(但在 Ubuntu 上没有——我不知道为什么)。因此,我建议使用 char 数组并将数组名称分配给 char 指针(同时分配给 char 和 char**)。在执行此操作时,您可能需要强制进行类型转换(指针赋值)。
char block4[100];
...
password_prompt = block4;
...
char block1[500];
const char **keywords = (const char **)&block1;
...
char block3[300];
char *listen = block3;
sprintf(listen, "listen %s", id);
PQfreemem(id);
res = PQexec(db, listen);
使用下面的table来计算超时,其中md是mature_duration这是最新的refresh(lr)时间点和当前时间之间的时间差。
当 md >= callback_delay(cd) ==> 超时:0
当 md + PING_INTERVAL >= cd ==> 超时:cd-md[=cd-(now-lr)]
当 md + PING_INTERVAL < cd ==> 超时:PI
要实现此算法(第 3 点),您应该按如下方式初始化 'lr' -
res = PQexec(db, command);
latest_refresh = time(0);
if (PQresultStatus(res) == PGRES_COMMAND_OK) {
我需要在对涉及的表进行每次更改时调用 REFRESH MATERIALIZED VIEW
,对吧?我很惊讶在网络上没有找到太多关于这个的讨论。
我应该怎么做?
我想这里答案的上半部分就是我要找的:
这样做有什么危险吗?如果更新视图失败,是否会回滚调用更新、插入等的事务? (这就是我想要的……我觉得)
I'll need to invoke
REFRESH MATERIALIZED VIEW
on each change to the tables involved, right?
是的,PostgreSQL 本身永远不会自动调用它,您需要以某种方式调用它。
How should I go about doing this?
实现这一点的方法有很多。在给出一些示例之前,请记住 REFRESH MATERIALIZED VIEW
command 确实会在 AccessExclusive 模式下阻止视图,因此在它工作时,您甚至不能在 table.[=49 上执行 SELECT
=]
尽管如此,如果您使用的是 9.4 或更高版本,您可以为其提供 CONCURRENTLY
选项:
REFRESH MATERIALIZED VIEW CONCURRENTLY my_mv;
这将获取一个 ExclusiveLock,并且不会阻止 SELECT
查询,但可能会有更大的开销(取决于更改的数据量,如果更改的行很少,那么它可能会更快)。虽然你仍然不能 运行 同时执行两个 REFRESH
命令。
手动刷新
这是一个可以考虑的选项。特别是在数据加载或批量更新的情况下(例如,系统在很长一段时间后只加载大量 information/data),通常在最后进行操作以修改或处理数据,因此您可以简单地包含一个REFRESH
运行到此结束。
正在安排刷新操作
第一个也是广泛使用的选项是使用一些调度系统来调用刷新,例如,您可以在 cron 作业中配置类似的:
*/30 * * * * psql -d your_database -c "REFRESH MATERIALIZED VIEW CONCURRENTLY my_mv"
然后您的实体化视图将每 30 分钟刷新一次。
注意事项
这个选项非常好,特别是 CONCURRENTLY
选项,但前提是您可以接受数据并非始终 100% 最新。请记住,即使有或没有 CONCURRENTLY
,REFRESH
命令确实需要 运行 整个查询,因此您必须花时间 运行 内部在考虑安排 REFRESH
.
使用触发器刷新
另一种选择是在触发器函数中调用 REFRESH MATERIALIZED VIEW
,如下所示:
CREATE OR REPLACE FUNCTION tg_refresh_my_mv()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY my_mv;
RETURN NULL;
END;
$$;
然后,在涉及视图更改的任何 table 中,您可以:
CREATE TRIGGER tg_refresh_my_mv AFTER INSERT OR UPDATE OR DELETE
ON table_name
FOR EACH STATEMENT EXECUTE PROCEDURE tg_refresh_my_mv();
注意事项
它在性能和并发性方面存在一些严重缺陷:
- 任何 INSERT/UPDATE/DELETE 操作都必须执行查询(如果您正在考虑 MV,这可能会很慢);
- 即使使用
CONCURRENTLY
,一个REFRESH
仍然会阻塞另一个,因此所涉及的 table 上的任何 INSERT/UPDATE/DELETE 都将被序列化。
我认为唯一的情况是更改真的很少见。
刷新使用LISTEN/NOTIFY
前一个选项的问题在于它是同步的,并且在每个操作中都会产生很大的开销。为了改善这一点,您可以像以前一样使用触发器,但这只会调用 NOTIFY
operation:
CREATE OR REPLACE FUNCTION tg_refresh_my_mv()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NOTIFY refresh_mv, 'my_mv';
RETURN NULL;
END;
$$;
因此您可以构建一个保持连接并使用 LISTEN
operation to identify the need to call REFRESH
. One nice project that you can use to test this is pgsidekick 的应用程序,在这个项目中您可以使用 shell 脚本来执行 LISTEN
,因此您可以安排 REFRESH
为:
pglisten --listen=refresh_mv --print0 | xargs -0 -n1 -I? psql -d your_database -c "REFRESH MATERIALIZED VIEW CONCURRENTLY ?;"
或使用 pglater
(也在 pgsidekick
内)以确保您不会经常调用 REFRESH
。例如,你可以使用下面的触发器使它成为 REFRESH
,但在 1 分钟(60 秒)内:
CREATE OR REPLACE FUNCTION tg_refresh_my_mv()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NOTIFY refresh_mv, '60 REFRESH MATERIALIZED VIEW CONCURRENLTY my_mv';
RETURN NULL;
END;
$$;
所以它不会在 60 秒内调用 REFRESH
,而且如果你在 60 秒内多次 NOTIFY
,REFRESH
只会被触发一次.
注意事项
作为 cron 选项,只有当您可以使用一些过时的数据时,这个选项才好用,但这的优点是 REFRESH
仅在真正需要时才调用,因此您的开销更少, 并且数据更新更接近需要的时候。
OBS:我还没有真正尝试过这些代码和示例,所以如果有人发现错误、打字错误或尝试过但有效(或无效),请告诉我。
让我在 MatheusOl 之前的回答中指出三点 - pglater 技术。
作为 long_options 数组的最后一个元素,它应该包含短语 [=45] 指向 https://linux.die.net/man/3/getopt_long 的“{0, 0, 0, 0}”元素=] 所以,它应该是 -
static struct option long_options[] = { //...... {"help", no_argument, NULL, '?'}, {0, 0, 0, 0} };
关于 malloc/free 的事情——一个免费的(对于 char listen = malloc(...);)不见了。无论如何,malloc 导致 pglater 进程在 CentOS 上崩溃(但在 Ubuntu 上没有——我不知道为什么)。因此,我建议使用 char 数组并将数组名称分配给 char 指针(同时分配给 char 和 char**)。在执行此操作时,您可能需要强制进行类型转换(指针赋值)。
char block4[100]; ... password_prompt = block4; ... char block1[500]; const char **keywords = (const char **)&block1; ... char block3[300]; char *listen = block3; sprintf(listen, "listen %s", id); PQfreemem(id); res = PQexec(db, listen);
使用下面的table来计算超时,其中md是mature_duration这是最新的refresh(lr)时间点和当前时间之间的时间差。
当 md >= callback_delay(cd) ==> 超时:0
当 md + PING_INTERVAL >= cd ==> 超时:cd-md[=cd-(now-lr)]
当 md + PING_INTERVAL < cd ==> 超时:PI
要实现此算法(第 3 点),您应该按如下方式初始化 'lr' -
res = PQexec(db, command); latest_refresh = time(0); if (PQresultStatus(res) == PGRES_COMMAND_OK) {