A
UPDATE
sintaxe requer para nomear explicitamente as colunas de destino. Possíveis razões para evitar isso:- Você tem muitas colunas e quer apenas encurtar a sintaxe.
- Você não sabe nomes de coluna, exceto para a(s) coluna(s) exclusiva(s).
"All columns"
tem que significar "todas as colunas da tabela de destino" (ou pelo menos "colunas iniciais da tabela" ) em ordem e tipo de dados correspondentes. Caso contrário, você teria que fornecer uma lista de nomes de coluna de destino de qualquer maneira. Tabela de teste:
CREATE TABLE tbl (
id int PRIMARY KEY
, text text
, extra text
);
INSERT INTO tbl AS t
VALUES (1, 'foo')
, (2, 'bar');
1. DELETE
&INSERT
em uma única consulta
Sem saber nenhum nome de coluna, exceto
id
. Funciona apenas para "todas as colunas da tabela de destino" . Embora a sintaxe funcione até mesmo para um subconjunto inicial, as colunas em excesso na tabela de destino seriam redefinidas para NULL com
DELETE
e INSERT
. UPSERT (
INSERT ... ON CONFLICT ...
) é necessário para evitar problemas de simultaneidade/bloqueio sob carga de gravação simultânea e apenas porque não há uma maneira geral de bloquear linhas ainda não existentes no Postgres (bloqueio de valor ). Seu requisito especial afeta apenas o
UPDATE
papel. Possíveis complicações não se aplicam quando existir linhas são afetadas. Esses estão bloqueados corretamente. Simplificando um pouco mais, você pode reduzir seu caso para DELETE
e INSERT
:WITH data(id) AS ( -- Only 1st column gets explicit name!
VALUES
(1, 'foo_upd', 'a') -- changed
, (2, 'bar', 'b') -- unchanged
, (3, 'baz', 'c') -- new
)
, del AS (
DELETE FROM tbl AS t
USING data d
WHERE t.id = d.id
-- AND t <> d -- optional, to avoid empty updates
) -- only works for complete rows
INSERT INTO tbl AS t
TABLE data -- short for: SELECT * FROM data
ON CONFLICT (id) DO NOTHING
RETURNING t.id;
No modelo Postgres MVCC, um
UPDATE
é basicamente o mesmo que DELETE
e INSERT
de qualquer forma (exceto para alguns casos de canto com simultaneidade, atualizações HOT e grandes valores de coluna armazenados fora de linha). Como você deseja substituir todas as linhas de qualquer maneira, apenas remova as linhas conflitantes antes do INSERT
. As linhas excluídas permanecem bloqueadas até que a transação seja confirmada. O INSERT
só poderá encontrar linhas conflitantes para valores de chave anteriormente não existentes se uma transação simultânea acontecer para inseri-los simultaneamente (após o DELETE
, mas antes do INSERT
). Você perderia valores de coluna adicionais para linhas afetadas neste caso especial. Nenhuma exceção levantada. Mas se as consultas concorrentes tiverem a mesma prioridade, isso dificilmente será um problema:a outra consulta ganhou para algumas linhas. Além disso, se a outra consulta for um UPSERT semelhante, sua alternativa é aguardar a confirmação dessa transação e, em seguida, atualizar imediatamente. "Vencer" poderia ser uma vitória de Pirro.
Sobre "atualizações vazias":
- Como posso (ou posso) SELECT DISTINCT em várias colunas?
Não, minha consulta deve vencer!
Pronto, você pediu:
WITH data(id) AS ( -- Only 1st column gets explicit name!
VALUES -- rest gets default names "column2", etc.
(1, 'foo_upd', NULL) -- changed
, (2, 'bar', NULL) -- unchanged
, (3, 'baz', NULL) -- new
, (4, 'baz', NULL) -- new
)
, ups AS (
INSERT INTO tbl AS t
TABLE data -- short for: SELECT * FROM data
ON CONFLICT (id) DO UPDATE
SET id = t.id
WHERE false -- never executed, but locks the row!
RETURNING t.id
)
, del AS (
DELETE FROM tbl AS t
USING data d
LEFT JOIN ups u USING (id)
WHERE u.id IS NULL -- not inserted !
AND t.id = d.id
-- AND t <> d -- avoid empty updates - only for full rows
RETURNING t.id
)
, ins AS (
INSERT INTO tbl AS t
SELECT *
FROM data
JOIN del USING (id) -- conflict impossible!
RETURNING id
)
SELECT ARRAY(TABLE ups) AS inserted -- with UPSERT
, ARRAY(TABLE ins) AS updated -- with DELETE & INSERT;
Como?
- Os
data
do 1º CTE apenas fornece dados. Pode ser uma tabela. - Os
ups
do 2º CTE :UPSERT. Linhas comid
conflitante não são alterados, mas também bloqueados . - O 3º CTE
del
exclui linhas conflitantes. Eles permanecem bloqueados. - O 4º CTE
ins
insere linhas inteiras . Permitido apenas para a mesma transação - O SELECT final é apenas para a demonstração para mostrar o que aconteceu.
Para verificar se há atualizações vazias, teste (antes e depois) com:
SELECT ctid, * FROM tbl; -- did the ctid change?
A verificação (comentada) para quaisquer alterações na linha
AND t <> d
funciona mesmo com valores NULL porque estamos comparando dois valores de linha digitados de acordo com o manual:
dois valores de campo NULL são considerados iguais e um NULL é considerado maior que um não NULL
2. SQL dinâmico
Isso também funciona para um subconjunto de colunas iniciais, preservando os valores existentes.
O truque é deixar o Postgres construir a string de consulta com os nomes das colunas dos catálogos do sistema dinamicamente e depois executá-la.
Veja as respostas relacionadas para o código:
-
Atualizar várias colunas em uma função de gatilho em plpgsql
-
Atualização em massa de todas as colunas
-
SQL atualiza campos de uma tabela a partir de campos de outra