sql >> Databasteknik >  >> RDS >> PostgreSQL

Generera DEFAULT-värden i en CTE UPSERT med PostgreSQL 9.3

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 en INSERT , kan ett uttryck ersättas med DEFAULT för att indikera att målkolumnens standardvärde ska infogas. DEFAULT kan inte användas när VALUES 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 eller IMMUTABLE standarduttryck . Mest VOLATILE 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 till serial kolumner direkt. De ska inte listas i INSERT uttalanden överhuvudtaget.
    Återstående fel för serial 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 i serial kolumner.

Ytterligare två problem kan lösas:

  • Om du har definierade kolumner NOT NULL , måste du infoga dummyvärden och ersätta med NULL 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 och WHERE satser i ON 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 speciella excluded 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 till UPDATE 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.




  1. Prestandapåverkan av olika felhanteringstekniker

  2. Hämta en bild lagrad som BLOB på en MYSQL DB

  3. Så här fixar du:JSON_VALUE Returnerar NULL med långa strängar (SQL-server)

  4. Kan jag köra flera frågor separerade med semikolon med MySQL Connector/J?