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

Come aggiornare tutte le colonne con INSERT... ON CONFLICT...?

Il UPDATE sintassi richiede per nominare esplicitamente le colonne di destinazione. Possibili ragioni per evitarlo:

  • Hai molte colonne e vuoi solo abbreviare la sintassi.
  • Non sai nomi di colonna ad eccezione delle colonne univoche.

"All columns" deve significare "tutte le colonne della tabella di destinazione" (o almeno "colonne principali della tabella" ) in ordine e tipo di dati corrispondenti. Altrimenti dovresti comunque fornire un elenco di nomi di colonne di destinazione.

Tabella di prova:

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

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

1. DELETE &INSERT invece in una singola query

Senza conoscere i nomi delle colonne tranne id .

Funziona solo per "tutte le colonne della tabella di destinazione" . Sebbene la sintassi funzioni anche per un sottoinsieme iniziale, le colonne in eccesso nella tabella di destinazione verrebbero reimpostate su NULL con DELETE e INSERT .

UPSERT (INSERT ... ON CONFLICT ... ) è necessario per evitare problemi di simultaneità/blocco sotto carico di scrittura simultaneo e solo perché non esiste un modo generale per bloccare le righe non ancora esistenti in Postgres (blocco del valore ).

Il tuo requisito speciale riguarda solo il UPDATE parte. Eventuali complicazioni non si applicano laddove esistente le righe sono interessate. Quelli sono bloccati correttamente. Semplificando ulteriormente, puoi ridurre il tuo caso a DELETE e 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;

Nel modello Postgres MVCC, un UPDATE è sostanzialmente lo stesso di DELETE e INSERT comunque (ad eccezione di alcuni casi d'angolo con concorrenza, aggiornamenti HOT e valori di colonne di grandi dimensioni archiviati fuori linea). Dal momento che vuoi comunque sostituire tutte le righe, rimuovi semplicemente le righe in conflitto prima di INSERT . Le righe eliminate rimangono bloccate fino al commit della transazione. Il INSERT potrebbero trovare righe in conflitto per valori chiave precedentemente non esistenti solo se una transazione simultanea li inserisce contemporaneamente (dopo il DELETE , ma prima di INSERT ).

In questo caso speciale perderesti valori di colonna aggiuntivi per le righe interessate. Nessuna eccezione sollevata. Ma se le query concorrenti hanno la stessa priorità, non è certo un problema:l'altra query ha vinto per alcuni righe. Inoltre, se l'altra query è un UPSERT simile, la sua alternativa consiste nell'attendere che questa transazione venga confermata e quindi si aggiorni immediatamente. "Vincere" potrebbe essere una vittoria di Pirro.

A proposito di "aggiornamenti vuoti":

  • Come faccio (o posso) SELEZIONARE DISTINCT su più colonne?

No, la mia domanda deve vincere!

OK, l'hai chiesto tu:

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;

Come?

  • Il 1° CTE data fornisce solo dati. Potrebbe essere invece un tavolo.
  • Il 2° CTE ups :UPSERT. Righe con id in conflitto non vengono modificati, ma anche bloccati .
  • Il 3° CTE del elimina le righe in conflitto. Rimangono bloccati.
  • Il 4° CTE ins inserisce righe intere . Consentito solo per la stessa transazione
  • L'ultimo SELECT serve solo alla demo per mostrare cosa è successo.

Per verificare la presenza di aggiornamenti vuoti, verifica (prima e dopo) con:

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

Il controllo (commentato) per eventuali modifiche nella riga AND t <> d funziona anche con valori NULL perché stiamo confrontando due valori di riga digitati secondo il manuale:

due valori di campo NULL sono considerati uguali e un NULL è considerato maggiore di un non NULL

2. SQL dinamico

Funziona anche per un sottoinsieme di colonne iniziali, preservando i valori esistenti.

Il trucco è lasciare che Postgres crei la stringa di query con i nomi delle colonne dai cataloghi di sistema in modo dinamico, quindi la esegua.

Vedi le risposte correlate per il codice:

  • Aggiorna più colonne in una funzione trigger in plpgsql

  • Aggiornamento collettivo di tutte le colonne

  • Aggiorna i campi SQL di una tabella dai campi di un'altra