奇怪的 now() 与 Postgres 触发器的时差

Ben*_*wis 7 postgresql timestamp transactions plpgsql sequelize.js

在 Postgres 10.10 数据库中,我有一个 tabletable1和一个AFTER INSERT触发器table1for table2

CREATE TABLE table1 (
    id SERIAL PRIMARY KEY,
    -- other cols
    created_at timestamp with time zone NOT NULL,
    updated_at timestamp with time zone NOT NULL
);

CREATE UNIQUE INDEX table1_pkey ON table1(id int4_ops);

CREATE TABLE table2 (
    id SERIAL PRIMARY KEY,
    table1_id integer NOT NULL REFERENCES table1(id) ON UPDATE CASCADE,
    -- other cols (not used in query)
    created_at timestamp with time zone NOT NULL,
    updated_at timestamp with time zone NOT NULL
);

CREATE UNIQUE INDEX table2_pkey ON table2(id int4_ops);
Run Code Online (Sandbox Code Playgroud)

此查询在应用程序启动时执行:

CREATE OR REPLACE FUNCTION after_insert_table1()
RETURNS trigger AS
$$
BEGIN
    INSERT INTO table2 (table1_id, ..., created_at, updated_at)
    VALUES (NEW.id, ..., 'now', 'now');
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';

DROP TRIGGER IF EXISTS after_insert_table1 ON "table1";

CREATE TRIGGER after_insert_table1
AFTER INSERT ON "table1"
FOR EACH ROW 
EXECUTE PROCEDURE after_insert_table1();      
Run Code Online (Sandbox Code Playgroud)

我注意到 somecreated_at和 上的updated_attable2table1. 事实上,table2大多具有较旧的值。

这里有 10 个连续的条目,它们显示了在几分钟内大幅波动的差异:

|table1_id|table1_created            |table2_created               |diff            |
|---------|--------------------------|-----------------------------|----------------|
|2000     |2019-11-07 22:29:47.245+00|2019-11-07 19:51:09.727021+00|-02:38:37.517979|
|2001     |2019-11-07 22:30:02.256+00|2019-11-07 13:18:29.45962+00 |-09:11:32.79638 |
|2002     |2019-11-07 22:30:43.021+00|2019-11-07 13:44:12.099577+00|-08:46:30.921423|
|2003     |2019-11-07 22:31:00.794+00|2019-11-07 19:51:09.727021+00|-02:39:51.066979|
|2004     |2019-11-07 22:31:11.315+00|2019-11-07 13:18:29.45962+00 |-09:12:41.85538 |
|2005     |2019-11-07 22:31:27.234+00|2019-11-07 13:44:12.099577+00|-08:47:15.134423|
|2006     |2019-11-07 22:31:47.436+00|2019-11-07 13:18:29.45962+00 |-09:13:17.97638 |
|2007     |2019-11-07 22:33:19.484+00|2019-11-07 17:22:48.129063+00|-05:10:31.354937|
|2008     |2019-11-07 22:33:51.607+00|2019-11-07 19:51:09.727021+00|-02:42:41.879979|
|2009     |2019-11-07 22:34:28.786+00|2019-11-07 13:18:29.45962+00 |-09:15:59.32638 |
|2010     |2019-11-07 22:36:50.242+00|2019-11-07 13:18:29.45962+00 |-09:18:20.78238 |
Run Code Online (Sandbox Code Playgroud)

顺序条目在序列中具有相似的差异(主要是负/主要是正)和相似的数量级(主要是几分钟 vs 主要是几小时),但也有例外

以下是前 5 个最大的积极差异:

|table1_id|table1_created            |table2_created               |diff            |
|---------|--------------------------|-----------------------------|----------------|
|1630     |2019-10-25 21:12:14.971+00|2019-10-26 00:52:09.376+00   |03:39:54.405    |
|950      |2019-09-16 12:36:07.185+00|2019-09-16 14:07:35.504+00   |01:31:28.319    |
|1677     |2019-10-26 22:19:12.087+00|2019-10-26 23:38:34.102+00   |01:19:22.015    |
|58       |2018-12-08 20:11:20.306+00|2018-12-08 21:06:42.246+00   |00:55:21.94     |
|171      |2018-12-17 22:24:57.691+00|2018-12-17 23:16:05.992+00   |00:51:08.301    |
Run Code Online (Sandbox Code Playgroud)

以下是前 5 个最大的负面差异:

|table1_id|table1_created            |table2_created               |diff            |
|---------|--------------------------|-----------------------------|----------------|
|1427     |2019-10-15 16:03:43.641+00|2019-10-14 17:59:41.57749+00 |-22:04:02.06351 |
|1426     |2019-10-15 13:26:07.314+00|2019-10-14 18:00:50.930513+00|-19:25:16.383487|
|1424     |2019-10-15 13:13:44.092+00|2019-10-14 18:00:50.930513+00|-19:12:53.161487|
|4416     |2020-01-11 00:15:03.751+00|2020-01-10 08:43:19.668399+00|-15:31:44.082601|
|4420     |2020-01-11 01:58:32.541+00|2020-01-10 11:04:19.288023+00|-14:54:13.252977|
Run Code Online (Sandbox Code Playgroud)

负差异的数量超过正差异的 10 倍。数据库时区是 UTC。

table2.table1_id是外键,因此在插入table1完成之前应该不可能插入。

table1.created_at由 Sequelize 设置,使用timestamps: true模型上的选项。

当一行插入到 中时table1,它是在事务中完成的。从我可以找到的文档中,触发器是在同一个事务中执行的,所以我想不出这样做的原因。

我可以通过将触发器更改为使用NEW.created_at而不是“现在”来解决此问题,但我很好奇是否有人知道此错误的原因是什么?

这是用于生成上述差异表的查询:

SELECT
    table1.id AS table1_id,
    table1.created_at AS table1_created,
    table2.created_at AS table2_created,
    (table2.created_at - table1.created_at) AS diff
FROM table1
INNER JOIN table2   ON 
    table2.table1_id = table1.id AND (
        (table2.created_at - table1.created_at) > '2 min' OR 
        (table1.created_at - table2.created_at) > '2 min')
ORDER BY diff;
Run Code Online (Sandbox Code Playgroud)

Erw*_*ter 6

虽然'now'不是纯字符串,但在此上下文中也不是函数,而是特殊的日期/时间输入手册:

...简单的符号简写,读取时将转换为普通的日期/时间值。(特别是,now相关字符串一旦被读取就会被转换为特定的时间值。)

PL/pgSQL 函数的主体存储为字符串,每个会话的控制权第一次到达时,每个嵌套的 SQL 命令都会被解析和准备。手册:

PL/pgSQL 解释器解析函数的源文本并在第一次调用函数时(在每个会话中)生成一个内部二进制指令树。指令树完全翻译了 PL/pgSQL 语句结构,但函数中使用的单个 SQL 表达式和 SQL 命令不会立即翻译。

由于每个表达式和 SQL 命令首先在函数中执行,PL/pgSQL 解释器使用 SPI 管理器的SPI_prepare函数解析和分析命令以创建准备好的语句。对该表达式或命令的后续访问会重用准备好的语句。

还有更多。继续阅读。但这对于我们的案例来说已经足够了:

每个会话第一次执行触发器时,'now'将转换为当前时间戳(事务时间戳)。在同一个事务中进行更多插入时,不会有任何区别,transaction_timestamp()因为这在设计上是稳定的。但是同一会话中的每个后续事务都会在 中插入相同的常量时间戳table2,而 的值table1可能是任何东西(不确定 Sequelize 在那里做了什么)。如果新值table1是当时的当前时间戳,则会导致测试中出现“负”差异。(时间戳table2会更旧。)

解决方案

您真正想要的'now'情况很少见。通常,您需要函数now()(没有单引号!) - 相当于CURRENT_TIMESTAMP(标准 SQL)和transaction_timestamp(). 相关(推荐阅读!):

在您的特定情况下,我建议使用列默认值而不是在触发器中做额外的工作。如果您now()table1and 中设置相同的默认值table2,您还可以消除INSERTtotable1可能添加的任何废话。而且您甚至不必再在插入中提及这些列:

CREATE TABLE table1 (
    id SERIAL PRIMARY KEY,
    -- other cols
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now()   -- or leave this one NULL?
);

CREATE TABLE table2 (
    id SERIAL PRIMARY KEY,
    table1_id integer NOT NULL REFERENCES table1(id) ON UPDATE CASCADE,
    -- other cols (not used in query)
    created_at timestamptz NOT NULL DEFAULT now(),  -- not 'now'!
    updated_at timestamptz NOT NULL DEFAULT now()   -- or leave this one NULL?
);

CREATE OR REPLACE FUNCTION after_insert_table1()
  RETURNS trigger LANGUAGE plpgsql AS
$$
BEGIN
   INSERT INTO table2 (table1_id)  -- more columns? but not: created_at, updated_at
   VALUES (NEW.id);                -- more columns?

   RETURN NULL;                     -- can be NULL for AFTER trigger
END
$$;
Run Code Online (Sandbox Code Playgroud)