J'ai deux colonnes dans la table col1
, col2
, les deux sont uniques indexées (col1 est unique et col2 également).
J'ai besoin d'insérer dans cette table, utiliser la syntaxe ON CONFLICT
et mettre à jour d'autres colonnes, mais je ne peux pas utiliser les deux colonnes dans conflict_target
clause.
Ça marche:
INSERT INTO table
...
ON CONFLICT ( col1 )
DO UPDATE
SET
-- update needed columns here
Mais comment faire cela pour plusieurs colonnes, quelque chose comme ceci:
...
ON CONFLICT ( col1, col2 )
DO UPDATE
SET
....
CREATE TABLE dupes(col1 int primary key, col2 int, col3 text,
CONSTRAINT col2_unique UNIQUE (col2)
);
INSERT INTO dupes values(1,1,'a'),(2,2,'b');
INSERT INTO dupes values(3,2,'c')
ON CONFLICT (col1) DO UPDATE SET col3 = 'c', col2 = 2
Appelons cela Q1. Le résultat est
ERROR: duplicate key value violates unique constraint "col2_unique"
DETAIL: Key (col2)=(2) already exists.
conflit_target peut effectuer une inférence d'index unique. Lors de l'exécution inférence, elle consiste en une ou plusieurs colonnes index_column_name et/ou expressions_index_expression et un index_predicate facultatif. Tout table_name index uniques qui, sans tenir compte de l'ordre, contiennent exactement les colonnes/expressions spécifiées par conflit_target sont déduites (choisi) en tant qu'indices d'arbitrage. Si un index_predicate est spécifié, il doit, comme autre exigence d’inférence, satisfaire les indices de l’arbitre.
Cela donne l'impression que la requête suivante devrait fonctionner, mais ce n'est pas le cas, car elle nécessiterait en fait un index unique sur col1 et col2. Cependant, un tel indice ne garantirait pas que col1 et col2 seraient uniques, ce qui est l'une des exigences du PO.
INSERT INTO dupes values(3,2,'c')
ON CONFLICT (col1,col2) DO UPDATE SET col3 = 'c', col2 = 2
Appelons cette requête Q2 (cela échoue avec une erreur de syntaxe)
Postgresql se comporte de cette façon parce que ce qui devrait se passer lorsqu'un conflit se produit dans la deuxième colonne n'est pas bien défini. Il y a beaucoup de possibilités. Par exemple, dans la requête Q1 ci-dessus, postgresql devrait-il mettre à jour col1
en cas de conflit sur col2
? Mais que faire si cela conduit à un autre conflit sur col1
? Comment Postgresql devrait-il gérer cela?
Une solution consiste à combiner ON CONFLICT avec à l'ancienne UPSERT .
CREATE OR REPLACE FUNCTION merge_db(key1 INT, key2 INT, data TEXT) RETURNS VOID AS
$$
BEGIN
LOOP
-- first try to update the key
UPDATE dupes SET col3 = data WHERE col1 = key1 and col2 = key2;
IF found THEN
RETURN;
END IF;
-- not there, so try to insert the key
-- if someone else inserts the same key concurrently, or key2
-- already exists in col2,
-- we could get a unique-key failure
BEGIN
INSERT INTO dupes VALUES (key1, key2, data) ON CONFLICT (col1) DO UPDATE SET col3 = data;
RETURN;
EXCEPTION WHEN unique_violation THEN
BEGIN
INSERT INTO dupes VALUES (key1, key2, data) ON CONFLICT (col2) DO UPDATE SET col3 = data;
RETURN;
EXCEPTION WHEN unique_violation THEN
-- Do nothing, and loop to try the UPDATE again.
END;
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
Vous devrez modifier la logique de cette fonction stockée afin qu'elle mette à jour les colonnes exactement comme vous le souhaitez. Invoquez-le comme
SELECT merge_db(3,2,'c');
SELECT merge_db(1,2,'d');
ON CONFLICT
requiert un index unique * pour détecter les conflits. Il vous suffit donc de créer un index unique sur les deux colonnes:
t=# create table t (id integer, a text, b text);
CREATE TABLE
t=# create unique index idx_t_id_a on t (id, a);
CREATE INDEX
t=# insert into t values (1, 'a', 'foo');
INSERT 0 1
t=# insert into t values (1, 'a', 'bar') on conflict (id, a) do update set b = 'bar';
INSERT 0 1
t=# select * from t;
id | a | b
----+---+-----
1 | a | bar
* En plus des index uniques, vous pouvez également utiliser des contraintes d’exclusion . Celles-ci sont un peu plus générales que des contraintes uniques. Supposons que votre table ait des colonnes pour id
et valid_time
(et valid_time
est une tsrange
) et que vous souhaitiez autoriser les doublons id
s, mais pas pour les périodes qui se chevauchent. Une contrainte unique ne vous aidera pas, mais avec une contrainte d'exclusion, vous pouvez dire "exclure les nouveaux enregistrements si leur id
est égal à un ancien id
et que leur valid_time
chevauche son valid_time
."
OU ET
Vlad a eu la bonne idée.
Vous devez d’abord créer une contrainte unique de la table sur les colonnes col1, col2
. Ensuite, vous pouvez effectuer les opérations suivantes:
INSERT INTO dupes values(3,2,'c')
ON CONFLICT ON CONSTRAINT dupes_pkey
DO UPDATE SET col3 = 'c', col2 = 2
Si vous utilisez postgres 9.5, vous pouvez utiliser l’espace EXCLUDED.
Exemple tiré de Quoi de neuf dans PostgreSQL 9.5 :
INSERT INTO user_logins (username, logins)
VALUES ('Naomi',1),('James',1)
ON CONFLICT (username)
DO UPDATE SET logins = user_logins.logins + EXCLUDED.logins;
C'est un peu compliqué, mais j'ai résolu ce problème en concaténant les deux valeurs de col1 et col2 dans une nouvelle colonne, col3 (un peu comme un index des deux) et comparé à cela. Cela ne fonctionne que si vous en avez besoin pour faire correspondre les deux col1 et col2.
INSERT INTO table
...
ON CONFLICT ( col3 )
DO UPDATE
SET
-- update needed columns here
Où col3 = la concaténation des valeurs de col1 et de col2.
Vous pouvez généralement (je pense) générer une instruction avec un seul on conflict
qui spécifie la seule et unique contrainte qui est pertinente, pour la chose que vous insérez.
Parce que généralement, une seule contrainte est la "pertinente", à la fois. (Si beaucoup, alors je me demande si quelque chose est bizarre/bizarre, hmm.)
Exemple:
(Licence: Not CC0, uniquement CC-By)
// there're these unique constraints:
// unique (site_id, people_id, page_id)
// unique (site_id, people_id, pages_in_whole_site)
// unique (site_id, people_id, pages_in_category_id)
// and only *one* of page-id, category-id, whole-site-true/false
// can be specified. So only one constraint is "active", at a time.
val thingColumnName = thingColumnName(notfificationPreference)
val insertStatement = s"""
insert into page_notf_prefs (
site_id,
people_id,
notf_level,
page_id,
pages_in_whole_site,
pages_in_category_id)
values (?, ?, ?, ?, ?, ?)
-- There can be only one on-conflict clause.
on conflict (site_id, people_id, $thingColumnName) <—— look
do update set
notf_level = excluded.notf_level
"""
val values = List(
siteId.asAnyRef,
notfPref.peopleId.asAnyRef,
notfPref.notfLevel.toInt.asAnyRef,
// Only one of these is non-null:
notfPref.pageId.orNullVarchar,
if (notfPref.wholeSite) true.asAnyRef else NullBoolean,
notfPref.pagesInCategoryId.orNullInt)
runUpdateSingleRow(insertStatement, values)
Et:
private def thingColumnName(notfPref: PageNotfPref): String =
if (notfPref.pageId.isDefined)
"page_id"
else if (notfPref.pagesInCategoryId.isDefined)
"pages_in_category_id"
else if (notfPref.wholeSite)
"pages_in_whole_site"
else
die("TyE2ABK057")
La clause on conflict
est générée dynamiquement, en fonction de ce que j'essaie de faire. Si j'insère une préférence de notification pour une page, il peut y avoir un conflit unique sur la contrainte site_id, people_id, page_id
. Et si je configure les préférences de notification pour une catégorie, je sais plutôt que la contrainte qui peut être violée est site_id, people_id, category_id
.
Je peux donc, et vous aussi probablement, dans votre cas?, Générer le on conflict (... columns )
correct, parce que je sais ce que je veux faire, puis je sais laquelle des nombreuses contraintes uniques peut être violé.
ON CONFLICT est une solution très maladroite, lancez
UPDATE dupes SET key1=$1, key2=$2 where key3=$3
if rowcount > 0
INSERT dupes (key1, key2, key3) values ($1,$2,$3);
fonctionne sur Oracle, Postgres et toutes les autres bases de données