如何确保物化视图始终是最新的?

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% 最新。请记住,即使有或没有 CONCURRENTLYREFRESH 命令确实需要 运行 整个查询,因此您必须花时间 运行 内部在考虑安排 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();

注意事项

它在性能和并发性方面存在一些严重缺陷:

  1. 任何 INSERT/UPDATE/DELETE 操作都必须执行查询(如果您正在考虑 MV,这可能会很慢);
  2. 即使使用 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 秒内多次 NOTIFYREFRESH 只会被触发一次.

注意事项

作为 cron 选项,只有当您可以使用一些过时的数据时,这个选项才好用,但这的优点是 REFRESH 仅在真正需要时才调用,因此您的开销更少, 并且数据更新更接近需要的时候。

OBS:我还没有真正尝试过这些代码和示例,所以如果有人发现错误、打字错误或尝试过但有效(或无效),请告诉我。

让我在 MatheusOl 之前的回答中指出三点 - pglater 技术。

  1. 作为 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} 
    };
    
  2. 关于 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);
    
  3. 使用下面的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) {