Postgres 9.5 zaimplementowany UPSERT
. Zobacz poniżej.
Postgres 9.4 lub starszy
To trudny problem. Natrafiasz na to ograniczenie (zgodnie z dokumentacją):
W VALUES
lista pojawiająca się na najwyższym poziomie INSERT
, wyrażenie można zastąpić przez DEFAULT
aby wskazać, że należy wstawić domyślną wartość kolumny docelowej. DEFAULT
nie można użyć, gdyVALUES
pojawia się w innych kontekstach.
Moje odważne podkreślenie. Wartości domyślne nie są definiowane bez tabeli do wstawienia. Więc nie ma bezpośredniego rozwiązanie Twojego pytania, ale istnieje wiele możliwych tras alternatywnych, w zależności od dokładnych wymagań .
Pobrać wartości domyślne z katalogu systemowego?
możesz pobierz je z katalogu systemowego pg_attrdef
jak skomentował @Patrick lub z information_schema.columns
. Pełne instrukcje tutaj:
- Pobrać domyślne wartości kolumn tabeli w Postgresie?
Ale wtedy nadal mieć tylko listę wierszy z tekstową reprezentacją wyrażenia gotującego wartość domyślną. Trzeba by dynamicznie budować i wykonywać instrukcje, aby uzyskać wartości do pracy. Żmudne i niechlujne. Zamiast tego możemy pozwolić wbudowanej funkcjonalności Postgres zrobić to za nas :
Prosty skrót
Wstaw fikcyjny wiersz i zwróć go, aby używał wygenerowanych wartości domyślnych:
INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
Problemy / zakres rozwiązania
- Gwarantuje się, że zadziała tylko w przypadku
STABLE
lubIMMUTABLE
wyrażenia domyślne . WiększośćVOLATILE
funkcje będą działać równie dobrze, ale nie ma żadnych gwarancji.current_timestamp
rodzina funkcji kwalifikuje się jako stabilna, ponieważ ich wartości nie zmieniają się w ramach transakcji.
W szczególności ma to skutki uboczne w przypadkuserial
kolumny (lub dowolne inne wartości domyślne na podstawie sekwencji). Ale to nie powinno stanowić problemu, ponieważ zwykle nie piszesz doserial
kolumny bezpośrednio. Nie powinny być wymienione wINSERT
w ogóle.
Pozostała luka dlaserial
kolumny:sekwencja jest nadal przesuwana przez pojedyncze wywołanie, aby uzyskać domyślny wiersz, tworząc lukę w numeracji. Ponownie, nie powinno to stanowić problemu, ponieważ luki na ogół należy się spodziewać wserial
kolumny.
Można rozwiązać jeszcze dwa problemy:
-
Jeśli masz zdefiniowane kolumny
NOT NULL
, musisz wstawić fałszywe wartości i zastąpić jeNULL
w wyniku. -
W rzeczywistości nie chcemy wstawiać fałszywego wiersza . Moglibyśmy usunąć później (w tej samej transakcji), ale może to mieć więcej skutków ubocznych, takich jak wyzwalacze
ON DELETE
. Jest lepszy sposób:
Unikaj fałszywego rzędu
Sklonuj tablicę tymczasową w tym wartości domyślne kolumn i wstawiaj do tego :
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 *;
...
Ten sam wynik, mniej skutków ubocznych. Ponieważ wyrażenia domyślne są kopiowane dosłownie, klon rysuje z tych samych sekwencji, jeśli takie istnieją. Ale inne efekty uboczne z niechcianego rzędu lub wyzwalaczy są całkowicie unikane.
Podziękowania dla Igora za pomysł:
- Postgresql, wybierz „fałszywy” wiersz
Usuń NOT NULL
ograniczenia
Musiałbyś podać fałszywe wartości dla NOT NULL
kolumny, ponieważ (zgodnie z dokumentacją):
Ograniczenia typu not-null są zawsze kopiowane do nowej tabeli.
Albo pomieścić dla tych w INSERT
oświadczenie lub (lepiej) wyeliminuj ograniczenia:
ALTER TABLE tmp_playlist_items
ALTER COLUMN foo DROP NOT NULL
, ALTER COLUMN bar DROP NOT NULL;
Jest szybki i brudny sposób z uprawnieniami superużytkownika:
UPDATE pg_attribute
SET attnotnull = FALSE
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0;
Jest to po prostu tymczasowa tabela bez danych i bez innego celu, która jest usuwana na koniec transakcji. Więc skrót jest kuszący. Jednak podstawowa zasada brzmi:nigdy nie manipuluj bezpośrednio w katalogach systemowych.
Spójrzmy więc na czysty sposób :Automatyzacja za pomocą dynamicznego SQL w DO
oświadczenie. Potrzebujesz tylko regularnych uprawnień masz gwarancję, że masz, ponieważ ta sama rola utworzyła tabelę tymczasową.
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$$
Dużo czyściej i nadal bardzo szybko. Zachowaj ostrożność za pomocą poleceń dynamicznych i uważaj na wstrzykiwanie SQL. To stwierdzenie jest bezpieczne. Wysłałem kilka powiązanych odpowiedzi z dodatkowymi wyjaśnieniami.
Rozwiązanie ogólne (9.4 i starsze)
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;
Potrzebujesz tylko LOCK
jeśli masz jednoczesne transakcje, które próbują zapisywać do tej samej tabeli.
Zgodnie z żądaniem zastępuje to tylko wartości NULL w kolumnie legacy
w wierszach wejściowych dla INSERT
walizka. Można łatwo rozszerzyć do pracy z innymi kolumnami lub w UPDATE
przypadku również. Na przykład możesz UPDATE
również warunkowo:tylko jeśli wartość wejściowa to NOT NULL
. Dodałem skomentowaną linię do UPDATE
powyżej.
Na bok:nie musisz przesyłać wartości w dowolnym wierszu oprócz pierwszego w VALUES
wyrażenie, ponieważ typy pochodzą od pierwszego wiersz.
Postgres 9.5
implementuje UPSERT z INSERT .. ON CONFLICT .. DO NOTHING | UPDATE
. To znacznie upraszcza operację:
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;
Możemy dołączyć VALUES
klauzula INSERT
bezpośrednio, co pozwala na DEFAULT
słowo kluczowe. W przypadku unikalnych naruszeń w (id)
, zamiast tego aktualizacje Postgres. Możemy użyć wykluczonych wierszy w UPDATE
. Instrukcja:
SET
i WHERE
klauzule w ON CONFLICT DO UPDATE
mieć dostęp do istniejącego wiersza za pomocą nazwy tabeli (lub aliasu) oraz do wierszy proponowanych do wstawienia za pomocą specjalnego excluded
tabela.
Oraz:
Zwróć uwagę, że efekty wszystkich na wiersz BEFORE INSERT
wyzwalacze są odzwierciedlane w wartościach wykluczonych, ponieważ te efekty mogły przyczynić się do wykluczenia wiersza z wstawiania.
Pozostały przypadek narożny
Masz różne opcje UPDATE
:Możesz...
- ... w ogóle nie aktualizuj:dodaj
WHERE
klauzula doUPDATE
pisać tylko do wybranych wierszy. - ... aktualizuj tylko wybrane kolumny.
- ... aktualizuj tylko, jeśli kolumna ma obecnie wartość NULL:
COALESCE(l.legacy, EXCLUDED.legacy)
- ... aktualizuj tylko, jeśli nowa wartość to
NOT NULL
:COALESCE(EXCLUDED.legacy, l.legacy)
Ale nie ma sposobu, aby rozpoznać DEFAULT
wartości i wartości faktycznie podane w INSERT
. Tylko wynikowe EXCLUDED
widoczne są wiersze. Jeśli potrzebujesz rozróżnienia, wróć do poprzedniego rozwiązania, w którym masz oba do naszej dyspozycji.