Wie füge ich eine Zeile ein, die einen Fremdschlüssel enthält?

54

Verwendung von PostgreSQL v9.1. Ich habe folgende Tabellen:

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

Angenommen, die erste Tabelle foowird wie folgt gefüllt:

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

Gibt es eine Möglichkeit, Zeilen bareinfach durch Verweisen auf die fooTabelle einzufügen ? Oder muss ich das in zwei Schritten tun, indem fooich zuerst den gewünschten Typ suche und dann eine neue Zeile einfüge bar?

Hier ist ein Beispiel eines Pseudocodes, der zeigt, was ich mir erhofft habe:

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
quelle

Antworten:

67

Ihre Syntax ist fast gut, benötigt einige Klammern um die Unterabfragen und es wird funktionieren:

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

Getestet bei SQL-Fiddle

Eine andere Möglichkeit mit kürzerer Syntax, wenn Sie viele Werte einfügen müssen:

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ᵀᴹ
quelle
Ich habe es ein paar Mal gelesen, aber jetzt verstehe ich die zweite Lösung, die Sie bereitgestellt haben. Ich mag das. Jetzt verwenden, um meine Datenbank mit einer Handvoll bekannter Werte zu booten, wenn das System zum ersten Mal gestartet wird.
Stéphane
37

Einfaches INSERT

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);
  • Die Verwendung von a LEFT [OUTER] JOINanstelle von [INNER] JOINbedeutet, dass Zeilen von val nicht gelöscht werden, wenn in keine Übereinstimmung gefunden wird foo. Stattdessen NULLwird für eingegeben foo_id.

  • Der VALUESAusdruck in der Unterabfrage entspricht dem CTE von @ ypercube . Gängige Tabellenausdrücke bieten zusätzliche Funktionen und sind in großen Abfragen leichter zu lesen, sie stellen jedoch auch ein Optimierungshindernis dar. Daher sind Unterabfragen in der Regel etwas schneller, wenn keine der oben genannten Anforderungen erfüllt ist.

  • idals Spaltenname dient ein weit verbreitetes Anti-Pattern. Sollte foo_idund bar_idoder etwas beschreibend sein. Wenn Sie eine Reihe von Tabellen verbinden, erhalten Sie am Ende mehrere Spalten mit dem Namen id...

  • Betrachten Sie einfach textoder varcharstatt varchar(n). Wenn Sie wirklich eine Längenbeschränkung festlegen müssen, fügen Sie eine CHECKEinschränkung hinzu:

  • Möglicherweise müssen Sie explizite Typumwandlungen hinzufügen. Da der VALUESAusdruck nicht direkt an eine Tabelle angehängt ist (wie in INSERT ... VALUES ...), können keine Typen abgeleitet und Standarddatentypen ohne explizite Typdeklaration verwendet werden, was möglicherweise nicht in allen Fällen funktioniert. Es ist genug, um es in der ersten Reihe zu tun, der Rest wird in die Schlange fallen.

INSERT fügt gleichzeitig fehlende FK-Zeilen ein

Wenn Sie nicht vorhandene Einträge im laufenden Betrieb erstellen möchten, sind CTEs fooin einer einzelnen SQL-Anweisung von entscheidender Bedeutung:

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

Beachten Sie die zwei neuen einzufügenden Platzhalterzeilen. Beide sind lila , was es in foonoch nicht gibt . Zwei Zeilen zur Veranschaulichung der Notwendigkeit DISTINCTin der ersten INSERTAnweisung.

Schritt für Schritt Erklärung

  1. Der 1. CTE selstellt mehrere Zeilen von Eingabedaten bereit. Die Unterabfrage valmit dem VALUESAusdruck kann durch eine Tabelle oder Unterabfrage als Quelle ersetzt werden. Sofort LEFT JOIN, fooum die foo_idfür bereits vorhandene typeZeilen anzufügen. Alle anderen Zeilen erhalten auf foo_id IS NULLdiese Weise.

  2. Der 2. CTE insfügt verschiedene neue Typen ( foo_id IS NULL) ein foound gibt den neu generierten foo_idzusammen mit dem typezurück, um Zeilen einzufügen.

  3. Das letzte äußere INSERTkann jetzt eine foo.id für jede Zeile einfügen: Entweder der Typ, der bereits vorhanden war, oder er wurde in Schritt 2 eingefügt.

Genau genommen geschehen beide Einfügungen "parallel", aber da dies eine einzelne Anweisung ist, werden sich Standardeinschränkungen FOREIGN KEYnicht beschweren. Die referenzielle Integrität wird standardmäßig am Ende der Anweisung erzwungen.

SQL Fiddle für Postgres 9.3. (Funktioniert genauso in 9.1.)

Es ist eine winzige Race - Bedingung , wenn Sie mehrere dieser Abfragen gleichzeitig ausgeführt werden . Lesen Sie mehr unter verwandten Fragen hier und hier und hier . Wirklich nur unter starker gleichzeitiger Belastung, wenn überhaupt. Im Vergleich zu Caching-Lösungen, wie sie in einer anderen Antwort angekündigt wurden, ist die Chance winzig .

Funktion für den wiederholten Gebrauch

Für die wiederholte Verwendung würde ich eine SQL-Funktion erstellen, die ein Array von Datensätzen als Parameter verwendet und unnest(param)anstelle des VALUESAusdrucks verwendet.

Wenn Ihnen die Syntax für Arrays von Datensätzen zu unübersichtlich ist, verwenden Sie eine durch Kommas getrennte Zeichenfolge als Parameter _param. Zum Beispiel des Formulars:

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

Verwenden Sie dann diesen Befehl, um den VALUESAusdruck in der obigen Anweisung zu ersetzen :

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


Funktion mit UPSERT in Postgres 9.5

Erstellen Sie einen benutzerdefinierten Zeilentyp für die Parameterübergabe. Wir könnten darauf verzichten, aber es ist einfacher:

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

Funktion:

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;

Anruf:

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

Schnell und absolut zuverlässig für Umgebungen mit gleichzeitigen Transaktionen.

Zusätzlich zu den obigen Abfragen ...

  • ... trifft zu SELECToder INSERTein foo: Alle type, die noch nicht in der FK-Tabelle vorhanden sind, werden eingefügt. Vorausgesetzt, die meisten Typen existieren bereits. Um absolut sicher zu sein und Rennbedingungen auszuschließen, werden vorhandene Zeilen, die wir benötigen, gesperrt (damit gleichzeitige Transaktionen nicht stören können). Wenn das für Ihren Fall zu paranoid ist, können Sie Folgendes ersetzen:

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

    mit

      ON     CONFLICT(type) DO NOTHING
  • ... gilt INSERToder UPDATE(wahres "UPSERT") am bar: Wenn das descriptionbereits existiert, typewird es aktualisiert:

      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

    Aber nur wenn sich typetatsächlich ändert:

  • ... übergibt Werte als bekannte Zeilentypen mit einem VARIADICParameter. Beachten Sie das Standardmaximum von 100 Parametern! Vergleichen Sie:

    Es gibt viele andere Möglichkeiten, mehrere Zeilen zu übergeben ...

Verbunden:

Erwin Brandstetter
quelle
INSERT missing FK rows at the same timeWürde das Einfügen dieser Option in eine Transaktion in Ihrem Beispiel das Risiko von Racebedingungen in SQL Server verringern?
Element11
1
@ element11: Die Antwort ist für Postgres, aber da es sich um einen einzelnen SQL-Befehl handelt, handelt es sich auf jeden Fall um eine einzelne Transaktion. Die Ausführung es sich in einer größeren Transaktion würde nur erhöhen das Zeitfenster für mögliche Rennbedingungen. Wie bei SQL Server: Datenmodifizierende CTEs werden überhaupt nicht unterstützt (nur SELECTinnerhalb einer WITHKlausel). Quelle: MS-Dokumentation.
Erwin Brandstetter
1
Sie können dies auch mit INSERT ... RETURNING \gsetin tun psql, indem Sie die zurückgegebenen Werte als psql verwenden :'variables', dies funktioniert jedoch nur bei Einfügungen in einzelne Zeilen.
Craig Ringer
@ErwinBrandstetter das ist großartig, aber ich bin zu neu in SQL, um alles zu verstehen. Könnten Sie ein paar Kommentare zu "INSERT missing FK rows at same time" hinzufügen, um zu erklären, wie es funktioniert? Vielen Dank auch für die SQLFiddle-Arbeitsbeispiele!
Glallen
@glallen: Ich habe eine schrittweise Erklärung hinzugefügt. Es gibt auch viele Links zu verwandten Antworten und dem Handbuch mit weiteren Erklärungen. Sie müssen verstehen, was die Abfrage bewirkt, oder Sie sind möglicherweise überfordert.
Erwin Brandstetter
4

Sieh nach oben. Sie benötigen im Allgemeinen die foo Identifikation, um sie in Stab einzufügen.

Nicht postgres-spezifisch, übrigens. (und Sie haben es nicht so getaggt) - so funktioniert SQL im Allgemeinen. Keine Abkürzungen hier.

In Bezug auf die Anwendung haben Sie möglicherweise einen Cache mit foo Elementen im Speicher. Meine Tabellen enthalten häufig bis zu 3 eindeutige Felder:

  • ID (Ganzzahl oder so), die der Primärschlüssel auf Tabellenebene ist.
  • Kennung, die eine GUID ist, die als stabile ID auf Anwendungsebene verwendet wird (und in URLs usw. dem Kunden zur Verfügung gestellt werden kann).
  • Code - Eine Zeichenfolge, die möglicherweise vorhanden ist und eindeutig sein muss, wenn sie vorhanden ist (SQL Server: Gefilterter eindeutiger Index ungleich Null). Das ist eine Kundensetkennung.

Beispiel:

  • Konto (in einer Handelsanwendung) -> ID ist ein int, das für Fremdschlüssel verwendet wird. -> Identifier ist ein Guid und wird in den Webportalen etc. verwendet - immer akzeptiert. -> Code wird manuell eingestellt. Regel: Einmal eingestellt, ändert sich nichts.

Wenn Sie etwas mit einem Konto verknüpfen möchten, müssen Sie zunächst die ID abrufen. Wenn sich jedoch sowohl der Bezeichner als auch der Code ändern, kann ein positiver Cache im Speicher die meisten Suchvorgänge daran hindern, auf die Datenbank zuzugreifen.

TomTom
quelle
10
Sie wissen, dass Sie das RDBMS in einer einzigen SQL-Anweisung die Suche für Sie durchführen lassen können, um fehleranfälligen Cache zu vermeiden?
Erwin Brandstetter
Sie wissen, dass das Nachschlagen von sich nicht ändernden Elementen nicht fehleranfällig ist? Außerdem ist das RDBMS aufgrund der Lizenzkosten normalerweise nicht skalierbar und das teuerste Element im Spiel. Es ist nicht gerade schlecht, so viel Last wie möglich davon zu nehmen. Auch nicht viele ORMs unterstützen dies von Anfang an.
TomTom
14
Gleichbleibende Elemente? Das teuerste Element? Lizenzkosten (für PostgreSQL)? ORMs, die definieren, was vernünftig ist? Nein, mir war das alles nicht bewusst.
Erwin Brandstetter