it-swarm.com.de

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

Verwenden 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 foo wird folgendermaßen gefüllt:

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

Gibt es eine Möglichkeit, Zeilen einfach in bar einzufügen, indem auf die Tabelle foo verwiesen wird? Oder muss ich es in zwei Schritten tun, indem ich zuerst den gewünschten Typ foo nachschlage und dann eine neue Zeile in bar einfüge?

Hier ist ein Beispiel für Pseudocode, das zeigt, was ich mir erhofft hatte:

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

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 ;
74
ypercubeᵀᴹ

Einfach EINFÜGEN

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 eines LEFT [OUTER] JOIN Anstelle von [INNER] JOIN Bedeutet, dass Zeilen aus val nicht gelöscht werden wenn in foo keine Übereinstimmung gefunden wird. Stattdessen wird NULL für foo_id Eingegeben.

  • Der Ausdruck VALUES in der Unterabfrage entspricht dem von @ ypercube's CTE. Common Table Expressions bieten zusätzliche Funktionen und sind bei großen Abfragen leichter zu lesen, stellen aber auch Optimierungsbarrieren dar. Daher sind Unterabfragen in der Regel etwas schneller, wenn keine der oben genannten Anforderungen erfüllt ist.

  • id als Spaltenname ist ein weit verbreitetes Anti-Pattern. Sollte foo_id Und bar_id Oder etwas Beschreibendes sein. Wenn Sie eine Reihe von Tabellen verbinden, erhalten Sie mehrere Spalten mit dem Namen id ...

  • Betrachten Sie einfach text oder varchar anstelle von varchar(n). Wenn Sie wirklich eine Längenbeschränkung auferlegen müssen, fügen Sie eine CHECK -Einschränkung hinzu:

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

Fügen Sie gleichzeitig fehlende FK-Zeilen ein

Wenn Sie nicht vorhandene Einträge in foo im laufenden Betrieb in einer einzelnen SQL-Anweisung erstellen möchten, sind CTEs 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 Dummy-Zeilen, die eingefügt werden sollen. Beide sind lila, was in foo noch nicht existiert. Zwei Zeilen, um die Notwendigkeit von DISTINCT in der ersten INSERT -Anweisung zu veranschaulichen.

Schritt für Schritt Erklärung

  1. Der 1. CTE sel liefert mehrere Zeilen mit Eingabedaten. Die Unterabfrage val mit dem Ausdruck VALUES kann durch eine Tabelle oder Unterabfrage als Quelle ersetzt werden. Sofort LEFT JOIN An foo, um die foo_id Für bereits vorhandene type Zeilen anzuhängen. Alle anderen Zeilen erhalten auf diese Weise foo_id IS NULL.

  2. Der 2. CTE ins fügt different neue Typen (foo_id IS NULL) In foo ein und gibt den neu generierten foo_id - zurück. zusammen mit dem type, um sich wieder anzuschließen und Zeilen einzufügen.

  3. Das letzte äußere INSERT kann jetzt für jede Zeile eine foo.id einfügen: entweder der bereits vorhandene Typ oder er wurde in Schritt 2 eingefügt.

Genau genommen erfolgen beide Einfügungen "parallel", aber da dies eine single Anweisung ist, beschweren sich die Standardbeschränkungen FOREIGN KEY Nicht. 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 gibt eine winzige Rennbedingung , wenn Sie mehrere dieser Abfragen gleichzeitig ausführen. 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 super-winzig.

Funktion zur wiederholten Verwendung

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 Ausdrucks VALUES 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 dies dann, um den Ausdruck VALUES 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 solide für Umgebungen mit gleichzeitigen Transaktionen.

Zusätzlich zu den obigen Fragen ...

  • ... wendet SELECT oder INSERT auf foo an: Jedes type, das noch nicht in der FK-Tabelle vorhanden ist, wird eingefügt. Angenommen, die meisten Typen existieren bereits. Um absolut sicher zu sein und Rennbedingungen auszuschließen, sind 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
    
  • ... wendet INSERT oder UPDATE (true "UPSERT") auf bar an: Wenn das description bereits vorhanden ist, wird sein type 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 type tatsächlich ändert:

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

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

Verbunden:

45

Nachsehen. Grundsätzlich benötigen Sie die Foo-IDs, um sie in die Leiste einzufügen.

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

In Bezug auf die Anwendung befindet sich möglicherweise ein Cache mit Foo-Elementen im Speicher. Meine Tabellen haben oft bis zu 3 eindeutige Felder:

  • ID (Ganzzahl oder so), die der Primärschlüssel auf Tabellenebene ist.
  • Bezeichner, bei dem es sich um eine GUID] handelt, die als stabile ID-Anwendungsebene verwendet wird (und dem Kunden in URLs usw. 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 auf nicht null). Das ist eine Kundensatzkennung.

Beispiel:

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

Wenn Sie etwas mit einem Konto verknüpfen möchten - Sie müssen zunächst technisch die ID abrufen -, aber wenn sich sowohl die Kennung als auch der Code nie ändern, sobald sie vorhanden sind, kann ein positiver Cache im Speicher verhindern, dass die meisten Suchvorgänge die Datenbank erreichen.

5
TomTom