sql >> Database >  >> RDS >> PostgreSQL

Genereer DEFAULT-waarden in een CTE UPSERT met behulp van PostgreSQL 9.3

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 of IMMUTABLE standaarduitdrukkingen . De meeste VOLATILE functies zullen net zo goed werken, maar er zijn geen garanties. De current_timestamp familie van functies kwalificeert als stabiel, omdat hun waarden niet veranderen binnen een transactie.
    Dit heeft met name bijwerkingen op serial kolommen (of andere standaardwaarden die uit een reeks zijn afgeleid). Maar dat zou geen probleem moeten zijn, want normaal schrijf je niet naar serial kolommen direct. Die moeten niet worden vermeld in INSERT verklaringen helemaal niet.
    Resterende fout voor serial 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 in serial kolommen.

Er kunnen nog twee problemen worden opgelost:

  • Als u kolommen heeft gedefinieerd NOT NULL , moet u dummy-waarden invoegen en vervangen door NULL 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 de UPDATE 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.




  1. Hoe schakel ik referentiële integriteit in Postgres 8.2 uit?

  2. Hoe de activiteit van één database in SQL Server te analyseren

  3. TNSPING OK maar sqlplus geeft ORA-12154?

  4. Wat is er nieuw in PostgreSQL 12