Postgres 9.5 implementou
UPSERT
. Veja abaixo. Postgres 9.4 ou anterior
Este é um problema complicado. Você está se deparando com esta restrição (por documentação):
Em umVALUES
lista que aparece no nível superior de umINSERT
, uma expressão pode ser substituída porDEFAULT
para indicar que o valor padrão da coluna de destino deve ser inserido.DEFAULT
não pode ser usado quandoVALUES
aparece em outros contextos.
Minha ênfase em negrito. Os valores padrão não são definidos sem uma tabela para inserir. Portanto, não há direto solução para sua pergunta, mas há várias rotas alternativas possíveis, dependendo dos requisitos exatos .
Buscar valores padrão do catálogo do sistema?
Você poderia buscá-los no catálogo do sistema
pg_attrdef
como @Patrick comentou ou de information_schema.columns
. Instruções completas aqui:- Obter os valores padrão das colunas da tabela no Postgres?
Mas então você ainda tem apenas uma lista de linhas com uma representação de texto da expressão para cozinhar o valor padrão. Você teria que construir e executar instruções dinamicamente para obter valores para trabalhar. Cansativo e confuso. Em vez disso, podemos permitir que a funcionalidade interna do Postgres faça isso por nós :
Atalho simples
Insira uma linha fictícia e retorne-a para usar os padrões gerados:
INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
Problemas/escopo da solução
- Isso só funciona para
STABLE
ouIMMUTABLE
expressões padrão . A maioriaVOLATILE
funções funcionarão tão bem, mas não há garantias. Ocurrent_timestamp
família de funções se qualifica como estável, pois seus valores não mudam dentro de uma transação.
Em particular, isso tem efeitos colaterais emserial
colunas (ou qualquer outro desenho padrão de uma sequência). Mas isso não deve ser um problema, porque você normalmente não escreve emserial
colunas diretamente. Esses não devem ser listados emINSERT
nenhuma instrução.
Falha restante paraserial
colunas:a sequência ainda é avançada pela chamada única para obter uma linha padrão, produzindo uma lacuna na numeração. Novamente, isso não deve ser um problema, porque as lacunas são geralmente esperadas emserial
colunas.
Mais dois problemas podem ser resolvidos:
-
Se você tiver colunas definidasNOT NULL
, você deve inserir valores fictícios e substituir porNULL
no resultado.
-
Na verdade, não queremos inserir a linha fictícia . Poderíamos excluir mais tarde (na mesma transação), mas isso pode ter mais efeitos colaterais, como gatilhosON DELETE
. Há um caminho melhor:
Evite linha fictícia
Clonar uma tabela temporária incluindo padrões de coluna e inserir naquele :
BEGIN;
CREATE TEMP TABLE tmp_playlist_items (LIKE playlist_items INCLUDING DEFAULTS)
ON COMMIT DROP; -- drop at end of transaction
INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *;
...
Mesmo resultado, menos efeitos colaterais. Como as expressões padrão são copiadas literalmente, o clone extrai das mesmas sequências, se houver. Mas outros efeitos colaterais da linha ou gatilhos indesejados são evitados completamente.
Crédito ao Igor pela ideia:
- Postgresql, selecione uma linha "falsa"
Remover NOT NULL
restrições
Você teria que fornecer valores fictícios para
NOT NULL
colunas, porque (por documentação):
As restrições não nulas são sempre copiadas para a nova tabela.
Acomode para aqueles no
INSERT
declaração ou (melhor) eliminar as restrições:ALTER TABLE tmp_playlist_items
ALTER COLUMN foo DROP NOT NULL
, ALTER COLUMN bar DROP NOT NULL;
Existe uma maneira rápida e suja com privilégios de superusuário:
UPDATE pg_attribute
SET attnotnull = FALSE
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0;
É apenas uma tabela temporária sem dados e sem outra finalidade, e é descartada no final da transação. Portanto, o atalho é tentador. Ainda assim, a regra básica é:nunca adultere diretamente os catálogos do sistema.
Então, vamos analisar uma maneira limpa :Automatize com SQL dinâmico em um
DO
demonstração. Você só precisa dos privilégios normais você tem a garantia de ter uma vez que a mesma função criou a tabela temporária. DO $$BEGIN
EXECUTE (
SELECT 'ALTER TABLE tmp_playlist_items ALTER '
|| string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
|| ' DROP NOT NULL'
FROM pg_catalog.pg_attribute
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0
);
END$$
Muito mais limpo e ainda muito rápido. Cuide-se com comandos dinâmicos e tenha cuidado com injeção de SQL. Esta afirmação é segura. Eu postei várias respostas relacionadas com mais explicações.
Solução geral (9.4 e anterior)
BEGIN;
CREATE TEMP TABLE tmp_playlist_items
(LIKE playlist_items INCLUDING DEFAULTS) ON COMMIT DROP;
DO $$BEGIN
EXECUTE (
SELECT 'ALTER TABLE tmp_playlist_items ALTER '
|| string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
|| ' DROP NOT NULL'
FROM pg_catalog.pg_attribute
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0
);
END$$;
LOCK TABLE playlist_items IN EXCLUSIVE MODE; -- forbid concurrent writes
WITH default_row AS (
INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *
)
, new_values (id, playlist, item, group_name, duration, sort, legacy) AS (
VALUES
(651, 21, 30012, 'a', 30, 1, FALSE)
, (NULL, 21, 1, 'b', 34, 2, NULL)
, (668, 21, 30012, 'c', 30, 3, FALSE)
, (7428, 21, 23068, 'd', 0, 4, FALSE)
)
, upsert AS ( -- *not* replacing existing values in UPDATE (?)
UPDATE playlist_items m
SET ( playlist, item, group_name, duration, sort, legacy)
= (n.playlist, n.item, n.group_name, n.duration, n.sort, n.legacy)
-- ..., COALESCE(n.legacy, m.legacy) -- see below
FROM new_values n
WHERE n.id = m.id
RETURNING m.id
)
INSERT INTO playlist_items
(playlist, item, group_name, duration, sort, legacy)
SELECT n.playlist, n.item, n.group_name, n.duration, n.sort
, COALESCE(n.legacy, d.legacy)
FROM new_values n, default_row d -- single row can be cross-joined
WHERE NOT EXISTS (SELECT 1 FROM upsert u WHERE u.id = n.id)
RETURNING id;
COMMIT;
Você só precisa do
LOCK
se você tiver transações simultâneas tentando gravar na mesma tabela. Conforme solicitado, isso substitui apenas valores NULL na coluna
legacy
nas linhas de entrada para o INSERT
caso. Pode ser facilmente estendido para trabalhar com outras colunas ou no UPDATE
caso também. Por exemplo, você pode UPDATE
condicionalmente também:somente se o valor de entrada for NOT NULL
. Eu adicionei uma linha comentada ao UPDATE
acima de. Além:Você não precisa conjurar valores em qualquer linha, exceto o primeiro em um
VALUES
expressão, uma vez que os tipos são derivados do primeiro fileira. Postgres 9.5
implementa UPSERT com
INSERT .. ON CONFLICT .. DO NOTHING | UPDATE
. Isso simplifica bastante a operação:INSERT INTO playlist_items AS m (id, playlist, item, group_name, duration, sort, legacy)
VALUES (651, 21, 30012, 'a', 30, 1, FALSE)
, (DEFAULT, 21, 1, 'b', 34, 2, DEFAULT) -- !
, (668, 21, 30012, 'c', 30, 3, FALSE)
, (7428, 21, 23068, 'd', 0, 4, FALSE)
ON CONFLICT (id) DO UPDATE
SET (playlist, item, group_name, duration, sort, legacy)
= (EXCLUDED.playlist, EXCLUDED.item, EXCLUDED.group_name
, EXCLUDED.duration, EXCLUDED.sort, EXCLUDED.legacy)
-- (..., COALESCE(l.legacy, EXCLUDED.legacy)) -- see below
RETURNING m.id;
Podemos anexar os
VALUES
cláusula para INSERT
diretamente, o que permite que o DEFAULT
palavra-chave. No caso de violações exclusivas em (id)
, as atualizações do Postgres em vez disso. Podemos usar linhas excluídas no UPDATE
. O manual:
OSET
eWHERE
cláusulas emON CONFLICT DO UPDATE
ter acesso à linha existente usando o nome da tabela (ou um alias) e às linhas propostas para inserção usando o especialexcluded
tabela.
E:
Observe que os efeitos de todos osBEFORE INSERT
por linha os gatilhos são refletidos nos valores excluídos, pois esses efeitos podem ter contribuído para que a linha fosse excluída da inserção.
Caixa de canto restante
Você tem várias opções para o
UPDATE
:Você pode ... - ... não atualiza:adicione um
WHERE
cláusula para oUPDATE
para gravar apenas nas linhas selecionadas. - ... atualizar apenas as colunas selecionadas.
- ... atualizar somente se a coluna for NULL no momento:
COALESCE(l.legacy, EXCLUDED.legacy)
- ... atualizar somente se o novo valor for
NOT NULL
:COALESCE(EXCLUDED.legacy, l.legacy)
Mas não há como discernir
DEFAULT
valores e valores realmente fornecidos no INSERT
. Apenas resultante EXCLUDED
linhas são visíveis. Se você precisar da distinção, volte para a solução anterior, onde você tem as duas à nossa disposição.