Come inserisco una riga che contiene una chiave esterna?


54

Utilizzando PostgreSQL v9.1. Ho le seguenti tabelle:

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
);

Supponiamo che la prima tabella foosia popolata in questo modo:

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

Esiste un modo per inserire barfacilmente le righe facendo riferimento alla footabella? O devo farlo in due passaggi, prima cercando il footipo desiderato e poi inserendo una nuova riga bar?

Ecco un esempio di pseudo-codice che mostra ciò che speravo potesse essere fatto:

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

Risposte:


67

La tua sintassi è quasi buona, ha bisogno di alcune parentesi attorno alle sottoquery e funzionerà:

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

Testato su SQL-Fiddle

Un altro modo, con una sintassi più breve se hai molti valori da inserire:

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 ;

Ho preso a leggerlo alcune volte, ma ora capisco quella seconda soluzione che hai fornito. Mi piace. Usalo ora per avviare il mio database con una manciata di valori noti al primo avvio del sistema.
Stéphane,

37

INSERTO semplice

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);
  • L'uso di un LEFT [OUTER] JOINinvece di [INNER] JOINsignifica che le righe da val non vengono eliminate quando non viene trovata alcuna corrispondenza foo. Invece, NULLè inserito per foo_id.

  • L' VALUESespressione nella sottoquery fa lo stesso del CTE di @ ypercube . Le espressioni di tabella comuni offrono funzionalità aggiuntive e sono più facili da leggere in query di grandi dimensioni, ma rappresentano anche barriere di ottimizzazione. Quindi le subquery sono in genere un po 'più veloci quando nessuna delle precedenti è necessaria.

  • idcome nome della colonna è un anti-pattern diffuso. Dovrebbe essere foo_ide bar_ido niente descrittivo. Quando si unisce un gruppo di tabelle, si finisce con più colonne tutte denominate id...

  • Considerare semplice texto varcharinvece di varchar(n). Se devi davvero imporre una limitazione di lunghezza, aggiungi un CHECKvincolo:

  • Potrebbe essere necessario aggiungere cast di tipo esplicito. Poiché l' VALUESespressione non è direttamente collegata a una tabella (come in INSERT ... VALUES ...), i tipi non possono essere derivati ​​e i tipi di dati predefiniti vengono utilizzati senza una dichiarazione di tipo esplicita, che potrebbe non funzionare in tutti i casi. Basta farlo nella prima fila, il resto andrà in linea.

INSERISCI le righe FK mancanti contemporaneamente

Se si desidera creare voci inesistenti fooal volo, in una singola istruzione SQL , i CTE sono strumentali:

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);

Nota le due nuove righe fittizie da inserire. Entrambi sono viola , che non esiste fooancora. Due righe per illustrare la necessità DISTINCTnella prima INSERTistruzione.

Spiegazione dettagliata

  1. Il 1 ° CTE selfornisce file multiple di dati di input. La sottoquery valcon l' VALUESespressione può essere sostituita con una tabella o una sottoquery come origine. Immediatamente LEFT JOINper fooaggiungere le righe foo_idpreesistenti type. Tutte le altre righe arrivano in foo_id IS NULLquesto modo.

  2. Il 2 ° CTE insinserisce nuovi tipi distinti ( foo_id IS NULL) in foo, e restituisce i nuovi generati foo_id, insieme a typeper unire nuovamente per inserire le righe.

  3. L'esterno finale INSERTora può inserire un foo.id per ogni riga: o il tipo preesistente o è stato inserito nel passaggio 2.

A rigor di termini, entrambi gli inserti si verificano "in parallelo", ma poiché si tratta di una singola istruzione, i FOREIGN KEYvincoli predefiniti non si lamentano. L'integrità referenziale viene applicata alla fine dell'istruzione per impostazione predefinita.

SQL Fiddle for Postgres 9.3. (Funziona allo stesso modo in 9.1.)

C'è una minuscola condizione di competizione se si eseguono più di queste query contemporaneamente. Leggi di più sotto le domande correlate qui e qui e qui . Succede davvero solo con un carico simultaneo pesante, se mai. In confronto a caching soluzioni come pubblicizzato in un'altra risposta, la probabilità è super-piccola .

Funzione per uso ripetuto

Per un uso ripetuto, creerei una funzione SQL che accetta un array di record come parametro e lo usa unnest(param)al posto VALUESdell'espressione.

In alternativa, se la sintassi per le matrici di record è troppo complicata per te, utilizzare una stringa separata da virgola come parametro _param. Ad esempio del modulo:

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

Quindi utilizzare questo per sostituire l' VALUESespressione nell'istruzione precedente:

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


Funzione con UPSERT in Postgres 9.5

Crea un tipo di riga personalizzato per il passaggio dei parametri. Potremmo farne a meno, ma è più semplice:

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

Funzione:

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;

Chiamata:

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

Veloce e solido per ambienti con transazioni simultanee.

Oltre alle query sopra, questo ...

  • ... si applica SELECTo INSERTsu foo: typeviene inserito qualsiasi elemento che non esiste nella tabella FK. Supponendo che la maggior parte dei tipi preesistano. Per essere assolutamente sicuri ed escludere le condizioni di gara, le file esistenti di cui abbiamo bisogno sono bloccate (in modo che le transazioni simultanee non possano interferire). Se è troppo paranoico per il tuo caso, puoi sostituire:

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

    con

      ON     CONFLICT(type) DO NOTHING
  • ... si applica INSERTo UPDATE(vero "UPSERT") su bar: Se descriptionesiste già, typeviene aggiornato:

      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

    Ma solo se typeeffettivamente cambia:

  • ... passa valori anche a tipi di riga noti con un VARIADICparametro. Nota il massimo predefinito di 100 parametri! Confrontare:

    Esistono molti altri modi per passare più righe ...

Relazionato:


Nel tuo INSERT missing FK rows at the same timeesempio, mettere questo in una transazione ridurrebbe il rischio di condizioni di competizione in SQL Server?
element11

1
@ element11: la risposta è per Postgres, ma dato che stiamo parlando di un singolo comando SQL, è comunque una singola transazione. Eseguirlo all'interno di una transazione più grande non farebbe che aumentare la finestra temporale per possibili condizioni di gara. Come per SQL Server: i CTE che modificano i dati non sono affatto supportati (solo SELECTall'interno di una WITHclausola). Fonte: documentazione MS.
Erwin Brandstetter,

1
Puoi anche farlo con INSERT ... RETURNING \gsetin psqlpoi usa i valori restituiti come psql :'variables', ma questo funziona solo per inserimenti a riga singola.
Craig Ringer,

@ErwinBrandstetter è fantastico, ma sono troppo nuovo per sql per capirlo tutto, potresti aggiungere alcuni commenti a "INSERISCI le righe FK mancanti allo stesso tempo" spiegando come funziona? inoltre, grazie per gli esempi di lavoro di SQLFiddle!
glallen,

@glallen: ho aggiunto una spiegazione dettagliata. Ci sono anche molti collegamenti a risposte correlate e al manuale con ulteriori spiegazioni. È necessario capire che cosa la query fa o si può essere in sopra la vostra testa.
Erwin Brandstetter,

4

Consultare. Fondamentalmente hai bisogno degli id ​​foo per inserirli nella barra.

Postgres non specifico, a proposito. (e non l'hai taggato così) - questo è generalmente il modo in cui funziona SQL. Nessuna scorciatoia qui.

Per quanto riguarda l'applicazione, tuttavia, potresti avere una cache di elementi foo in memoria. I miei tavoli hanno spesso fino a 3 campi univoci:

  • Id (numero intero o qualcosa) che è la chiave primaria a livello di tabella.
  • Identificatore, che è un GUID utilizzato come applicazione ID stabile a livello di applicazione (e che può essere esposto al cliente negli URL, ecc.)
  • Codice - una stringa che può essere presente e deve essere unica se presente (server sql: indice univoco filtrato su non null). Questo è un identificatore di set cliente.

Esempio:

  • Conto (in un'applicazione di trading) -> ID è un int usato per le chiavi esterne. -> L'identificatore è un Guid e utilizzato nei portali web ecc. - Sempre accettato. -> Il codice viene impostato manualmente. Regola: una volta impostata non cambia.

Ovviamente quando vuoi collegare qualcosa a un account - prima devi tecnicamente ottenere l'ID - ma dato che sia l'identificatore che il codice non cambiano mai una volta che ci sono, una cache positiva nella memoria kan impedisce alla maggior parte delle ricerche di colpire il database.


10
Sei consapevole di poter consentire a RDBMS di eseguire la ricerca per te, in una singola istruzione SQL, evitando cache soggetta a errori?
Erwin Brandstetter,

Sei consapevole del fatto che cercare elementi che non cambiano non è soggetto a errori? Inoltre, in genere, RDBMS non è scalabile e l'elemento più costoso del gioco, a causa dei costi di licenza. Prendere il maggior carico possibile non è esattamente male. Inoltre, non molti ORM supportano questo per cominciare.
TomTom,

14
Elementi che non cambiano? Elemento più costoso? Costi di licenza (per PostgreSQL)? Gli ORM che definiscono ciò che è sano? No, non ero a conoscenza di tutto ciò.
Erwin Brandstetter,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.