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
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.
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.
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: