Utilice varios conflictos_target en la cláusula ON CONFLICT

95

Tengo dos columnas en la tabla col1, col2ambas están indexadas de forma única (col1 es única y también lo es col2).

Necesito insertar en esta tabla, usar la ON CONFLICTsintaxis y actualizar otras columnas, pero no puedo usar ambas columnas en la conflict_targetcláusula.

Funciona:

INSERT INTO table
...
ON CONFLICT ( col1 ) 
DO UPDATE 
SET 
-- update needed columns here

Pero cómo hacer esto para varias columnas, algo como esto:

...
ON CONFLICT ( col1, col2 )
DO UPDATE 
SET 
....
Oto Shavadze
fuente
4
"col1, col2, ambos tienen un índice único". ¿Eso significa que col1 es única y col2 es única o son las combinaciones de col1, col2 únicas?
e4c5
1
¿Eso significa que col1 es único y col2 es único, individualmente
Oto Shavadze

Respuestas:

48

Una tabla de muestra y datos

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

Reproduciendo el problema

INSERT INTO dupes values(3,2,'c')
ON CONFLICT (col1) DO UPDATE SET col3 = 'c', col2 = 2

Llamemos a esto Q1. El resultado es

ERROR:  duplicate key value violates unique constraint "col2_unique"
DETAIL:  Key (col2)=(2) already exists.

Que dice la documentación

conflict_target puede realizar una inferencia de índice única. Al realizar inferencias, consta de una o más columnas index_column_name y / o expresiones index_expression, y un index_predicate opcional. Todos los índices únicos table_name que, independientemente del orden, contienen exactamente las columnas / expresiones especificadas por conflict_target se infieren (eligen) como índices de árbitro. Si se especifica un index_predicate, debe, como requisito adicional para la inferencia, satisfacer los índices de árbitro.

Esto da la impresión de que la siguiente consulta debería funcionar, pero no es así porque en realidad requeriría un índice único conjunto en col1 y col2. Sin embargo, tal índice no garantizaría que col1 y col2 sean únicos individualmente, que es uno de los requisitos del OP.

INSERT INTO dupes values(3,2,'c') 
ON CONFLICT (col1,col2) DO UPDATE SET col3 = 'c', col2 = 2

Llamemos a esta consulta Q2 (esto falla con un error de sintaxis)

¿Por qué?

Postgresql se comporta de esta manera porque lo que debería suceder cuando ocurre un conflicto en la segunda columna no está bien definido. Hay varias posibilidades. Por ejemplo, en la consulta Q1 anterior, ¿debería actualizarse postgresql col1cuando hay un conflicto col2? Pero, ¿y si eso lleva a otro conflicto col1? ¿Cómo se espera que postgresql maneje eso?

Una solución

Una solución es combinar ON CONFLICT con UPSERT a la antigua .

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;

Debería modificar la lógica de esta función almacenada para que actualice las columnas exactamente de la manera que desea. Invocarlo como

SELECT merge_db(3,2,'c');
SELECT merge_db(1,2,'d');
e4c5
fuente
3
Esta es la forma en que funciona, pero un poco más de trabajo / lógica de lo necesario, todo lo que tienes que hacer es crear una restricción única en las dos columnas. Vea mi respuesta a continuación.
Jubair
¿Puedo usar la solución merge_db también si estoy insertando varios conjuntos de VALORES a la vez?
daniyel
@daniyel tendrás que reescribir la función almacenada
e4c5
3
No me queda claro cómo es útil sugerir el uso de upsert anticuado; esta pregunta está bien referenciada para "postgres upsert 9.5" y podría ser mejor si se explica cómo usarla con todas las opciones de constraint_names.
Pak
3
@Pak No le queda claro porque no ha leído la pregunta con claridad. La operación no busca una clave compuesta en esos campos. La otra respuesta funciona para claves compuestas
e4c5
65

ON CONFLICTrequiere un índice único * para realizar la detección de conflictos. Entonces, solo necesita crear un índice único en ambas columnas:

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

* Además de los índices únicos, también puede utilizar restricciones de exclusión . Estas son restricciones un poco más generales que únicas. Suponga que su tabla tiene columnas para idy valid_time(y valid_timees a tsrange), y desea permitir duplicados id, pero no para períodos de tiempo superpuestos. Una restricción única no le ayudará, pero con una restricción de exclusión puede decir "excluir nuevos registros si son idiguales a los antiguos idy también se valid_timesuperponen valid_time".

Paul A Jungwirth
fuente
4
Lo que esto crea es un índice único en conjunto que crea un índice único idx_t_id_a en t (id, a); Por supuesto, el OP no indica claramente si las dos columnas son únicas individualmente o juntas.
e4c5
¿Por qué Postgres a veces dice que no hay una columna con el nombre del índice y no se usa ON CONFLICT?
Pak
@Pak parece que debería escribir su propia pregunta con el comando específico que está usando y el mensaje de error que recibe.
Paul A Jungwirth
@PaulAJungwirth No lo sé, su respuesta es acertada: un índice único como restricción para el on conflictcomando. El error es simplemente "la columna my_index_name no existe".
Pak
Intenté esto de todos modos con una restricción única separada en cada columna como pedía el OP, y no funcionó. No es que lo esperaba, pero lo estaba esperando.
sudo
6

En la actualidad es (parece) imposible. Ni la última versión de la ON CONFLICT sintaxis permite repetir la cláusula, ni con CTE es posible: no es posible romper el INSERT de ON CONFLICT para agregar más objetivos de conflicto.

Peter Krauss
fuente
3

Si está utilizando postgres 9.5, puede utilizar el espacio EXCLUIDO.

Ejemplo tomado de Novedades de 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;
Martin Gerhardy
fuente
2
  1. Cree una restricción (índice externo, por ejemplo).

O Y

  1. Observe las restricciones existentes (\ d en psq).
  2. Utilice ON CONSTRAINT (nombre_restricción) en la cláusula INSERT.
Vladimir Voznesensky
fuente
1

Vlad tuvo la idea correcta.

Primero debe crear una restricción única de tabla en las columnas col1, col2 Luego, una vez que lo haga, puede hacer lo siguiente:

INSERT INTO dupes values(3,2,'c') 
ON CONFLICT ON CONSTRAINT dupes_pkey 
DO UPDATE SET col3 = 'c', col2 = 2
Jubair
fuente
5
Lo siento, pero ha entendido mal la pregunta. El OP no quiere una restricción única conjunta.
e4c5
1

Algo hacky, pero resolví esto concatenando los dos valores de col1 y col2 en una nueva columna, col3 (algo así como un índice de los dos) y comparándolo con eso. Esto solo funciona si necesita que coincida con AMBOS col1 y col2.

INSERT INTO table
...
ON CONFLICT ( col3 ) 
DO UPDATE 
SET 
-- update needed columns here

Donde col3 = la concatenación de los valores de col1 y col2.

Niko Dunk
fuente
3
puede crear un índice único para esas dos columnas y aplicar esa restricción on conflict.
Kishore Relangi
0

Por lo general, creo que puede generar una declaración con solo una on conflictque especifica la única restricción que es relevante para lo que está insertando.

Porque normalmente, solo una restricción es la "relevante", a la vez. (Si hay muchos, entonces me pregunto si algo tiene un diseño extraño / extraño, hmm).

Ejemplo:
(Licencia: No CC0, solo 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)

Y:

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 on conflictcláusula se genera dinámicamente, dependiendo de lo que estoy tratando de hacer. Si estoy insertando una preferencia de notificación, para una página, entonces puede haber un conflicto único en la site_id, people_id, page_idrestricción. Y si estoy configurando preferencias de notificación, para una categoría, entonces sé que la restricción que puede violarse es site_id, people_id, category_id.

Entonces, puedo, y es muy probable que usted también, en su caso, generar la correcta on conflict (... columns ), porque sé lo que quiero hacer, y luego sé cuál de las muchas restricciones únicas es la que puede ser violada.

KajMagnus
fuente
-4

ON CONFLICT es una solución muy torpe, ejecuta

UPDATE dupes SET key1=$1, key2=$2 where key3=$3    
if rowcount > 0    
  INSERT dupes (key1, key2, key3) values ($1,$2,$3);

funciona en Oracle, Postgres y todas las demás bases de datos

usuario2625834
fuente
No es atómico, por lo que podría fallar y producir resultados incorrectos en caso de múltiples conexiones al mismo tiempo.
Bogdan Mart