web-dev-qa-db-fra.com

Utilisez multiple conflict_target dans la clause ON CONFLICT

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_targetclause. 

Ç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 
....
48
OTAR

Un exemple de tableau et de données

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');

Reproduire le problème

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.

Que dit la documentation

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)

Pourquoi?

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

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');
28
e4c5

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 ids, 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."

40
Paul A Jungwirth

De nos jours, c'est (semble) impossible. Ni la dernière version de ON CONFLICTsyntax ne permet de répéter la clause, ni avec CTE n’est possible: il n’est pas possible de supprimer l’INSERT de ON CONFLICT pour ajouter d’autres cibles de conflit.

4
Peter Krauss
  1. Créez une contrainte (index étranger, par exemple).

OU ET

  1. Regardez les contraintes existantes (\ d dans psq).
  2. Utilisez ON CONSTRAINT (nom_contrainte) dans la clause INSERT.
3

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
1
Jubair

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;
1
Martin Gerhardy

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.

0
Niko Dunk

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é.

0
KajMagnus

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

0
user2625834