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

Weird now() time difference with Postgres triggers

在 Postgres 10.10 数据库中,我有一个 table table1 和一个 AFTER INSERT 触发器 table1 for 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);

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

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();      

我注意到 table2 上的某些 created_atupdated_at 值与 table1 不同。事实上,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 |

序列中的连续条目具有相似的差异(大部分 negative/mostly 为正)和相似的数量级(主要是分钟对主要是小时),但也有例外

以下是前 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    |

以下是前 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|

负差异比正差异多 10 倍。数据库时区是 UTC。

table2.table1_id 是外键,因此在 table1 上的插入完成之前应该无法插入。

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

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

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

这是用于产生上述差异的查询 tables:

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;

虽然 'now' 不是普通字符串,但在此上下文中它也不是函数,而是 特殊字符串date/time输入The manual:

... simply notational shorthands that will be converted to ordinary date/time values when read. (In particular, now and related strings are converted to a specific time value as soon as they are read.)

PL/pgSQL 函数的主体存储为字符串,每个嵌套的 SQL 命令在控件第一次到达它时被解析和准备 每个会话 . The manual:

The PL/pgSQL interpreter parses the function's source text and produces an internal binary instruction tree the first time the function is called (within each session). The instruction tree fully translates the PL/pgSQL statement structure, but individual SQL expressions and SQL commands used in the function are not translated immediately.

As each expression and SQL command is first executed in the function, the PL/pgSQL interpreter parses and analyzes the command to create a prepared statement, using the SPI manager's SPI_prepare function. Subsequent visits to that expression or command reuse the prepared statement.

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

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

解决方案

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

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

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 <b>DEFAULT now()</b>,  -- 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 <b>(table1_id)</b>  -- more columns? but not: created_at, updated_at
   VALUES <b>(NEW.id)</b>;                -- more columns?

   RETURN NULL;                     -- can be NULL for AFTER trigger
END
$$;