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