Como insiro uma linha que contém uma chave estrangeira?

54

Usando o PostgreSQL v9.1. Eu tenho as seguintes tabelas:

CREATE TABLE foo
(
    id BIGSERIAL     NOT NULL UNIQUE PRIMARY KEY,
    type VARCHAR(60) NOT NULL UNIQUE
);

CREATE TABLE bar
(
    id BIGSERIAL NOT NULL UNIQUE PRIMARY KEY,
    description VARCHAR(40) NOT NULL UNIQUE,
    foo_id BIGINT NOT NULL REFERENCES foo ON DELETE RESTRICT
);

Digamos que a primeira tabela fooseja preenchida assim:

INSERT INTO foo (type) VALUES
    ( 'red' ),
    ( 'green' ),
    ( 'blue' );

Existe alguma maneira de inserir linhas barfacilmente fazendo referência à footabela? Ou devo fazê-lo em duas etapas, primeiro pesquisando o footipo que desejo e, em seguida, inserindo uma nova linha bar?

Aqui está um exemplo de pseudo-código mostrando o que eu esperava que pudesse ser feito:

INSERT INTO bar (description, foo_id) VALUES
    ( 'testing',     SELECT id from foo WHERE type='blue' ),
    ( 'another row', SELECT id from foo WHERE type='red'  );
Stéphane
fonte

Respostas:

67

Sua sintaxe é quase boa, precisa de parênteses em torno das subconsultas e funcionará:

INSERT INTO bar (description, foo_id) VALUES
    ( 'testing',     (SELECT id from foo WHERE type='blue') ),
    ( 'another row', (SELECT id from foo WHERE type='red' ) );

Testado no SQL-Fiddle

Outra maneira, com sintaxe mais curta, se você tiver muitos valores para inserir:

WITH ins (description, type) AS
( VALUES
    ( 'more testing',   'blue') ,
    ( 'yet another row', 'green' )
)  
INSERT INTO bar
   (description, foo_id) 
SELECT 
    ins.description, foo.id
FROM 
  foo JOIN ins
    ON ins.type = foo.type ;
ypercubeᵀᴹ
fonte
Tomou a leitura algumas vezes, mas agora entendo a segunda solução que você forneceu. Eu gosto disso. Utilizando-o agora para inicializar meu banco de dados com alguns valores conhecidos quando o sistema é ativado pela primeira vez.
Stéphane
37

INSERT Simples

INSERT INTO bar (description, foo_id)
SELECT val.description, f.id
FROM  (
   VALUES
      (text 'testing', text 'blue')  -- explicit type declaration; see below
    , ('another row', 'red' )
    , ('new row1'   , 'purple')      -- purple does not exist in foo, yet
    , ('new row2'   , 'purple')
   ) val (description, type)
LEFT   JOIN foo f USING (type);
  • O uso de um em LEFT [OUTER] JOINvez de [INNER] JOINsignifica que as linhas de val não são descartadas quando nenhuma correspondência é encontrada foo. Em vez disso, NULLé inserido para foo_id.

  • A VALUESexpressão na subconsulta faz o mesmo que o CTE do @ ypercube . As expressões de tabela comum oferecem recursos adicionais e são mais fáceis de ler em grandes consultas, mas também representam barreiras de otimização. Portanto, as subconsultas geralmente são um pouco mais rápidas quando nenhuma das opções acima é necessária.

  • idcomo o nome da coluna é um anti-padrão generalizado. Deve ser foo_ide bar_idou qualquer coisa descritiva. Ao ingressar em várias tabelas, você acaba com várias colunas, todas nomeadas id...

  • Considere simples textou em varcharvez de varchar(n). Se você realmente precisar impor uma restrição de comprimento, adicione uma CHECKrestrição:

  • Pode ser necessário adicionar conversões de tipo explícitas. Como a VALUESexpressão não está diretamente anexada a uma tabela (como em INSERT ... VALUES ...), os tipos não podem ser derivados e os tipos de dados padrão são usados ​​sem declaração explícita de tipo, o que pode não funcionar em todos os casos. É o suficiente para fazê-lo na primeira linha, o resto ficará alinhado.

INSERIR linhas FK ausentes ao mesmo tempo

Se você deseja criar entradas inexistentes em tempo fooreal, em uma única instrução SQL , os CTEs são instrumentais:

WITH sel AS (
   SELECT val.description, val.type, f.id AS foo_id
   FROM  (
      VALUES
         (text 'testing', text 'blue')
       , ('another row', 'red'   )
       , ('new row1'   , 'purple')
       , ('new row2'   , 'purple')
      ) val (description, type)
   LEFT   JOIN foo f USING (type)
   )
, ins AS ( 
   INSERT INTO foo (type)
   SELECT DISTINCT type FROM sel WHERE foo_id IS NULL
   RETURNING id AS foo_id, type
   )
INSERT INTO bar (description, foo_id)
SELECT sel.description, COALESCE(sel.foo_id, ins.foo_id)
FROM   sel
LEFT   JOIN ins USING (type);

Observe as duas novas linhas fictícias a serem inseridas. Ambos são roxos , o que ainda não existe foo. Duas linhas para ilustrar a necessidade DISTINCTna primeira INSERTinstrução.

Explicação passo a passo

  1. O 1º CTE selfornece várias linhas de dados de entrada. A subconsulta valcom a VALUESexpressão pode ser substituída por uma tabela ou subconsulta como origem. Imediatamente LEFT JOINpara fooanexar as linhas foo_idpré-existentes type. Todas as outras linhas ficam foo_id IS NULLassim.

  2. O 2º CTE insinsere novos tipos distintos ( foo_id IS NULL) fooe retorna os recém-gerados foo_id- junto com o typepara se juntar novamente para inserir linhas.

  3. O exterior final INSERTagora pode inserir um foo.id para cada linha: o tipo pré-existente ou foi inserido na etapa 2.

Estritamente falando, as duas inserções acontecem "em paralelo", mas como essa é uma declaração única , as FOREIGN KEYrestrições padrão não irão reclamar. A integridade referencial é imposta no final da instrução por padrão.

SQL Fiddle para Postgres 9.3. (Funciona da mesma maneira em 9.1.)

Há uma pequena condição de corrida se você executar várias dessas consultas simultaneamente. Leia mais em perguntas relacionadas aqui e aqui e aqui . Realmente só acontece sob carga simultânea pesada, se é que alguma vez. Em comparação com soluções de cache como anunciadas em outra resposta, a chance é super pequena .

Função para uso repetido

Para uso repetido, eu criaria uma função SQL que pegasse uma matriz de registros como parâmetro e usasse unnest(param)no lugar da VALUESexpressão.

Ou, se a sintaxe para matrizes de registros estiver muito bagunçada para você, use uma string separada por vírgula como parâmetro _param. Por exemplo do formulário:

'description1,type1;description2,type2;description3,type3'

Em seguida, use isso para substituir a VALUESexpressão na instrução acima:

SELECT split_part(x, ',', 1) AS description
       split_part(x, ',', 2) AS type
FROM unnest(string_to_array(_param, ';')) x;


Função com UPSERT no Postgres 9.5

Crie um tipo de linha personalizado para a passagem de parâmetros. Poderíamos ficar sem ele, mas é mais simples:

CREATE TYPE foobar AS (description text, type text);

Função:

CREATE OR REPLACE FUNCTION f_insert_foobar(VARIADIC _val foobar[])
  RETURNS void AS
$func$
   WITH val AS (SELECT * FROM unnest(_val))    -- well-known row type
   ,    ins AS ( 
      INSERT INTO foo AS f (type)
      SELECT DISTINCT v.type                   -- DISTINCT!
      FROM   val v
      ON     CONFLICT(type) DO UPDATE          -- type already exists
      SET    type = excluded.type WHERE FALSE  -- never executed, but lock rows
      RETURNING f.type, f.id
      )
   INSERT INTO bar AS b (description, foo_id)
   SELECT v.description, COALESCE(f.id, i.id)  -- assuming most types pre-exist
   FROM        val v
   LEFT   JOIN foo f USING (type)              -- already existed
   LEFT   JOIN ins i USING (type)              -- newly inserted
   ON     CONFLICT (description) DO UPDATE     -- description already exists
   SET    foo_id = excluded.foo_id             -- real UPSERT this time
   WHERE  b.foo_id IS DISTINCT FROM excluded.foo_id  -- only if actually changed
$func$  LANGUAGE sql;

Ligar:

SELECT f_insert_foobar(
     '(testing,blue)'
   , '(another row,red)'
   , '(new row1,purple)'
   , '(new row2,purple)'
   , '("with,comma",green)'  -- added to demonstrate row syntax
   );

Rápido e sólido para ambientes com transações simultâneas.

Além das consultas acima, isso ...

  • ... aplica-se SELECTou INSERTativa foo: qualquer um typeque ainda não exista na tabela FK é inserido. Supondo que a maioria dos tipos preexista. Para ter certeza absoluta e descartar as condições de corrida, as linhas existentes de que precisamos são bloqueadas (para que transações simultâneas não possam interferir). Se isso for muito paranóico para o seu caso, você pode substituir:

      ON     CONFLICT(type) DO UPDATE          -- type already exists
      SET    type = excluded.type WHERE FALSE  -- never executed, but lock rows

    com

      ON     CONFLICT(type) DO NOTHING
  • ... aplica-se INSERTou UPDATE(verdadeiro "UPSERT") em bar: Se o descriptionjá existir, typeé atualizado:

      ON     CONFLICT (description) DO UPDATE     -- description already exists
      SET    foo_id = excluded.foo_id             -- real UPSERT this time
      WHERE  b.foo_id IS DISTINCT FROM excluded.foo_id  -- only if actually changed

    Mas somente se typerealmente mudar:

  • ... passa valores como tipos de linha conhecidos com um VARIADICparâmetro Observe o máximo padrão de 100 parâmetros! Comparar:

    Existem muitas outras maneiras de passar várias linhas ...

Palavras-chave:

Erwin Brandstetter
fonte
No seu INSERT missing FK rows at the same timeexemplo, colocar isso em uma transação reduziria o risco de condições de corrida no SQL Server?
element11 21/07
11
@ element11: A resposta é para o Postgres, mas como estamos falando de um único comando SQL, é uma transação única em qualquer caso. Executá-lo em uma transação maior apenas aumentaria a janela de tempo para possíveis condições de corrida. Quanto ao SQL Server: CTEs modificadores de dados não são suportados (apenas SELECTdentro de uma WITHcláusula). Fonte: documentação da MS.
Erwin Brandstetter
11
Você também pode fazer isso com INSERT ... RETURNING \gsetem psqlseguida, usar os valores retornados como psql :'variables', mas isso só funciona para inserções de linhas individuais.
Craig Ringer
@ErwinBrandstetter isso é ótimo, mas eu sou muito novo no sql para entender tudo, você pode adicionar alguns comentários a "INSERIR linhas ausentes do FK ao mesmo tempo" explicando como funciona? Além disso, obrigado pelos exemplos de trabalho do SQLFiddle!
glallen
@ Glallen: eu adicionei uma explicação passo a passo. Há também muitos links para respostas relacionadas e o manual com mais explicações. Você precisa entender o que a consulta faz ou você pode estar pensando demais.
Erwin Brandstetter
4

Olho para cima. Você basicamente precisa dos IDs de foo para inseri-los na barra.

Não é específico do postgres, btw. (e você não marcou dessa maneira) - geralmente é assim que o SQL funciona. Não há atalhos aqui.

No que diz respeito à aplicação, você pode ter um cache de itens foo na memória. Minhas tabelas geralmente têm até 3 campos exclusivos:

  • ID (inteiro ou algo assim) que é a chave primária no nível da tabela.
  • Identificador, que é um GUID usado como nível de aplicativo de ID estável (e pode ser exposto ao cliente nos URLs etc.)
  • Código - uma string que pode estar lá e deve ser única, se existir (sql server: índice exclusivo filtrado, não nulo). Esse é um identificador de conjunto do cliente.

Exemplo:

  • Conta (em um aplicativo de negociação) -> Id é um int usado para chaves estrangeiras. -> Identifier é um Guid e é usado nos portais da web etc. - sempre aceito. -> O código é definido manualmente. Regra: uma vez definido, ele não muda.

Obviamente, quando você deseja vincular algo a uma conta - primeiro é necessário, tecnicamente, obter o ID - mas, dado que o Identificador e o Código nunca mudam quando estão lá, um cache positivo na memória pode impedir que a maioria das pesquisas atinja o banco de dados.

TomTom
fonte
10
Você está ciente de que pode deixar o RDBMS fazer a pesquisa para você, em uma única instrução SQL, evitando o cache propenso a erros?
Erwin Brandstetter
Você está ciente de que procurar elementos que não mudam não é propenso a erros? Além disso, normalmente, o RDBMS não é escalável e o elemento mais caro no jogo, devido aos custos de licenciamento. Tirar o máximo de carga possível não é exatamente ruim. Além disso, muitos ORMs não suportam isso para começar.
TomTom
14
Elementos que não mudam? Elemento mais caro? Custos de licenciamento (para PostgreSQL)? ORMs definindo o que é são? Não, eu não estava ciente de tudo isso.
precisa