Postgres 9.5 implementoval UPSERT
. Viz níže.
Postgres 9.4 nebo starší
To je zapeklitý problém. Narážíte na toto omezení (podle dokumentace):
V
VALUES
seznam zobrazený na nejvyšší úrovniINSERT
, výraz může být nahrazen výrazemDEFAULT
označující, že by měla být vložena výchozí hodnota cílového sloupce.DEFAULT
nelze použít, kdyžVALUES
se objevuje v jiných kontextech.
Odvážný důraz můj. Výchozí hodnoty nejsou definovány bez tabulky pro vložení. Neexistuje tedy žádný přímý řešení vaší otázky, ale existuje řada možných alternativních cest v závislosti na přesných požadavcích .
Načíst výchozí hodnoty ze systémového katalogu?
Mohli byste načtěte je ze systémového katalogu pg_attrdef
jako komentář @Patrick nebo z information_schema.columns
. Kompletní pokyny zde:
- Získat výchozí hodnoty sloupců tabulky v Postgresu?
Ale pak pořád mít pouze seznam řádků s textovou reprezentací výrazu pro vaření výchozí hodnoty. Abyste získali hodnoty, se kterými byste mohli pracovat, museli byste vytvářet a provádět příkazy dynamicky. Nudné a chaotické. Místo toho můžeme nechat vestavěnou funkci Postgres, aby to udělala za nás :
Jednoduchá zkratka
Vložte fiktivní řádek a nechte jej vrátit, aby používal vygenerované výchozí hodnoty:
INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
Problémy / rozsah řešení
- Je zaručeno, že to bude fungovat pouze pro
STABLE
neboIMMUTABLE
výchozí výrazy . NejvíceVOLATILE
funkce budou fungovat stejně dobře, ale neexistují žádné záruky.current_timestamp
rodina funkcí se kvalifikuje jako stabilní, protože jejich hodnoty se v rámci transakce nemění.
To má zejména vedlejší účinky naserial
sloupce (nebo jakékoli jiné výchozí hodnoty kreslené ze sekvence). To by ale neměl být problém, protože doserial
běžně nezapisujete sloupce přímo. Ty by neměly být uvedeny vINSERT
vůbec.
Zbývající chyba proserial
sloupce:sekvence je stále posouvána jediným voláním, aby se získal výchozí řádek, což vytváří mezeru v číslování. Opět by to neměl být problém, protože mezery obecně lze očekávat vserial
sloupce.
Lze vyřešit další dva problémy:
-
Pokud máte definované sloupce
NOT NULL
, musíte vložit fiktivní hodnoty a nahradit jeNULL
ve výsledku. -
Ve skutečnosti nechceme vložit fiktivní řádek . Můžeme smazat později (v rámci stejné transakce), ale to může mít více vedlejších účinků, jako jsou spouštěče
ON DELETE
. Existuje lepší způsob:
Vyhněte se fiktivní řadě
Naklonujte dočasnou tabulku včetně výchozích hodnot sloupců a vložit do toho :
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 *;
...
Stejný výsledek, méně vedlejších účinků. Vzhledem k tomu, že výchozí výrazy jsou kopírovány doslovně, klon kreslí ze stejných sekvencí, pokud existují. Ale jiným vedlejším účinkům nechtěného řádku nebo spouštěčů se zcela zabrání.
Poděkování Igorovi za nápad:
- Postgresql, vyberte „falešný“ řádek
Odstraňte NOT NULL
omezení
Budete muset zadat fiktivní hodnoty pro NOT NULL
sloupců, protože (podle dokumentace):
Nenulová omezení se vždy zkopírují do nové tabulky.
Buď se přizpůsobí těm v INSERT
nebo (lépe) odstranit omezení:
ALTER TABLE tmp_playlist_items
ALTER COLUMN foo DROP NOT NULL
, ALTER COLUMN bar DROP NOT NULL;
Existuje rychlá a špinavá cesta s oprávněními superuživatele:
UPDATE pg_attribute
SET attnotnull = FALSE
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0;
Je to jen dočasná tabulka bez dat a bez dalšího účelu a na konci transakce je vypuštěna. Takže zkratka je lákavá. Přesto základní pravidlo zní:nikdy nemanipulujte přímo se systémovými katalogy.
Pojďme se tedy podívat na čistý způsob :Automatizujte pomocí dynamického SQL v DO
prohlášení. Potřebujete pouze běžná oprávnění zaručeně budete mít, protože stejná role vytvořila dočasnou tabulku.
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$$
Mnohem čistší a stále velmi rychlý. Buďte opatrní s dynamickými příkazy a dávejte si pozor na SQL injection. Toto prohlášení je bezpečné. Poslal jsem několik souvisejících odpovědí s podrobnějším vysvětlením.
Obecné řešení (9.4 a starší)
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;
Potřebujete pouze LOCK
pokud máte souběžné transakce, které se snaží zapisovat do stejné tabulky.
Jak bylo požadováno, nahradí pouze hodnoty NULL ve sloupci legacy
ve vstupních řádcích pro INSERT
případ. Může být snadno rozšířen tak, aby fungoval pro další sloupce nebo v UPDATE
případ také. Můžete například UPDATE
podmíněně také:pouze pokud je vstupní hodnota NOT NULL
. Přidal jsem do UPDATE
řádek s komentářem výše.
Stranou:Nemusíte obsazovat hodnoty v libovolném řádku kromě prvního v VALUES
výraz, protože typy jsou odvozeny od prvního řádek.
Postgres 9.5
implementuje UPSERT s INSERT .. ON CONFLICT .. DO NOTHING | UPDATE
. To značně zjednodušuje operaci:
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;
Můžeme připojit VALUES
klauzule na INSERT
přímo, což umožňuje DEFAULT
klíčové slovo. V případě jedinečných porušení na (id)
, místo toho se aktualizuje Postgres. Můžeme použít vyloučené řádky v UPDATE
. Manuál:
SET
aWHERE
klauzule vON CONFLICT DO UPDATE
mít přístup k existujícímu řádku pomocí názvu tabulky (nebo aliasu) a k řádkům navrženým pro vložení pomocí speciálníhoexcluded
tabulka.
A:
Všimněte si, že účinky všech na řádek
BEFORE INSERT
spouštěče se odrážejí ve vyloučených hodnotách, protože tyto efekty mohly přispět k vyloučení řádku z vložení.
Zbývající rohové pouzdro
Pro UPDATE
máte různé možnosti :Můžete ...
- ... neaktualizovat vůbec:přidejte
WHERE
klauzule doUPDATE
zapisovat pouze do vybraných řádků. - ... aktualizujte pouze vybrané sloupce.
- ... aktualizujte, pouze pokud je sloupec aktuálně NULL:
COALESCE(l.legacy, EXCLUDED.legacy)
- ... aktualizujte pouze v případě, že nová hodnota je
NOT NULL
:COALESCE(EXCLUDED.legacy, l.legacy)
Ale neexistuje způsob, jak rozeznat DEFAULT
hodnoty a hodnoty skutečně poskytnuté v INSERT
. Pouze výsledné EXCLUDED
řádky jsou viditelné. Pokud potřebujete rozlišení, vraťte se k předchozímu řešení, kde máte k dispozici obojí.