Estou tendo um problema ao usar o novo recurso UPSERT no Postgres 9.5
Eu tenho uma tabela que é usada para agregar dados de outra tabela. A chave composta é composta por 20 colunas, 10 das quais podem ser anuláveis. Abaixo, criei uma versão menor do problema que estou tendo, especificamente com valores NULL.
CREATE TABLE public.test_upsert (
upsert_id serial,
name character varying(32) NOT NULL,
status integer NOT NULL,
test_field text,
identifier character varying(255),
count integer,
CONSTRAINT upsert_id_pkey PRIMARY KEY (upsert_id),
CONSTRAINT test_upsert_name_status_test_field_key UNIQUE (name, status, test_field)
);
A execução desta consulta funciona conforme necessário (primeiro insira, depois as inserções subsequentes simplesmente incrementam a contagem):
INSERT INTO test_upsert as tu(name,status,test_field,identifier, count)
VALUES ('shaun',1,'test value','ident', 1)
ON CONFLICT (name,status,test_field) DO UPDATE set count = tu.count + 1
where tu.name = 'shaun' AND tu.status = 1 AND tu.test_field = 'test value';
No entanto, se eu executar esta consulta, 1 linha será inserida a cada vez, em vez de incrementar a contagem da linha inicial:
INSERT INTO test_upsert as tu(name,status,test_field,identifier, count)
VALUES ('shaun',1,null,'ident', 1)
ON CONFLICT (name,status,test_field) DO UPDATE set count = tu.count + 1
where tu.name = 'shaun' AND tu.status = 1 AND tu.test_field = null;
Este é o meu problema. Preciso simplesmente incrementar o valor de contagem e não criar várias linhas idênticas com valores nulos.
Tentando adicionar um índice exclusivo parcial:
CREATE UNIQUE INDEX test_upsert_upsert_id_idx
ON public.test_upsert
USING btree
(name COLLATE pg_catalog."default", status, test_field, identifier);
No entanto, isso produz os mesmos resultados, várias linhas nulas sendo inseridas ou esta mensagem de erro ao tentar inserir:
ERRO: não há restrição exclusiva ou de exclusão correspondente à especificação ON CONFLICT
Já tentei adicionar detalhes extras no índice parcial, como WHERE test_field is not null OR identifier is not null
. No entanto, ao inserir, recebo a mensagem de erro de restrição.
esclarecer o
ON CONFLICT DO UPDATE
comportamentoConsidere o manual aqui :
Ênfase em negrito minha. Portanto, você não precisa repetir predicados para colunas incluídas no índice exclusivo na
WHERE
cláusula para oUPDATE
(oconflict_action
):A violação única já estabelece o que sua
WHERE
cláusula adicionada aplicaria de forma redundante.Esclarecer índice parcial
Adicione uma
WHERE
cláusula para torná-lo um índice parcial real, como você mesmo mencionou (mas com lógica invertida):Para usar este índice parcial em seu UPSERT, você precisa de uma correspondência como @ypercube demonstra :
conflict_target
Agora o índice parcial acima é inferido. No entanto , como o manual também observa :
Se você tiver um índice adicional (ou apenas) apenas
(name, status)
ele será (também) usado. Um índice em(name, status, test_field)
explicitamente não seria inferido. Isso não explica seu problema, mas pode ter aumentado a confusão durante o teste.Soluções
AIUI, nenhuma das opções acima resolve seu problema , ainda. Com o índice parcial, apenas casos especiais com valores NULL correspondentes seriam capturados. E outras linhas duplicadas seriam inseridas se você não tiver outros índices/restrições exclusivos correspondentes ou levantaria uma exceção se tivesse. Acho que não é isso que você quer. Você escreve:
O que exatamente você considera uma duplicata? Postgres (de acordo com o padrão SQL) não considera dois valores NULL iguais. O manual:
Relacionado:
Suponho que você deseja que
NULL
os valores em todas as 10 colunas anuláveis sejam considerados iguais. É elegante e prático cobrir uma única coluna anulável com um índice parcial adicional, como demonstrado aqui:Mas isso fica fora de controle rapidamente para mais colunas anuláveis. Você precisaria de um índice parcial para cada combinação distinta de colunas anuláveis. Para apenas 2 desses, são 3 índices parciais para
(a)
,(b)
e(a,b)
. O número está crescendo exponencialmente com2^n - 1
. Para suas 10 colunas anuláveis, para cobrir todas as combinações possíveis de valores NULL, você já precisaria de 1.023 índices parciais. Não vá.A solução simples: substitua os valores NULL e defina as colunas envolvidas
NOT NULL
, e tudo funcionaria bem com umaUNIQUE
restrição simples.Se essa não for uma opção, sugiro uma expressão index com
COALESCE
para substituir NULL no índice:A string vazia (
''
) é um candidato óbvio para tipos de caracteres, mas você pode usar qualquer valor legal que nunca apareça ou que possa ser dobrado com NULL de acordo com sua definição de "único".Em seguida, use esta declaração:
Como @ypercube, suponho que você realmente deseja adicionar
count
ao arquivocount
. Como a coluna pode ser NULL, adicionar NULL definiria a coluna NULL. Se você definircount NOT NULL
, poderá simplificar.Ou considere esta resposta posterior relacionada usando um valor de hash para a linha:
Uma ideia diferente seria apenas eliminar o conflict_target da instrução para cobrir todas as violações exclusivas . Em seguida, você pode definir vários índices exclusivos para uma definição mais sofisticada do que deveria ser "exclusivo". Mas isso não vai voar com
ON CONFLICT DO UPDATE
. O manual mais uma vez:Acho que o problema é que você não tem um índice parcial e a
ON CONFLICT
sintaxe não corresponde aotest_upsert_upsert_id_idx
índice, mas à outra restrição exclusiva.Se você definir o índice como parcial (com
WHERE test_field IS NULL
):e essas linhas já na tabela:
então a query terá sucesso:
com os seguintes resultados: