Esclarecer o ON CONFLICT DO UPDATE
comportamento
Considere o manual aqui :
Para cada linha individual proposta para inserção, a inserção prossegue ou, se uma restrição ou índice de árbitro especificado por
conflict_target
for violado, a alternativa conflict_action
será adotada.
Negrito ênfase minha. Portanto, você não precisa repetir predicados para colunas incluídas no índice exclusivo na WHERE
cláusula to UPDATE
(the conflict_action
):
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'
A violação exclusiva 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ê se mencionou (mas com lógica invertida):
CREATE UNIQUE INDEX test_upsert_partial_idx
ON public.test_upsert (name, status)
WHERE test_field IS NULL; -- not: "is not null"
Para usar este índice parcial no seu UPSERT, você precisa de uma correspondência como @ypercube demonstra :conflict_target
ON CONFLICT (name, status) WHERE test_field IS NULL
Agora, o índice parcial acima é inferido. No entanto , como o manual também observa :
[...] um índice exclusivo não parcial (um índice exclusivo sem predicado) será inferido (e, portanto, usado por ON CONFLICT
) se um índice que satisfaça todos os outros critérios estiver disponível.
Se você tiver um índice adicional (ou apenas) apenas (name, status)
esse (também) será usado. Um índice (name, status, test_field)
ativado explicitamente não seria inferido. Isso não explica o seu problema, mas pode ter aumentado a confusão durante o teste.
Solução
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, se houver, criarão uma exceção. Suponho que não é isso que você quer. Você escreve:
A chave composta é composta por 20 colunas, 10 das quais podem ser anuláveis.
O que exatamente você considera uma duplicata? O Postgres (de acordo com o padrão SQL) não considera dois valores NULL iguais. O manual:
Em geral, uma restrição exclusiva é violada se houver mais de uma linha na tabela em que os valores de todas as colunas incluídas na restrição são iguais. No entanto, dois valores nulos nunca são considerados iguais nessa comparação. Isso significa que, mesmo na presença de uma restrição exclusiva, é possível armazenar linhas duplicadas que contêm um valor nulo em pelo menos uma das colunas restritas. Esse comportamento está em conformidade com o padrão SQL, mas ouvimos dizer que outros bancos de dados SQL podem não seguir esta regra. Portanto, tenha cuidado ao desenvolver aplicativos que se destinam a serem portáteis.
Palavras-chave:
Suponho que você queira que osNULL
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 deles, são 3 índices parciais para (a)
, (b)
e (a,b)
. O número está crescendo exponencialmente com 2^n - 1
. Para suas 10 colunas anuláveis, para cobrir todas as combinações possíveis de valores NULL, você já precisaria de 1023 índices parciais. Não vá.
A solução simples: substitua valores NULL e defina as colunas envolvidas NOT NULL
, e tudo funcionaria bem com uma UNIQUE
restrição simples .
Se isso não for uma opção, sugiro um índice de expressão com COALESCE
para substituir NULL no índice:
CREATE UNIQUE INDEX test_upsert_solution_idx
ON test_upsert (name, status, COALESCE(test_field, ''));
A string vazia ( ''
) é uma candidata óbvia aos tipos de caracteres, mas você pode usar qualquer valor legal que nunca apareça ou possa ser dobrado com NULL, de acordo com sua definição de "exclusivo".
Então use esta declaração:
INSERT INTO test_upsert as tu(name,status,test_field,identifier, count)
VALUES ('shaun', 1, null , 'ident', 11) -- works with
, ('bob' , 2, 'test value', 'ident', 22) -- and without NULL
ON CONFLICT (name, status, COALESCE(test_field, '')) DO UPDATE -- match expr. index
SET count = COALESCE(tu.count + EXCLUDED.count, EXCLUDED.count, tu.count);
Como @ypercube, suponho que você realmente deseja adicionar count
à contagem existente. Como a coluna pode ser NULL, a adição de NULL definiria a coluna NULL. Se você definir count NOT NULL
, poderá simplificar.
Outra idéia seria simplesmente retirar o conflito_target da declaraçã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 acontecer ON CONFLICT DO UPDATE
. O manual mais uma vez:
Para ON CONFLICT DO NOTHING
, é opcional especificar um destination_target; quando omitido, são tratados conflitos com todas as restrições utilizáveis (e índices exclusivos). Para ON CONFLICT DO UPDATE
, deve ser fornecido um target_target.
count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) END
pode ser simplificado paracount = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)