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] 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' VALUES
expression 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.
id
comme nom de colonne est un anti-motif largement répandu. Devrait être foo_id
et bar_id
ou quoi que ce soit descriptif. En rejoignant un tas de tables, vous vous retrouvez avec plusieurs colonnes toutes nommées id
...
Considérez simple text
ou varchar
au lieu de varchar(n)
. Si vous avez vraiment besoin d'imposer une restriction de longueur, ajoutez une CHECK
contrainte:
Vous devrez peut-être ajouter des conversions de type explicites. Comme l' VALUES
expression 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 à foo
la 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 foo
encore. Deux lignes pour illustrer la nécessité de DISTINCT
la première INSERT
déclaration.
Explication pas à pas
Le 1er CTE sel
fournit plusieurs lignes de données d'entrée. La sous-requête val
avec l' VALUES
expression peut être remplacée par une table ou une sous-requête en tant que source. Immédiatement LEFT JOIN
à foo
ajouter le foo_id
pour les type
lignes préexistantes . Toutes les autres lignes sont foo_id IS NULL
ainsi.
Le second CTE ins
insère différents nouveaux types ( foo_id IS NULL
) dans foo
, et renvoie le nouveau généré foo_id
- ainsi que le type
pour rejoindre pour insérer des lignes.
Le dernier extérieur INSERT
peut 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 KEY
contraintes 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' VALUES
expression.
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' VALUES
expression 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 SELECT
ou INSERT
sur foo
: Tout élément 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
... s'applique INSERT
ou UPDATE
((vrai "UPSERT")) sur bar
: s'il description
existe déjà, il 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 cela type
change réellement:
... transmet des valeurs aux types de lignes bien connus avec un VARIADIC
paramè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: