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

54

Utilisation de PostgreSQL v9.1. J'ai les tables suivantes:

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

Dites que la première table fooest peuplée comme ceci:

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

Existe-t-il un moyen d'insérer barfacilement des lignes en référençant le footableau? Ou dois-je le faire en deux étapes, d'abord en recherchant le footype que je veux, puis en insérant une nouvelle ligne 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'  );
Stéphane
la source

Réponses:

67

Votre syntaxe est presque bonne, nécessite des 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é chez 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 ;
ypercubeᵀᴹ
la source
A pris la lecture à quelques reprises, mais je comprends maintenant la deuxième solution que vous avez fournie. Je l'aime. En l'utilisant maintenant pour amorcer ma base de données avec une poignée de valeurs connues au premier démarrage du système.
Stéphane
37

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

  • L' VALUESexpression dans la sous-requête fait la même chose que le CTE de @ ypercube . Les expressions de table communes offrent des fonctionnalités supplémentaires et sont plus faciles à lire dans les grandes requêtes, mais elles constituent également un obstacle à l'optimisation. Ainsi, les sous-requêtes sont généralement un peu plus rapides lorsqu'aucun des éléments ci-dessus n'est nécessaire.

  • idcomme nom de colonne est un anti-motif largement répandu. Devrait être foo_idet bar_idou quoi que ce soit descriptif. En rejoignant un tas de tables, vous vous retrouvez avec plusieurs colonnes toutes nommées id...

  • Considérez simple textou varcharau lieu de varchar(n). Si vous avez vraiment besoin d'imposer une restriction de longueur, ajoutez une CHECKcontrainte:

  • Vous devrez peut-être ajouter des conversions de type explicites. Comme l' VALUESexpression 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 dans la première rangée, le reste va faire la queue.

INSERER les lignes FK manquantes en même temps

Si vous voulez créer des entrées inexistantes à foola volée, dans une seule instruction SQL , 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 violets , ce qui n'existe pas fooencore. Deux lignes pour illustrer la nécessité de DISTINCTla première INSERTdéclaration.

Explication pas à pas

  1. Le 1er CTE selfournit plusieurs lignes de données d'entrée. La sous-requête valavec l' VALUESexpression peut être remplacée par une table ou une sous-requête en tant que source. Immédiatement LEFT JOINà fooajouter le foo_idpour les typelignes préexistantes . Toutes les autres lignes sont foo_id IS NULLainsi.

  2. Le second CTE insinsère différents nouveaux types ( foo_id IS NULL) dans foo, et renvoie le nouveau généré foo_id- ainsi que le typepour rejoindre pour insérer des lignes.

  3. Le dernier extérieur INSERTpeut maintenant insérer un foo.id pour chaque ligne: le type préexistait ou il avait été inséré à l'étape 2.

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

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

Il y a une petite condition de concurrence si vous exécutez plusieurs de ces requêtes simultanément. Lire la suite sous des questions connexes ici et ici et ici . Cela ne se produit vraiment que sous une charge simultanée importante, si jamais. En comparaison avec les solutions de mise en cache comme celle annoncée dans une autre réponse, les chances sont minimes .

Fonction à usage répété

Pour une utilisation répétée, je créerais une fonction SQL prenant un tableau d'enregistrements en paramètre et l'utilisant unnest(param)à la place de l' VALUESexpression.

Ou, si la syntaxe des tableaux d'enregistrements est trop compliquée, utilisez une chaîne séparée par des virgules en tant que paramètre _param. Par exemple de la forme:

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

Ensuite, utilisez ceci pour remplacer l' VALUESexpression 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 de 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, cette ...

  • ... s'applique SELECTou INSERTsur foo: Tout élément typequi 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
  • ... s'applique INSERTou UPDATE((vrai "UPSERT")) sur bar: s'il descriptionexiste déjà, il typeest 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 cela typechange réellement:

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

    Il y a beaucoup d'autres façons de passer plusieurs lignes ...

Apparenté, relié, connexe:

Erwin Brandstetter
la source
Dans votre INSERT missing FK rows at the same timeexemple, le fait de placer cela dans une transaction réduirait-il le risque de concurrence dans SQL Server?
element11
1
@ element11: La réponse est pour Postgres, mais puisqu'il s'agit d'une seule commande SQL, il ne s'agit en aucun cas d'une seule transaction. L'exécuter dans le cadre d'une transaction plus importante ne ferait qu'augmenter la fenêtre temporelle des éventuelles conditions de concurrence. En ce qui concerne SQL Server: les CTE modifiant les données ne sont pas du tout pris en charge (uniquement SELECTdans une WITHclause). Source: documentation MS.
Erwin Brandstetter
1
Vous pouvez également le faire avec INSERT ... RETURNING \gsetin psqlpuis utiliser les valeurs renvoyées en tant que psql :'variables', mais cela ne fonctionne que pour les insertions à une seule ligne.
Craig Ringer
@ ErwinBrandstetter c'est très bien, mais je suis trop nouveau pour SQL pour tout comprendre. Pourriez-vous ajouter quelques commentaires à "INSÉRER les rangées FK manquantes en même temps" pour expliquer son fonctionnement? Merci également pour les exemples de travail SQLFiddle!
glallen
@glallen: J'ai ajouté une explication étape par étape. Il existe également de nombreux liens vers des réponses connexes et le manuel avec plus d'explications. Vous devez comprendre ce que la requête fait ou vous pouvez être au-dessus de votre tête.
Erwin Brandstetter
4

Chercher. Vous avez essentiellement besoin des identifiants de foo pour les insérer dans le bar.

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

En ce qui concerne les applications, vous pouvez cependant avoir un cache d’articles 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 utilisé comme niveau d'application d'ID stable (et pouvant être exposé au client dans des URL, etc.)
  • Code - une chaîne qui peut être présente et doit être unique si elle est présente (serveur SQL: index unique filtré sur non null). C'est un identifiant d'ensemble client.

Exemple:

  • Compte (dans une application de trading) -> Id est un int utilisé pour les clés étrangères. -> Identifier est un Guid utilisé dans les portails Web, etc. - toujours accepté. -> Le code est défini manuellement. Règle: une fois défini, il ne change pas.

Évidemment, lorsque vous voulez lier quelque chose à un compte - vous devez d'abord obtenir techniquement l'identifiant - 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 d'accéder à la base de données.

TomTom
la source
10
Vous savez que vous pouvez laisser le SGBDR effectuer la recherche pour vous, dans une seule instruction SQL, en évitant le cache propice aux erreurs?
Erwin Brandstetter
Vous êtes conscient que rechercher des éléments qui ne changent pas n’est pas sujet aux erreurs? De plus, généralement, le SGBDR n’est pas évolutif et constitue l’élément le plus coûteux du jeu, en raison des coûts de licence. Prendre autant de charge que possible n'est pas vraiment mauvais. En outre, peu d'ORM ne supportent cela.
TomTom
14
Éléments non changeants? L'élément le plus cher? Coûts de licence (pour PostgreSQL)? Les ORM définissant ce qui est sain Non, je n'étais pas au courant de tout cela.
Erwin Brandstetter