Use vários conflito_target na cláusula ON CONFLICT

93

Eu tenho duas colunas na tabela col1, col2ambas são indexadas exclusivas (col1 é exclusiva e também col2).

Eu preciso inserir nesta tabela, usar ON CONFLICTsintaxe e atualizar outras colunas, mas não posso usar ambas as colunas na conflict_targetcláusula.

Funciona:

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

Mas como fazer isso para várias colunas, algo assim:

...
ON CONFLICT ( col1, col2 )
DO UPDATE 
SET 
....
Oto Shavadze
fonte
4
"col1, col2, ambas são indexadas exclusivas." isso significa que col1 é único e col2 é único ou as combinações de col1, col2 são únicas?
e4c5
1
isso significa que col1 é único e col2 é único, individualmente
Oto Shavadze

Respostas:

48

Uma tabela de amostra e dados

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

Reproduzindo o problema

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

Vamos chamar isso de Q1. O resultado é

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

O que diz a documentação

conflito_target pode realizar inferência de índice exclusivo. Ao realizar inferência, ele consiste em uma ou mais colunas index_column_name e / ou expressões index_expression e um index_predicate opcional. Todos os índices exclusivos nome_da_tabela que, independentemente da ordem, contêm exatamente as colunas / expressões especificadas por conflito_alvo são inferidos (escolhidos) como índices de árbitro. Se um index_predicate for especificado, ele deve, como um requisito adicional para inferência, satisfazer os índices do árbitro.

Isso dá a impressão de que a consulta a seguir deve funcionar, mas não funciona porque, na verdade, seria necessário um índice exclusivo em col1 e col2. No entanto, tal índice não garantiria que col1 e col2 seriam únicas individualmente, o que é um dos requisitos do OP.

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

Vamos chamar essa consulta de Q2 (falha com um erro de sintaxe)

Por quê?

O Postgresql se comporta dessa forma porque o que deveria acontecer quando ocorre um conflito na segunda coluna não está bem definido. Existem várias possibilidades. Por exemplo, na consulta Q1 acima, o postgresql deve ser atualizado col1quando há um conflito col2? Mas e se isso levar a outro conflito col1? como o postgresql deve lidar com isso?

Uma solução

Uma solução é combinar ON CONFLICT com UPSERT antiquado .

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;

Você precisaria modificar a lógica desta função armazenada para que ela atualize as colunas exatamente da maneira que você deseja. Invoque como

SELECT merge_db(3,2,'c');
SELECT merge_db(1,2,'d');
e4c5
fonte
3
Esta é a maneira que funciona, mas um pouco mais de trabalho / lógica do que o necessário, tudo o que você realmente precisa fazer é criar uma restrição única nas duas colunas. Veja minha resposta abaixo.
Jubair
posso usar a solução merge_db também se estiver inserindo vários conjuntos de VALUES de uma vez?
daniyel
@daniyel você terá que reescrever a função armazenada
e4c5
3
Não está claro para mim como é útil sugerir o uso do upsert antiquado - esta pergunta é bem referenciada para "postgres upsert 9.5" e poderia ser melhor explicando como usá-lo com todas as opções de constraint_names.
Pak de
3
@Pak Não está claro para você porque você não leu a pergunta claramente. O op não está procurando por uma chave composta nesses campos. A outra resposta funciona para chaves compostas
e4c5
65

ON CONFLICTrequer um índice exclusivo * para fazer a detecção de conflito. Portanto, você só precisa criar um índice exclusivo em ambas as colunas:

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

* Além de índices exclusivos, você também pode usar restrições de exclusão . Estas são um pouco mais gerais do que restrições exclusivas. Suponha que sua tabela tenha colunas para ide valid_time(e valid_timeé a tsrange) e você queira permitir ids duplicados , mas não para períodos de tempo sobrepostos. Uma restrição única não o ajudará, mas com uma restrição de exclusão, você pode dizer "exclua novos registros se forem idiguais a um antigo ide também se valid_timesobrepuserem valid_time".

Paul A Jungwirth
fonte
4
O que isso cria é um índice exclusivo junto cria um índice exclusivo idx_t_id_a on t (id, a); É claro que o OP não declara claramente se as duas colunas são exclusivas individualmente ou juntas.
e4c5
Por que o postgres às vezes diz que não há uma coluna com o nome do índice e falha ao usar ON CONFLICT?
Pak de
@Pak parece que você deve escrever sua própria pergunta com o comando específico que está usando e a mensagem de erro que receber.
Paul A Jungwirth
@PaulAJungwirth Não sei, sua resposta é correta - um índice exclusivo como uma restrição para o on conflictcomando. O erro é apenas "a coluna my_index_name não existe".
Pak de
De qualquer forma, tentei fazer isso com uma restrição única separada em cada coluna, conforme o OP estava pedindo, e não funcionou. Não que eu esperasse, mas estava esperando.
sudo
5

Hoje em dia é (parece) impossível. Nem a última versão da ON CONFLICT sintaxe permite repetir a cláusula, nem com CTE é possível: não é possível quebrar o INSERT de ON CONFLICT para adicionar mais alvos de conflito.

Peter Krauss
fonte
3

Se você estiver usando o postgres 9.5, poderá usar o espaço EXCLUÍDO.

Exemplo retirado de O que há de novo no 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
fonte
2
  1. Crie uma restrição (índice estrangeiro, por exemplo).

OU E

  1. Observe as restrições existentes (\ d no psq).
  2. Use ON CONSTRAINT (constraint_name) na cláusula INSERT.
Vladimir Voznesensky
fonte
1

Vlad teve a ideia certa.

Primeiro, você deve criar uma restrição exclusiva da tabela nas colunas. col1, col2 Depois de fazer isso, você pode fazer o seguinte:

INSERT INTO dupes values(3,2,'c') 
ON CONFLICT ON CONSTRAINT dupes_pkey 
DO UPDATE SET col3 = 'c', col2 = 2
Jubair
fonte
4
Desculpe, mas você entendeu mal a pergunta. O OP não quer uma restrição única em conjunto.
e4c5
1

Tipo de hacky, mas resolvi isso concatenando os dois valores de col1 e col2 em uma nova coluna, col3 (como um índice dos dois) e comparei com isso. Isso só funciona se você precisar que corresponda a AMBOS col1 e col2.

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

Onde col3 = a concatenação dos valores de col1 e col2.

Niko Dunk
fonte
3
você pode criar um índice exclusivo para essas duas colunas e fornecer essa restrição em on conflict.
Kishore Relangi
0

Normalmente, você pode (eu acho) gerar uma declaração com apenas uma on conflictque especifica a única restrição que é relevante para o que você está inserindo.

Porque normalmente, apenas uma restrição é a "relevante" de cada vez. (Se muitos, então estou me perguntando se algo é estranho / desenhado de forma estranha, hmm.)

Exemplo:
(Licença: Não CC0, apenas 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)

E:

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")

A on conflictcláusula é gerada dinamicamente, dependendo do que estou tentando fazer. Se estou inserindo uma preferência de notificação, para uma página - então pode haver um conflito exclusivo, na site_id, people_id, page_idrestrição. E se eu estiver configurando preferências de notificação para uma categoria - então, em vez disso, sei que a restrição que pode ser violada é site_id, people_id, category_id.

Portanto, posso, e muito provavelmente você também, no seu caso ?, gerar o correto on conflict (... columns ), porque sei o que quero fazer e, então, sei qual das muitas restrições exclusivas é a que pode ser violada.

KajMagnus
fonte
-4

ON CONFLICT é uma solução muito desajeitada, execute

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

funciona em Oracle, Postgres e todos os outros bancos de dados

user2625834
fonte
Não é atômico, então pode falhar e produzir resultados errados no caso de várias conexões ao mesmo tempo.
Bogdan Mart