Postgres 9.5 имплементира UPSERT
. Вижте по-долу.
Postgres 9.4 или по-стара версия
Това е труден проблем. Сблъсквате се с това ограничение (по документация):
В
VALUES
списък, който се появява на най-горното ниво наINSERT
, анекспресията може да бъде заменена сDEFAULT
за да посочите, че стойността по подразбиране на колоната дестинация трябва да бъде вмъкната.DEFAULT
не може да се използва, когатоVALUES
се появява в друг контекст
Удебелен акцент мой. Стойностите по подразбиране не се дефинират без таблица за вмъкване. Така че няма директен решение на вашия въпрос, но има редица възможни алтернативни маршрути, в зависимост от точните изисквания .
Извличане на стойностите по подразбиране от системния каталог?
Вие можете извлечете ги от системния каталог pg_attrdef
като @Patrick коментира или от information_schema.columns
. Пълни инструкции тук:
- Вземете ли стойностите по подразбиране на колоните на таблицата в Postgres?
Но тогава ти все още имат само списък с редове с текстово представяне на израза за приготвяне на стойността по подразбиране. Ще трябва да изграждате и изпълнявате изрази динамично, за да получите стойности, с които да работите. Досадно и разхвърляно. Вместо това можем да оставим вградената функционалност на Postgres да направи това вместо нас :
Прост пряк път
Поставете фиктивен ред и го върнете, за да използва генерирани стойности по подразбиране:
INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
Проблеми/обхват на решението
- Това гарантирано работи само за
STABLE
илиIMMUTABLE
изрази по подразбиране . Най-VOLATILE
функциите ще работят също толкова добре, но няма гаранции.current_timestamp
семейството от функции се квалифицира като стабилно, тъй като техните стойности не се променят в рамките на транзакция.
По-специално, това има странични ефекти върхуserial
колони (или всякакви други чертежи по подразбиране от последователност). Но това не би трябвало да е проблем, защото обикновено не пишете вserial
колони директно. Те не трябва да са изброени вINSERT
изявления изобщо.
Оставащ недостатък заserial
колони:последователността все още се напредва от единичното извикване, за да се получи ред по подразбиране, което води до пропуск в номерирането. Отново, това не би трябвало да е проблем, защото пропуските по принцип се очакват вserial
колони.
Могат да бъдат решени още два проблема:
-
Ако имате дефинирани колони
NOT NULL
, трябва да вмъкнете фиктивни стойности и да ги замените сNULL
в резултата. -
Всъщност не искаме да вмъкваме фиктивния ред . Бихме могли да изтрием по-късно (в същата транзакция), но това може да има повече странични ефекти, като задействания
ON DELETE
. Има по-добър начин:
Избягвайте фиктивния ред
Клонирайте временна таблица включително колони по подразбиране и вмъкнете в това :
BEGIN;
CREATE TEMP TABLE tmp_playlist_items (LIKE playlist_items INCLUDING DEFAULTS)
ON COMMIT DROP; -- drop at end of transaction
INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *;
...
Същият резултат, по-малко странични ефекти. Тъй като изразите по подразбиране се копират дословно, клонингът черпи от същите последователности, ако има такива. Но други странични ефекти от нежелания ред или тригери се избягват напълно.
Благодарение на Игор за идеята:
- Postgresql, изберете "фалшив" ред
Премахнете NOT NULL
ограничения
Ще трябва да предоставите фиктивни стойности за NOT NULL
колони, защото (по документация):
Ограниченията, които не са нулеви, винаги се копират в новата таблица.
Или настанете за тези в INSERT
изявление или (по-добре) премахване на ограниченията:
ALTER TABLE tmp_playlist_items
ALTER COLUMN foo DROP NOT NULL
, ALTER COLUMN bar DROP NOT NULL;
Има побърз и мръсен начин с привилегии на суперпотребител:
UPDATE pg_attribute
SET attnotnull = FALSE
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0;
Това е просто временна таблица без данни и друга цел и се отпада в края на транзакцията. Така че прекият път е изкушаващ. Все пак основното правило е:никога не подправяйте директно системните каталози.
И така, нека да разгледаме почист начин :Автоматизирайте с динамичен SQL в DO
изявление. Нуждаете се само от обикновените привилегии гарантирано, че имате, тъй като същата роля е създала временната таблица.
DO $$BEGIN
EXECUTE (
SELECT 'ALTER TABLE tmp_playlist_items ALTER '
|| string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
|| ' DROP NOT NULL'
FROM pg_catalog.pg_attribute
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0
);
END$$
Много по-чисто и все пак много бързо. Изпълнявайте грижи с динамични команди и внимавайте при SQL инжектиране. Това твърдение е безопасно. Публикувах няколко свързани отговора с повече обяснения.
Общо решение (9.4 и по-стари)
BEGIN;
CREATE TEMP TABLE tmp_playlist_items
(LIKE playlist_items INCLUDING DEFAULTS) ON COMMIT DROP;
DO $$BEGIN
EXECUTE (
SELECT 'ALTER TABLE tmp_playlist_items ALTER '
|| string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
|| ' DROP NOT NULL'
FROM pg_catalog.pg_attribute
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0
);
END$$;
LOCK TABLE playlist_items IN EXCLUSIVE MODE; -- forbid concurrent writes
WITH default_row AS (
INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *
)
, new_values (id, playlist, item, group_name, duration, sort, legacy) AS (
VALUES
(651, 21, 30012, 'a', 30, 1, FALSE)
, (NULL, 21, 1, 'b', 34, 2, NULL)
, (668, 21, 30012, 'c', 30, 3, FALSE)
, (7428, 21, 23068, 'd', 0, 4, FALSE)
)
, upsert AS ( -- *not* replacing existing values in UPDATE (?)
UPDATE playlist_items m
SET ( playlist, item, group_name, duration, sort, legacy)
= (n.playlist, n.item, n.group_name, n.duration, n.sort, n.legacy)
-- ..., COALESCE(n.legacy, m.legacy) -- see below
FROM new_values n
WHERE n.id = m.id
RETURNING m.id
)
INSERT INTO playlist_items
(playlist, item, group_name, duration, sort, legacy)
SELECT n.playlist, n.item, n.group_name, n.duration, n.sort
, COALESCE(n.legacy, d.legacy)
FROM new_values n, default_row d -- single row can be cross-joined
WHERE NOT EXISTS (SELECT 1 FROM upsert u WHERE u.id = n.id)
RETURNING id;
COMMIT;
Нуждаете се само от LOCK
ако имате едновременни транзакции, които се опитват да запишат в една и съща таблица.
Както е поискано, това заменя само NULL стойности в колоната legacy
във входните редове за INSERT
случай. Може лесно да се разшири, за да работи за други колони или в UPDATE
случай също. Например, можете да UPDATE
условно също:само ако входната стойност е NOT NULL
. Добавих коментиран ред към UPDATE
по-горе.
Настрана:Не е нужно да прехвърляте стойности във всеки ред, освен първия в VALUES
израз, тъй като типовете се извличат от първия ред.
Postgres 9.5
прилага UPSERT с INSERT .. ON CONFLICT .. DO NOTHING | UPDATE
. Това до голяма степен опростява операцията:
INSERT INTO playlist_items AS m (id, playlist, item, group_name, duration, sort, legacy)
VALUES (651, 21, 30012, 'a', 30, 1, FALSE)
, (DEFAULT, 21, 1, 'b', 34, 2, DEFAULT) -- !
, (668, 21, 30012, 'c', 30, 3, FALSE)
, (7428, 21, 23068, 'd', 0, 4, FALSE)
ON CONFLICT (id) DO UPDATE
SET (playlist, item, group_name, duration, sort, legacy)
= (EXCLUDED.playlist, EXCLUDED.item, EXCLUDED.group_name
, EXCLUDED.duration, EXCLUDED.sort, EXCLUDED.legacy)
-- (..., COALESCE(l.legacy, EXCLUDED.legacy)) -- see below
RETURNING m.id;
Можем да прикачим VALUES
клауза за INSERT
директно, което позволява DEFAULT
ключова дума. В случай на уникални нарушения на (id)
, актуализации на Postgres вместо това. Можем да използваме изключени редове в UPDATE
. Ръководството:
SET
иWHERE
клаузи вON CONFLICT DO UPDATE
имат достъп до съществуващия ред, използвайки името на таблицата (или псевдоним) и до редове, предложени за вмъкване с помощта на специалнияexcluded
таблица.
И:
Обърнете внимание, че ефектите на всички
BEFORE INSERT
за всеки ред тригерите се отразяват в изключените стойности, тъй като тези ефекти може да са допринесли редът да бъде изключен от вмъкване.
Оставащ ъглов корпус
Имате различни опции за UPDATE
:Можете...
- ... изобщо не се актуализира:добавете
WHERE
клауза къмUPDATE
за запис само в избрани редове. - ... актуализирайте само избраните колони.
- ... актуализирайте само ако колоната в момента е NULL:
COALESCE(l.legacy, EXCLUDED.legacy)
- ... актуализирайте само ако новата стойност е
NOT NULL
:COALESCE(EXCLUDED.legacy, l.legacy)
Но няма начин да се разпознае DEFAULT
стойности и стойности, действително предоставени в INSERT
. Само полученото EXCLUDED
се виждат редове. Ако имате нужда от разликата, върнете се към предишното решение, където имате и двете на наше разположение.