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' );
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 ;
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.
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
.
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.
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.
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.
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;
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:
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:
Exemple:
É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.