PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Comment mettre à jour toutes les colonnes avec INSERT... ON CONFLICT... ?

La UPDATE syntaxe nécessite pour nommer explicitement les colonnes cibles. Raisons possibles pour éviter cela :

  • Vous avez de nombreuses colonnes et souhaitez simplement raccourcir la syntaxe.
  • Vous ne savez pas les noms de colonne à l'exception de la ou des colonne(s) unique(s).

"All columns" doit signifier "toutes les colonnes de la table cible" (ou au moins "colonnes de tête du tableau" ) dans l'ordre et le type de données correspondants. Sinon, vous devrez de toute façon fournir une liste de noms de colonnes cibles.

Tableau des tests :

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

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

1. DELETE &INSERT en requête unique à la place

Sans connaître les noms de colonne à l'exception de id .

Ne fonctionne que pour "toutes les colonnes de la table cible" . Bien que la syntaxe fonctionne même pour un sous-ensemble principal, les colonnes en excès dans la table cible seraient réinitialisées à NULL avec DELETE et INSERT .

UPSERT (INSERT ... ON CONFLICT ... ) est nécessaire pour éviter les problèmes de concurrence/verrouillage lors d'une charge d'écriture simultanée, et uniquement parce qu'il n'existe aucun moyen général de verrouiller des lignes qui n'existent pas encore dans Postgres (verrouillage de valeur ).

Votre exigence spéciale n'affecte que la UPDATE partie. Les complications possibles ne s'appliquent pas lorsqu'elles existent les lignes sont affectées. Ceux-ci sont correctement verrouillés. En simplifiant un peu plus, vous pouvez réduire votre cas à DELETE et 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;

Dans le modèle Postgres MVCC, un UPDATE est en grande partie identique à DELETE et INSERT de toute façon (sauf pour certains cas extrêmes avec concurrence, mises à jour HOT et grandes valeurs de colonne stockées hors ligne). Puisque vous voulez remplacer toutes les lignes de toute façon, supprimez simplement les lignes en conflit avant le INSERT . Les lignes supprimées restent verrouillées jusqu'à ce que la transaction soit validée. Le INSERT peut uniquement trouver des lignes en conflit pour des valeurs de clé précédemment inexistantes si une transaction simultanée arrive à les insérer simultanément (après le DELETE , mais avant le INSERT ).

Vous perdriez des valeurs de colonne supplémentaires pour les lignes concernées dans ce cas particulier. Aucune exception levée. Mais si les requêtes concurrentes ont la même priorité, ce n'est pas un problème :l'autre requête a gagné pour certaines Lignes. De plus, si l'autre requête est un UPSERT similaire, son alternative est d'attendre que cette transaction soit validée, puis mise à jour immédiatement. "Gagner" pourrait être une victoire à la Pyrrhus.

À propos des "mises à jour vides" :

  • Comment puis-je (ou puis-je) SELECT DISTINCT sur plusieurs colonnes ?

Non, ma requête doit gagner !

OK, vous l'avez demandé :

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;

Comment ?

  • Les 1ères data CTE fournit juste des données. Peut-être une table à la place.
  • Le 2ème CTE ups :UPSERT. Lignes avec id en conflit ne sont pas modifiés, mais également verrouillés .
  • Le 3e CTE del supprime les lignes en conflit. Ils restent verrouillés.
  • Le 4e CTE ins insère des lignes entières . Autorisé uniquement pour la même transaction
  • Le SELECT final est uniquement destiné à la démo pour montrer ce qui s'est passé.

Pour vérifier les mises à jour vides, testez (avant et après) avec :

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

La vérification (commentée) de tout changement dans la ligne AND t <> d fonctionne même avec des valeurs NULL car nous comparons deux valeurs de ligne typées selon le manuel :

deux valeurs de champ NULL sont considérées comme égales et une valeur NULL est considérée comme supérieure à une valeur non NULL

2. SQL dynamique

Cela fonctionne également pour un sous-ensemble de colonnes principales, en préservant les valeurs existantes.

L'astuce consiste à laisser Postgres créer dynamiquement la chaîne de requête avec les noms de colonne des catalogues système, puis à l'exécuter.

Voir les réponses associées pour le code :

  • Mettre à jour plusieurs colonnes dans une fonction de déclenchement dans plpgsql

  • Mise à jour en masse de toutes les colonnes

  • Mise à jour SQL des champs d'une table à partir des champs d'une autre