Postgres 9.5 geïmplementeerd UPSERT
. Zie hieronder.
Postgres 9.4 of ouder
Dit is een lastig probleem. U loopt tegen deze beperking aan (volgens documentatie):
In een VALUES
lijst die verschijnt op het hoogste niveau van een INSERT
, een uitdrukking kan worden vervangen door DEFAULT
om aan te geven dat de standaardwaarde van de bestemmingskolom moet worden ingevoegd. DEFAULT
kan niet worden gebruikt wanneerVALUES
verschijnt in andere contexten.
Vetgedrukte nadruk van mij. Standaardwaarden worden niet gedefinieerd zonder een tabel om in te voegen. Er is dus geen directe oplossing voor uw vraag, maar er zijn een aantal mogelijke alternatieve routes, afhankelijk van de exacte vereisten .
Standaardwaarden ophalen uit de systeemcatalogus?
Je zou haal die op uit de systeemcatalogus pg_attrdef
zoals @Patrick heeft gereageerd of van information_schema.columns
. Volledige instructies hier:
- De standaardwaarden van tabelkolommen in Postgres ophalen?
Maar dan ben je nog alleen een lijst met rijen hebben met een tekstrepresentatie van de uitdrukking om de standaardwaarde te koken. U zou instructies dynamisch moeten bouwen en uitvoeren om waarden te krijgen om mee te werken. Omslachtig en rommelig. In plaats daarvan kunnen we ingebouwde Postgres-functionaliteit dat voor ons laten doen :
Eenvoudige snelkoppeling
Voeg een dummy-rij in en laat deze terugkeren om gegenereerde standaardwaarden te gebruiken:
INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
Problemen / reikwijdte van de oplossing
- Dit werkt alleen gegarandeerd voor
STABLE
ofIMMUTABLE
standaarduitdrukkingen . De meesteVOLATILE
functies zullen net zo goed werken, maar er zijn geen garanties. Decurrent_timestamp
familie van functies kwalificeert als stabiel, omdat hun waarden niet veranderen binnen een transactie.
Dit heeft met name bijwerkingen opserial
kolommen (of andere standaardwaarden die uit een reeks zijn afgeleid). Maar dat zou geen probleem moeten zijn, want normaal schrijf je niet naarserial
kolommen direct. Die moeten niet worden vermeld inINSERT
verklaringen helemaal niet.
Resterende fout voorserial
kolommen:de reeks wordt nog steeds vooruitgeschoven door de enkele aanroep om een standaardrij te krijgen, waardoor er een gat in de nummering ontstaat. Nogmaals, dat zou geen probleem moeten zijn, omdat hiaten over het algemeen te verwachten zijn inserial
kolommen.
Er kunnen nog twee problemen worden opgelost:
-
Als u kolommen heeft gedefinieerd
NOT NULL
, moet u dummy-waarden invoegen en vervangen doorNULL
in het resultaat. -
We willen de dummy-rij eigenlijk niet invoegen . We zouden later kunnen verwijderen (in dezelfde transactie), maar dat kan meer bijwerkingen hebben, zoals triggers
ON DELETE
. Er is een betere manier:
Vermijd dummyrij
Kloon een tijdelijke tabel inclusief kolomstandaarden en invoegen in dat :
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 *;
...
Zelfde resultaat, minder bijwerkingen. Aangezien standaarduitdrukkingen woordelijk worden gekopieerd, put de kloon uit dezelfde reeksen als die er zijn. Maar andere bijwerkingen van de ongewenste rij of triggers worden volledig vermeden.
Met dank aan Igor voor het idee:
- Postgresql, selecteer een "nep"-rij
Verwijder NOT NULL
beperkingen
U zou dummy-waarden moeten opgeven voor NOT NULL
kolommen, omdat (volgens documentatie):
Niet-null-beperkingen worden altijd naar de nieuwe tabel gekopieerd.
Ofwel geschikt voor degenen in de INSERT
verklaring of (beter) elimineer de beperkingen:
ALTER TABLE tmp_playlist_items
ALTER COLUMN foo DROP NOT NULL
, ALTER COLUMN bar DROP NOT NULL;
Er is een snelle en vuile manier met superuser-privileges:
UPDATE pg_attribute
SET attnotnull = FALSE
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0;
Het is slechts een tijdelijke tabel zonder gegevens en zonder ander doel, en wordt aan het einde van de transactie verwijderd. Dus de snelkoppeling is verleidelijk. Toch is de basisregel:knoei nooit rechtstreeks met systeemcatalogi.
Dus laten we eens kijken naar een schone manier :Automatiseer met dynamische SQL in een DO
uitspraak. Je hebt alleen de gewone privileges nodig u bent gegarandeerd in het bezit sinds dezelfde rol de tijdelijke tabel heeft gemaakt.
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$$
Veel schoner en nog steeds erg snel. Wees voorzichtig met dynamische opdrachten en wees op uw hoede voor SQL-injectie. Deze verklaring is veilig. Ik heb verschillende gerelateerde antwoorden gepost met meer uitleg.
Algemene oplossing (9.4 en ouder)
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;
Je hebt alleen de LOCK
. nodig als u gelijktijdige transacties heeft die naar dezelfde tabel proberen te schrijven.
Zoals gevraagd vervangt dit alleen NULL-waarden in de kolom legacy
in de invoerrijen voor de INSERT
geval. Kan eenvoudig worden uitgebreid om te werken voor andere kolommen of in de UPDATE
geval ook. U kunt bijvoorbeeld UPDATE
ook voorwaardelijk:alleen als de invoerwaarde NOT NULL
. is . Ik heb een regel met commentaar toegevoegd aan de UPDATE
hierboven.
Terzijde:je hoeft niet te casten waarden in elke rij behalve de eerste in een VALUES
expressie, aangezien typen zijn afgeleid van de eerste rij.
Postgres 9.5
implementeert UPSERT met INSERT .. ON CONFLICT .. DO NOTHING | UPDATE
. Dit vereenvoudigt de bediening grotendeels:
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;
We kunnen de VALUES
. bijvoegen clausule naar INSERT
direct, waardoor de DEFAULT
trefwoord. In het geval van unieke overtredingen op (id)
, Postgres-updates in plaats daarvan. We kunnen uitgesloten rijen gebruiken in de UPDATE
. De handleiding:
De SET
en WHERE
clausules in ON CONFLICT DO UPDATE
toegang hebben tot de bestaande rij met de naam van de tabel (of een alias), en tot rijen die worden voorgesteld om in te voegen met de speciale excluded
tafel.
En:
Merk op dat de effecten van alle BEFORE INSERT
triggers worden weerspiegeld in uitgesloten waarden, aangezien deze effecten ertoe kunnen hebben bijgedragen dat de rij is uitgesloten van invoeging.
Overige hoekkast
Je hebt verschillende opties voor de UPDATE
:U kunt ...
- ... helemaal niet bijwerken:voeg een
WHERE
toe clausule aan deUPDATE
om alleen naar geselecteerde rijen te schrijven. - ... alleen geselecteerde kolommen bijwerken.
- ... alleen bijwerken als de kolom momenteel NULL is:
COALESCE(l.legacy, EXCLUDED.legacy)
- ... alleen bijwerken als de nieuwe waarde
NOT NULL
is :COALESCE(EXCLUDED.legacy, l.legacy)
Maar er is geen manier om DEFAULT
te onderscheiden waarden en waarden die daadwerkelijk zijn opgegeven in de INSERT
. Alleen resulterende EXCLUDED
rijen zijn zichtbaar. Als je het onderscheid nodig hebt, val dan terug naar de vorige oplossing, waar je beide tot onze beschikking hebt.