Как узнать, было ли обновление upsert обновлением PostgreSQL 9.5+ UPSERT?
Writable CTE считались решением UPSERT до 9.5, как описано в Вставить, при дублированном обновлении в PostgreSQL?
Можно выполнить UPSERT с информацией о том, закончилось ли оно как UPDATE или INSERT со следующим идиомом Writable CTE:
WITH
update_cte AS (
UPDATE t SET v = $1 WHERE id = $2 RETURNING 'updated'::text status
),
insert_cte AS (
INSERT INTO t(id, v) SELECT $2, $1 WHERE NOT EXISTS
(SELECT 1 FROM update_cte) RETURNING 'inserted'::text status
)
(SELECT status FROM update_cte) UNION (SELECT status FROM insert_cte)
Этот запрос возвращает либо "обновленный", либо "вставленный", или может (редко) сбой с нарушением ограничения, как описано в https://dba.stackexchange.com/info/78510/why-is-cte-open-to-lost-updates
Можно ли добиться чего-то подобного, используя новый синтаксис PostgreSQL 9.5+ "UPSERT", используя его оптимизацию и избегая возможного нарушения ограничений?
Ответы
Ответ 1
Рисунок из @lad2025 answer, результат может быть достигнут путем злоупотребления settings и настраиваемые параметры с связанные функции в предложениях WHERE для получения необходимого побочного эффекта.
CREATE TABLE t(id INT PRIMARY KEY, v TEXT);
INSERT INTO t (id, v)
SELECT $1, $2
WHERE 'inserted' = set_config('upsert.action', 'inserted', true)
ON CONFLICT (id) DO UPDATE
SET v = EXCLUDED.v
WHERE 'updated' = set_config('upsert.action', 'updated', true)
RETURNING current_setting('upsert.action') AS "upsert.action";
Третий параметр set_config
равен is_local
: true
означает, что в конце транзакции параметр будет удален. Точнее, current_setting('upsert.action')
вернет NULL (и не выдаст ошибку) до конца сеанса.
Ответ 2
В операторе SQL Server
MERGE
есть $action
, который возвращает строку 'INSERT', 'UPDATE', or 'DELETE'
.
В Postgresql
я не могу найти функцию/переменную, которая похожа на RETURNING
.
Один из способов обхода пути - добавить столбец is_updated
в таблицу:
DROP TABLE IF EXISTS tab;
CREATE TABLE tab(id INT PRIMARY KEY, col VARCHAR(100),
is_updated BOOLEAN DEFAULT false);
INSERT INTO tab(id, col) VALUES (1,'a'), (2, 'b');
-- main query
INSERT INTO tab(id, col)
VALUES (3, 'c'), (4, 'd'), (1,'aaaa')
ON CONFLICT (id) DO UPDATE SET col = EXCLUDED.col, is_updated = true
RETURNING id,col,
CASE WHEN is_updated THEN 'UPDATED' ELSE 'INSERTED' END AS action;
Демо-версия реестров
Вывод:
╔════╦══════╦══════════╗
║ id ║ col ║ action ║
╠════╬══════╬══════════╣
║ 3 ║ c ║ INSERTED ║
║ 4 ║ d ║ INSERTED ║
║ 1 ║ aaaa ║ UPDATED ║
╚════╩══════╩══════════╝
Ответ 3
Я считаю, что xmax::text::int > 0
будет самым простым трюком:
so=# DROP TABLE IF EXISTS tab;
NOTICE: table "tab" does not exist, skipping
DROP TABLE
so=# CREATE TABLE tab(id INT PRIMARY KEY, col text);
CREATE TABLE
so=# INSERT INTO tab(id, col) VALUES (1,'a'), (2, 'b');
INSERT 0 2
so=# INSERT INTO tab(id, col)
VALUES (3, 'c'), (4, 'd'), (1,'aaaa')
ON CONFLICT (id) DO UPDATE SET col = EXCLUDED.col
returning *,case when xmax::text::int > 0 then 'updated' else 'inserted' end,ctid;
id | col | case | ctid
----+------+----------+-------
3 | c | inserted | (0,3)
4 | d | inserted | (0,4)
1 | aaaa | updated | (0,5)
(3 rows)
INSERT 0 3
so=# INSERT INTO tab(id, col)
VALUES (3, 'c'), (4, 'd'), (1,'aaaa')
ON CONFLICT (id) DO UPDATE SET col = EXCLUDED.col
returning *,case when xmax::text::int > 0 then 'updated' else 'inserted' end,ctid;
id | col | case | ctid
----+------+---------+-------
3 | c | updated | (0,6)
4 | d | updated | (0,7)
1 | aaaa | updated | (0,8)
(3 rows)
INSERT 0 3