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

Hvordan opdaterer man alle kolonner med INSERT ... ON CONFLICT ...?

UPDATE syntaks kræver for eksplicit at navngive målkolonner. Mulige grunde til at undgå det:

  • Du har mange kolonner og vil bare forkorte syntaksen.
  • Du ved ikke kolonnenavne undtagen de unikke kolonne(r).

"All columns" skal betyde "alle kolonner i måltabellen" (eller i det mindste "førende kolonner i tabellen" ) i matchende rækkefølge og matchende datatype. Ellers skal du alligevel angive en liste over målkolonnenavne.

Testtabel:

CREATE TABLE tbl (
   id    int PRIMARY KEY
 , text  text
 , extra text
);

INSERT INTO tbl AS t
VALUES (1, 'foo')
     , (2, 'bar');

1. DELETE &INSERT i enkelt forespørgsel i stedet

Uden at kende nogen kolonnenavne undtagen id .

Virker kun for "alle kolonner i måltabellen" . Mens syntaksen endda fungerer for et førende undersæt, vil overskydende kolonner i måltabellen blive nulstillet til NULL med DELETE og INSERT .

UPSERT (INSERT ... ON CONFLICT ... ) er nødvendig for at undgå samtidigheds-/låseproblemer under samtidig skrivebelastning, og kun fordi der ikke er nogen generel måde at låse ikke-endnu-eksisterende rækker i Postgres (værdilåsning ).

Dit særlige krav påvirker kun UPDATE en del. Mulige komplikationer gælder ikke, hvor eksisterende rækker påvirkes. De er låst ordentligt. For at forenkle noget mere kan du reducere din sag til DELETE og INSERT :

WITH data(id) AS (              -- Only 1st column gets explicit name!
   VALUES
      (1, 'foo_upd', 'a')       -- changed
    , (2, 'bar', 'b')           -- unchanged
    , (3, 'baz', 'c')           -- new
   )
, del AS (
   DELETE FROM tbl AS t
   USING  data d
   WHERE  t.id = d.id
   -- AND    t <> d              -- optional, to avoid empty updates
   )                             -- only works for complete rows
INSERT INTO tbl AS t
TABLE  data                      -- short for: SELECT * FROM data
ON     CONFLICT (id) DO NOTHING
RETURNING t.id;

I Postgres MVCC-modellen er en UPDATE er stort set det samme som DELETE og INSERT alligevel (bortset fra nogle hjørnesager med samtidighed, HOT opdateringer og store kolonneværdier gemt ude af linjen). Da du alligevel vil erstatte alle rækker, skal du bare fjerne modstridende rækker før INSERT . Slettede rækker forbliver låst, indtil transaktionen er begået. INSERT finder muligvis kun modstridende rækker for tidligere ikke-eksisterende nøgleværdier, hvis en samtidig transaktion tilfældigvis indsætter dem samtidigt (efter DELETE , men før INSERT ).

Du vil miste yderligere kolonneværdier for berørte rækker i dette specielle tilfælde. Ingen undtagelse rejst. Men hvis konkurrerende forespørgsler har samme prioritet, er det næppe et problem:den anden forespørgsel vandt for nogle rækker. Desuden, hvis den anden forespørgsel er en lignende UPSERT, er dens alternativ at vente på, at denne transaktion forpligtes og derefter opdateres med det samme. "At vinde" kunne være en pyrrhussejr.

Om "tomme opdateringer":

  • Hvordan (eller kan jeg) VÆLGE DISTINCT på flere kolonner?

Nej, min forespørgsel skal vinde!

OK, du bad om det:

WITH data(id) AS (                   -- Only 1st column gets explicit name!
   VALUES                            -- rest gets default names "column2", etc.
     (1, 'foo_upd', NULL)              -- changed
   , (2, 'bar', NULL)                  -- unchanged
   , (3, 'baz', NULL)                  -- new
   , (4, 'baz', NULL)                  -- new
   )
, ups AS (
   INSERT INTO tbl AS t
   TABLE  data                       -- short for: SELECT * FROM data
   ON     CONFLICT (id) DO UPDATE
   SET    id = t.id
   WHERE  false                      -- never executed, but locks the row!
   RETURNING t.id
   )
, del AS (
   DELETE FROM tbl AS t
   USING  data     d
   LEFT   JOIN ups u USING (id)
   WHERE  u.id IS NULL               -- not inserted !
   AND    t.id = d.id
   -- AND    t <> d                  -- avoid empty updates - only for full rows
   RETURNING t.id
   )
, ins AS (
   INSERT INTO tbl AS t
   SELECT *
   FROM   data
   JOIN   del USING (id)             -- conflict impossible!
   RETURNING id
   )
SELECT ARRAY(TABLE ups) AS inserted  -- with UPSERT
     , ARRAY(TABLE ins) AS updated   -- with DELETE & INSERT;

Hvordan?

  • De 1. CTE data leverer blot data. Kunne være et bord i stedet for.
  • Den 2. CTE ups :UPSERT. Rækker med modstridende id er ikke ændret, men også låst .
  • Den 3. CTE del sletter modstridende rækker. De forbliver låst.
  • Den 4. CTE ins indsætter hele rækker . Kun tilladt for den samme transaktion
  • Det endelige SELECT er kun til demoen for at vise, hvad der skete.

For at tjekke for tomme opdateringer test (før og efter) med:

SELECT ctid, * FROM tbl; -- did the ctid change?

Den (kommenterede) check for eventuelle ændringer i rækken AND t <> d fungerer selv med NULL-værdier, fordi vi sammenligner to indtastede rækkeværdier i henhold til manualen:

to NULL-feltværdier betragtes som ens, og en NULL betragtes som større end en ikke-NULL

2. Dynamisk SQL

Dette fungerer også for en undergruppe af ledende kolonner, og bevarer eksisterende værdier.

Tricket er at lade Postgres bygge forespørgselsstrengen med kolonnenavne fra systemkatalogerne dynamisk og derefter udføre den.

Se relaterede svar for kode:

  • Opdater flere kolonner i en triggerfunktion i plpgsql

  • Masseopdatering af alle kolonner

  • SQL-opdatering af felter i en tabel fra felter i en anden



  1. Sådan får du det aktuelle forbindelsesobjekt i Spring JDBC

  2. Forældede funktioner til at tage ud af din værktøjskasse – Del 3

  3. Sådan får du MySQL til at håndtere UTF-8 korrekt

  4. Kan ikke oprette forbindelse til MySQL 4.1+ ved hjælp af gammel godkendelse