web-dev-qa-db-fra.com

Comment insérer une ligne contenant une clé étrangère?

Utilisation de PostgreSQL v9.1. J'ai les tableaux suivants:

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

Disons que la première table foo est remplie comme ceci:

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

Existe-t-il un moyen d'insérer facilement des lignes dans bar en référençant la table foo? Ou dois-je le faire en deux étapes, d'abord en recherchant le type foo que je veux, puis en insérant une nouvelle ligne dans bar?

Voici un exemple de pseudo-code montrant ce que j'espérais pouvoir faire:

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

Votre syntaxe est presque bonne, a besoin de parenthèses autour des sous-requêtes et cela fonctionnera:

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

Testé à SQL-Fiddle

Une autre façon, avec une syntaxe plus courte si vous avez beaucoup de valeurs à insérer:

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ᵀᴹ

INSERT simple

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'utilisation d'un LEFT [OUTER] JOIN Au lieu de [INNER] JOIN Signifie que les lignes de val ne sont pas supprimées lorsqu'aucune correspondance n'est trouvée dans foo. Au lieu de cela, NULL est entré pour foo_id.

  • L'expression VALUES dans la sous-requête fait la même chose que @ ypercube's CTE. Common Table Expressions offrent des fonctionnalités supplémentaires et sont plus faciles à lire dans les grandes requêtes, mais elles posent également des barrières d'optimisation. Les sous-requêtes sont donc généralement un peu plus rapides lorsqu'aucune des réponses ci-dessus n'est nécessaire.

  • id comme nom de colonne est un anti-motif largement répandu. Doit être foo_id Et bar_id Ou quoi que ce soit de descriptif. Lorsque vous rejoignez un groupe de tables, vous vous retrouvez avec plusieurs colonnes toutes nommées id ...

  • Considérez simplement text ou varchar au lieu de varchar(n). Si vous avez vraiment besoin d'imposer une restriction de longueur, ajoutez une contrainte CHECK:

  • Vous devrez peut-être ajouter des transtypages de type explicites. Étant donné que l'expression VALUES n'est pas directement attachée à une table (comme dans INSERT ... VALUES ...), Les types ne peuvent pas être dérivés et les types de données par défaut sont utilisés sans déclaration de type explicite, ce qui peut ne pas fonctionner dans tous les cas. Il suffit de le faire au premier rang, le reste suivra.

INSÉRER les lignes FK manquantes en même temps

Si vous souhaitez créer des entrées inexistantes dans foo à la volée, dans une instruction SQL unique, les CTE sont essentiels:

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

Notez les deux nouvelles lignes factices à insérer. Les deux sont violet, qui n'existe pas encore dans foo. Deux lignes pour illustrer la nécessité de DISTINCT dans la première instruction INSERT.

Explication étape par étape

  1. Le 1er CTE sel fournit plusieurs lignes de données d'entrée. La sous-requête val avec l'expression VALUES peut être remplacée par une table ou une sous-requête comme source. Immédiatement LEFT JOIN À foo pour ajouter le foo_id Aux lignes type préexistantes. Toutes les autres lignes obtiennent foo_id IS NULL De cette façon.

  2. Le 2ème CTE ins insère distinct de nouveaux types (foo_id IS NULL) Dans foo, et retourne le foo_id Nouvellement généré - avec le type pour rejoindre à nouveau pour insérer des lignes.

  3. Le dernier INSERT externe peut maintenant insérer un foo.id pour chaque ligne: soit le type préexistait, soit il a été inséré à l'étape 2.

À strictement parler, les deux insertions se produisent "en parallèle", mais comme il s'agit d'une instruction single, les contraintes par défaut FOREIGN KEY Ne se plaindront pas. L'intégrité référentielle est appliquée à la fin de l'instruction par défaut.

SQL Fiddle pour Postgres 9.3. (Fonctionne de la même manière dans 9.1.)

Il existe une condition de concurrence minuscule si vous exécutez plusieurs de ces requêtes simultanément. En savoir plus sous les questions connexes ici et ici et ici . Ne se produit vraiment que sous une charge simultanée lourde, voire jamais. Par rapport aux solutions de mise en cache comme annoncé dans une autre réponse, la chance est super-minuscule.

Fonction pour une utilisation répétée

Pour une utilisation répétée, je créerais une fonction SQL qui prend un tableau d'enregistrements comme paramètre et utiliser unnest(param) à la place de l'expression VALUES.

Ou, si la syntaxe des tableaux d'enregistrements est trop compliquée pour vous, utilisez une chaîne séparée par des virgules comme paramètre _param. Par exemple du formulaire:

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

Utilisez ensuite ceci pour remplacer l'expression VALUES dans l'instruction ci-dessus:

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


Fonction avec UPSERT dans Postgres 9.5

Créez un type de ligne personnalisé pour le passage des paramètres. On pourrait s'en passer, mais c'est plus simple:

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

Une fonction:

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;

Appel:

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

Rapide et solide pour les environnements avec des transactions simultanées.

En plus des requêtes ci-dessus, cela ...

  • ... applique SELECT ou INSERT sur foo: tout type qui n'existe pas encore dans la table FK est inséré. En supposant que la plupart des types préexistent. Pour être absolument sûr et exclure les conditions de concurrence, les lignes existantes dont nous avons besoin sont verrouillées (afin que les transactions simultanées ne puissent pas interférer). Si c'est trop paranoïaque pour votre cas, vous pouvez remplacer:

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

    avec

      ON     CONFLICT(type) DO NOTHING
    
  • ... applique INSERT ou UPDATE (vrai "UPSERT") sur bar: Si le description existe déjà, son type est mis à jour :

      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
    

    Mais seulement si type change réellement:

  • ... transmet des valeurs comme des types de lignes bien connus avec un paramètre VARIADIC. Notez le maximum par défaut de 100 paramètres! Comparer:

    Il existe de nombreuses autres façons de passer plusieurs lignes ...

En relation:

45
Erwin Brandstetter

Chercher. Vous avez essentiellement besoin des identifiants foo pour les insérer dans la barre.

Pas spécifique aux postgres, au fait. (et vous ne l'avez pas marqué comme ça) - c'est généralement ainsi que fonctionne SQL. Pas de raccourcis ici.

En ce qui concerne les applications, cependant, vous pouvez avoir un cache d'éléments foo en mémoire. Mes tables ont souvent jusqu'à 3 champs uniques:

  • Id (entier ou quelque chose) qui est la clé primaire au niveau de la table.
  • Identifiant, qui est un GUID qui est utilisé en tant que niveau d'application d'ID stable (et peut être exposé au client dans les URL, etc.)
  • Code - une chaîne qui peut être présente et doit être unique si elle existe (serveur SQL: index unique filtré sur non nul). Il s'agit d'un identifiant d'ensemble client.

Exemple:

  • Compte (dans une application de trading) -> Id est un entier utilisé pour les clés étrangères. -> L'identifiant est un guide et utilisé dans les portails Web, etc. - toujours accepté. -> Le code est défini manuellement. Règle: une fois réglée, elle ne change pas.

Évidemment, lorsque vous souhaitez lier quelque chose à un compte - vous devez d'abord, techniquement, obtenir l'ID - mais étant donné que l'identifiant et le code ne changent jamais une fois qu'ils sont là, un cache positif en mémoire peut empêcher la plupart des recherches de toucher la base de données.

5
TomTom