PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Gerar valores DEFAULT em um CTE UPSERT usando PostgreSQL 9.3


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 um VALUES lista que aparece no nível superior de um INSERT , uma expressão pode ser substituída por DEFAULT para indicar que o valor padrão da coluna de destino deve ser inserido. DEFAULT não pode ser usado quando VALUES 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 ou IMMUTABLE expressões padrão . A maioria VOLATILE funções funcionarão tão bem, mas não há garantias. O current_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 em serial 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 em serial colunas diretamente. Esses não devem ser listados em INSERT nenhuma instrução.
    Falha restante para serial 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 em serial colunas.

Mais dois problemas podem ser resolvidos:

  • Se você tiver colunas definidas NOT NULL , você deve inserir valores fictícios e substituir por NULL 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 gatilhos ON 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:

O SET e WHERE cláusulas em ON CONFLICT DO UPDATE ter acesso à linha existente usando o nome da tabela (ou um alias) e às linhas propostas para inserção usando o especial excluded tabela.

E:

Observe que os efeitos de todos os BEFORE 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 o UPDATE 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.