【问题标题】:Weird now() time difference with Postgres triggers奇怪的 now() 与 Postgres 触发器的时间差
【发布时间】:2020-05-06 18:03:22
【问题描述】:

在 Postgres 10.10 数据库中,我有一个表 table1 和 AFTER INSERT 上的 table1 触发器 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_at 和updated_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 |

连续的条目在序列中具有相似的差异(大部分为负数/大部分为正数)和相似的数量级(大部分为分钟与大部分为小时),但也有例外

以下是前 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 而不是“现在”来解决此问题,但我很好奇是否有人知道此错误的原因是什么?

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

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' 不是时间戳,它是一个字符串。尝试使用 now() 函数。但是对于您的直接问题,您可以 new.created_at 从 table1 行。
  • 它们是相同的:SELECT 'now'::timestamp = now();。知道我可以使用NEW.created_at,正在询问其根本原因
  • 1.您确定这两个表都没有收到更新吗?您可以通过查看xmax 来识别插入的行。请参阅:stackoverflow.com/a/40880200/939860 2. 表table1 上是否还有其他可能干扰的触发器? 3、你的INSERT的目标列列表和VALUES列表的元素个数一样吗?
  • 一般情况下,请逐字显示完整的触发器和触发器函数定义,并始终显示您的 Postgres 版本。此外,错误可能出现在用于连接 table1 和 table2 以检查差异的查询中。我们真的需要查看查询和表定义。总而言之,这是一个水晶球的案例......
  • @ErwinBrandstetter 我已经在上面添加了要求的详细信息

标签: postgresql transactions timestamp sequelize.js plpgsql


【解决方案1】:

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

... 简单的符号速记,在读取时将转换为普通日期/时间值。 (特别是,now 和相关字符串在读取后立即转换为特定时间值。)

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

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()。相关(推荐阅读!):

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

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
$$;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-20
    • 1970-01-01
    • 2020-12-19
    • 2014-11-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多