Странная разница во времени () с триггерами Postgres - PullRequest
6 голосов
/ 20 января 2020

В базе данных 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();      

Я заметил, что некоторые значения created_at и updated_at на table2 отличаются от 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 раз. Часовой пояс базы данных - UT C.

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;

1 Ответ

5 голосов
/ 22 января 2020

Хотя 'now' не является простой строкой, в этом контексте это также не функция, а специальный ввод даты / времени . Руководство:

... просто сокращенные обозначения, которые при чтении будут преобразованы в обычные значения даты / времени. (В частности, now и связанные строки преобразуются в заданное c значение времени, как только они прочитаны.)

Тело функции PL / pg SQL сохраняется в виде строки каждая вложенная команда SQL анализируется и подготавливается, когда управление достигает ее в первый раз за сеанс . Руководство:

Интерпретатор PL / pg SQL анализирует исходный текст функции и создает внутреннее двоичное дерево инструкций при первом вызове функции (в рамках каждого сеанса) , Дерево команд полностью преобразует структуру оператора PL / pg SQL, но отдельные SQL выражения и SQL команды, используемые в функции, не переводятся немедленно.

Поскольку каждое выражение и SQL команда Сначала исполняемый в функции интерпретатор PL / pg SQL анализирует и анализирует команду для создания подготовленного оператора, используя функцию SPI_prepare менеджера SPI. Последующие посещения этого выражения или команды повторно используют подготовленное утверждение.

Есть и другие. Читать дальше. Но для нашего случая этого достаточно:

При первом запуске триггера за сеанс , 'now' преобразуется в текущую метку времени (метка времени транзакции). Делая больше вставок в той же транзакции, не будет никакой разницы с transaction_timestamp(), потому что она стабильна в рамках транзакции. Но каждая последующая транзакция в том же сеансе будет вставлять одну и ту же постоянную метку времени в table2, тогда как значения для table1 могут быть любыми (не уверен, что там делает Sequelize). Если новые значения в table1 представляют собой текущую временную метку, это приводит к «отрицательному» различию в вашем тесте. (Временные метки в table2 будут старше.)

Решение

Ситуации, когда вы действительно хотите 'now', немногочисленны. Как правило, вам нужна функция now() (без одинарных кавычек!), Которая эквивалентна CURRENT_TIMESTAMP (стандарт SQL) и transaction_timestamp(). Связанные (рекомендуется чтение!):

В вашем конкретном случае я предлагаю столбец по умолчанию вместо выполнения дополнительной работы в триггерах. Если вы установите одно и то же значение по умолчанию now() в table1 и table2, вы также устраните любые несуразицы, которые может добавить 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 <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
$$;
...