Postgres 9.5 implementerade UPSERT
. Se nedan.
Postgres 9.4 eller äldre
Det här är ett knepigt problem. Du stöter på denna begränsning (per dokumentation):
I en
VALUES
lista som visas på den översta nivån av enINSERT
, kan ett uttryck ersättas medDEFAULT
för att indikera att målkolumnens standardvärde ska infogas.DEFAULT
kan inte användas närVALUES
dyker upp i andra sammanhang.
Djärv betoning min. Standardvärden definieras inte utan en tabell att infoga i. Så det finns ingen direkt lösning på din fråga, men det finns ett antal möjliga alternativa vägar, beroende på exakta krav .
Hämta standardvärden från systemkatalogen?
Du kunde hämta dem från systemkatalogen pg_attrdef
som @Patrick kommenterade eller från information_schema.columns
. Kompletta instruktioner här:
- Hämta standardvärdena för tabellkolumner i Postgres?
Men då fortfarande har bara en lista med rader med en textrepresentation av uttrycket för att laga standardvärdet. Du skulle behöva bygga och utföra uttalanden dynamiskt för att få värden att arbeta med. Tråkigt och rörigt. Istället kan vi låta inbyggd Postgres-funktionalitet göra det åt oss :
Enkel genväg
Infoga en dummy-rad och få den tillbaka för att använda genererade standardvärden:
INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
Problem/lösningens omfattning
- Detta fungerar garanterat endast för
STABLE
ellerIMMUTABLE
standarduttryck . MestVOLATILE
funktioner kommer att fungera lika bra, men det finns inga garantier.current_timestamp
familj av funktioner kvalificeras som stabila, eftersom deras värden inte ändras inom en transaktion.
Särskilt har detta bieffekter påserial
kolumner (eller andra standardinställningar som ritar från en sekvens). Men det borde inte vara ett problem, eftersom du normalt inte skriver tillserial
kolumner direkt. De ska inte listas iINSERT
uttalanden överhuvudtaget.
Återstående fel förserial
kolumner:sekvensen flyttas fortfarande fram av det enda anropet för att få en standardrad, vilket skapar ett gap i numreringen. Återigen, det borde inte vara ett problem, eftersom luckor allmänt är att förvänta sig iserial
kolumner.
Ytterligare två problem kan lösas:
-
Om du har definierade kolumner
NOT NULL
, måste du infoga dummyvärden och ersätta medNULL
i resultatet. -
Vi vill faktiskt inte infoga attrappraden . Vi skulle kunna ta bort senare (i samma transaktion), men det kan ha fler biverkningar, som triggers
ON DELETE
. Det finns ett bättre sätt:
Undvik dummy row
Klona en tillfällig tabell inklusive kolumnstandarder och infoga i det :
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 *;
...
Samma resultat, färre biverkningar. Eftersom standarduttryck kopieras ordagrant, drar klonen från samma sekvenser om några. Men andra biverkningar från den oönskade raden eller triggers undviks helt.
Tack till Igor för idén:
- Postgresql, välj en "falsk" rad
Ta bort NOT NULL
begränsningar
Du måste ange dummyvärden för NOT NULL
kolumner, eftersom (per dokumentation):
Begränsningar som inte är null kopieras alltid till den nya tabellen.
Antingen passar de i INSERT
uttalande eller (bättre) eliminera begränsningarna:
ALTER TABLE tmp_playlist_items
ALTER COLUMN foo DROP NOT NULL
, ALTER COLUMN bar DROP NOT NULL;
Det finns ett snabbt och smutsigt sätt med superanvändarbehörigheter:
UPDATE pg_attribute
SET attnotnull = FALSE
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0;
Det är bara en tillfällig tabell utan data och inget annat syfte, och den tas bort i slutet av transaktionen. Så genvägen är frestande. Ändå är grundregeln:manipulera aldrig systemkataloger direkt.
Så låt oss titta på ett rent sätt :Automatisera med dynamisk SQL i en DO
påstående. Du behöver bara de vanliga behörigheterna du har garanterat haft sedan samma roll skapade temptabellen.
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$$
Mycket renare och fortfarande väldigt snabb. Var försiktig med dynamiska kommandon och var försiktig med SQL-injektion. Detta uttalande är säkert. Jag har lagt upp flera relaterade svar med mer förklaring.
Allmän lösning (9.4 och äldre)
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;
Du behöver bara LOCK
om du har samtidiga transaktioner som försöker skriva till samma tabell.
Som begärt ersätter detta endast NULL-värden i kolumnen legacy
i inmatningsraderna för INSERT
fall. Kan enkelt utökas till att fungera för andra kolumner eller i UPDATE
fall också. Du kan till exempel UPDATE
villkorligt också:endast om inmatningsvärdet är NOT NULL
. Jag lade till en kommenterad rad i UPDATE
ovan.
Bortsett från:Du behöver inte casta värden i valfri rad men de första i en VALUES
uttryck, eftersom typer härrör från den första rad.
Postgres 9.5
implementerar UPSERT med INSERT .. ON CONFLICT .. DO NOTHING | UPDATE
. Detta förenklar till stor del operationen:
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;
Vi kan bifoga VALUES
sats till INSERT
direkt, vilket tillåter DEFAULT
nyckelord. Vid unika överträdelser på (id)
, Postgres uppdaterar istället. Vi kan använda exkluderade rader i UPDATE
. Manualen:
SET
ochWHERE
satser iON CONFLICT DO UPDATE
ha tillgång till den befintliga raden med tabellens namn (eller ett alias) och till rader som är föreslagna för infogning med den speciellaexcluded
bord.
Och:
Observera att effekterna av alla per rad
BEFORE INSERT
triggers återspeglas i exkluderade värden, eftersom dessa effekter kan ha bidragit till att raden uteslutits från infogning.
Återstående hörnfodral
Du har olika alternativ för UPDATE
:Du kan ...
- ... inte uppdatera alls:lägg till en
WHERE
klausul tillUPDATE
att bara skriva till markerade rader. - ... uppdatera bara valda kolumner.
- ... uppdatera bara om kolumnen för närvarande är NULL:
COALESCE(l.legacy, EXCLUDED.legacy)
- ... uppdatera bara om det nya värdet är
NOT NULL
:COALESCE(EXCLUDED.legacy, l.legacy)
Men det finns inget sätt att urskilja DEFAULT
värden och värden som faktiskt tillhandahålls i INSERT
. Endast resulterande EXCLUDED
rader är synliga. Om du behöver distinktionen, fall tillbaka till den tidigare lösningen, där du har båda till vårt förfogande.