sql >> Database teknologi >  >> RDS >> PostgreSQL

Generer DEFAULT-værdier i en CTE UPSERT ved hjælp af PostgreSQL 9.3

Postgres 9.5 implementeret UPSERT . Se nedenfor.

Postgres 9.4 eller ældre

Dette er et vanskeligt problem. Du løber ind i denne begrænsning (pr. dokumentation):

I en VALUES liste, der vises på det øverste niveau af en INSERT , kan et udtryk erstattes af DEFAULT for at angive, at destinationskolonnens standardværdi skal indsættes. DEFAULT kan ikke bruges, nårVALUES optræder i andre sammenhænge.

Fed understregning min. Standardværdier er ikke defineret uden en tabel at indsætte i. Så der er ingen direkte løsning på dit spørgsmål, men der er en række mulige alternative ruter, afhængigt af præcise krav .

Hent standardværdier fra systemkataloget?

Du kunne hent dem fra systemkataloget pg_attrdef som @Patrick kommenterede eller fra information_schema.columns . Fuldstændig instruktioner her:

  • Få standardværdierne for tabelkolonner i Postgres?

Men så stadig har kun en liste over rækker med en tekstrepræsentation af udtrykket for at tilberede standardværdien. Du ville være nødt til at bygge og udføre sætninger dynamisk for at få værdier at arbejde med. Kedeligt og rodet. I stedet kan vi lade indbygget Postgres-funktionalitet gøre det for os :

Simpel genvej

Indsæt en dummy-række og få den returneret til at bruge genererede standardindstillinger:

INSERT INTO playlist_items DEFAULT VALUES RETURNING *;

Problemer/løsningens omfang

  • Dette er kun garanteret at virke for STABLE eller IMMUTABLE standardudtryk . Mest VOLATILE funktioner vil fungere lige så godt, men der er ingen garantier. current_timestamp familie af funktioner kvalificeres som stabile, da deres værdier ikke ændres inden for en transaktion.
    Dette har især bivirkninger på serial kolonner (eller andre standardindstillinger, der tegner fra en sekvens). Men det burde ikke være et problem, for du skriver normalt ikke til serial kolonner direkte. Disse bør ikke være opført i INSERT udsagn overhovedet.
    Resterende fejl for serial kolonner:sekvensen er stadig avanceret af det enkelte kald for at få en standardrække, hvilket giver et hul i nummereringen. Igen, det burde ikke være et problem, for huller kan generelt forventes i serial kolonner.

Yderligere to problemer kan løses:

  • Hvis du har defineret kolonner NOT NULL , skal du indsætte dummy-værdier og erstatte med NULL i resultatet.

  • Vi ønsker faktisk ikke at indsætte dummy-rækken . Vi kunne slette senere (i samme transaktion), men det kan have flere bivirkninger, såsom triggere ON DELETE . Der er en bedre måde:

Undgå dummy row

Klon en midlertidig tabel inklusive kolonnestandarder og indsæt 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 *;
...

Samme resultat, færre bivirkninger. Da standardudtryk kopieres ordret, trækker klonen fra de samme sekvenser, hvis nogen. Men andre bivirkninger fra den uønskede række eller triggere undgås fuldstændigt.

Tak til Igor for ideen:

  • Postgresql, vælg en "falsk" række

Fjern NOT NULL begrænsninger

Du skal angive dummy-værdier for NOT NULL kolonner, fordi (pr. dokumentation):

Ikke-nul-begrænsninger kopieres altid til den nye tabel.

Enten rumme dem i INSERT sætning eller (bedre) eliminere begrænsningerne:

ALTER TABLE tmp_playlist_items
   ALTER COLUMN foo DROP NOT NULL
 , ALTER COLUMN bar DROP NOT NULL;

Der er en hurtig og beskidt måde med superbrugerrettigheder:

UPDATE pg_attribute
SET    attnotnull = FALSE
WHERE  attrelid = 'tmp_playlist_items'::regclass
AND    attnotnull
AND    attnum > 0;

Det er blot en midlertidig tabel uden data og intet andet formål, og den er droppet ved slutningen af ​​transaktionen. Så genvejen er fristende. Alligevel er den grundlæggende regel:manipuler aldrig med systemkataloger direkte.

Så lad os se på en ren måde :Automatiser med dynamisk SQL i en DO udmelding. Du skal bare bruge de almindelige privilegier du er garanteret at have siden den samme rolle skabte vikartabellen.

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$$

Meget renere og stadig meget hurtig. Udfør forsigtighed med dynamiske kommandoer og vær på vagt over for SQL-injektion. Denne erklæring er sikker. Jeg har postet flere relaterede svar med mere forklaring.

Generel løsning (9.4 og æ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 kun LOCK hvis du har samtidige transaktioner, der prøver at skrive til den samme tabel.

Som anmodet erstatter dette kun NULL-værdier i kolonnen legacy i inputrækkerne for INSERT sag. Kan nemt udvides til at fungere for andre kolonner eller i UPDATE sagen også. For eksempel kan du UPDATE også betinget:kun hvis inputværdien er NOT NULL . Jeg tilføjede en kommenteret linje til UPDATE ovenfor.

Til side:Du behøver ikke at caste værdier i enhver række, men den første i en VALUES udtryk, da typer er afledt af den første række.

Postgres 9.5

implementerer UPSERT med INSERT .. ON CONFLICT .. DO NOTHING | UPDATE . Dette forenkler i høj grad betjeningen:

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 vedhæfte VALUES klausul til INSERT direkte, hvilket tillader DEFAULT søgeord. I tilfælde af unikke overtrædelser på (id) , Postgres opdaterer i stedet. Vi kan bruge ekskluderede rækker i UPDATE . Manualen:

SET og WHERE klausuler i ON CONFLICT DO UPDATE har adgang til den eksisterende række ved hjælp af tabellens navn (eller et alias), og til rækker, der er foreslået til indsættelse ved hjælp af den særlige excluded tabel.

Og:

Bemærk, at effekterne af alle pr. række BEFORE INSERT triggere afspejles i ekskluderede værdier, da disse effekter kan have bidraget til, at rækken er blevet udelukket fra indsættelse.

Resterende hjørnekasse

Du har forskellige muligheder for UPDATE :Du kan ...

  • ... slet ikke opdatere:tilføj en WHERE klausul til UPDATE kun at skrive til udvalgte rækker.
  • ... opdater kun valgte kolonner.
  • ... opdater kun, hvis kolonnen i øjeblikket er NULL:COALESCE(l.legacy, EXCLUDED.legacy)
  • ... opdater kun, hvis den nye værdi er NOT NULL :COALESCE(EXCLUDED.legacy, l.legacy)

Men der er ingen måde at skelne DEFAULT værdier og værdier, der faktisk er angivet i INSERT . Kun resulterende EXCLUDED rækker er synlige. Hvis du har brug for udmærkelsen, så fald tilbage til den tidligere løsning, hvor du har begge dele til vores rådighed.




  1. Sådan rettes 'Systemressource overskredet' ved migrering til Windows 10

  2. Ukendt kolonne i 'feltliste'-fejl på MySQL Update-forespørgsel

  3. PostgreSQL deaktiver mere output

  4. Returner rækker i den nøjagtige rækkefølge, de blev indsat