web-dev-qa-db-fra.com

Insertion conditionnelle avec un CTE imbriqué?

J'essaie de comprendre s'il y a une manière que je puisse faire travailler des CTES imbriquées pour ce cas particulier.

Considérez le scénario suivant (très artificiel) basé sur l'application réelle: il y a une seule table de colonne des identifiants d'employés. Ensuite, il y a une table d'établissement avec tous les détails. (La principale raison de la table unique de Col est souvent, bien sûr, le nouvel employé de l'employé doit être créé dans les lots et attribués avant que tous les détails du personnel réel ne soit connu.)

Maintenant, à la tâche à la main, nous insérons des détails (c'est-à-dire le nom) pour un nouvel employé, mais nous devons d'abord vérifier si un employé avec ce nom existe déjà. Si cela le fait, nous retournerons simplement l'ID, et sinon, nous allons créer un nouvel enregistrement d'employé, puis insérer les détails, renvoyant enfin l'identifiant nouvellement créé.

Pour recréer ce scénario de test:

CREATE TABLE public.employee (
    id text DEFAULT gen_random_uuid(),
    PRIMARY KEY (id)
);

CREATE TABLE public.employee_details (
    employee_id text,
    name text,
    PRIMARY KEY (employee_id),
    FOREIGN KEY (employee_id) REFERENCES public.employee(id)
);

La requête que j'essaie de marteler en forme semble indiquée ci-dessous.

with 
e as 
    (select name, employee_id from employee_details where name = 'jack bauer'), 

i as (insert into employee_details (name, employee_id) 
    select 'jack bauer', 
        (with a as (insert into employee values(default) RETURNING id) select a.id from a)
    where not exists (select 1 from e) returning name, employee_id) 

select employee_id, name from e
union all 
select employee_id, name from i; 

Si je remplace le CTE imbriqué avec un ID déjà créé (exécutant le CTE imbriqué séparément), cela fonctionne (mais peut entraîner la création d'un ID superflu). Il est également possible de simplement déplacer le CTE imbriqué sur le niveau supérieur (donc tout ce que l'on ressemblera à with e as (..), i as (..), a as (..) select .. where not exists..., mais cela signifierait également qu'un identifiant d'employé superflu est créé lorsque aucun détail n'a besoin d'être inséré. Je veux comprendre un moyen de le faire "en ligne" - de sorte que le nouvel identifiant ne sera créé que si la clause not exists revient vrai.

Je continue à obtenir l'erreur:

Avec la clause contenant une instruction modification de données doit être au niveau supérieur.

Je suppose que le problème est que le CTE imbriqué renvoie une "colonne" alors que toute la requête fonctionnera si elle obtient une "valeur" à la place (laquelle elle fait, quand on copie simplement une valeur de texte au lieu de la CTE). J'ai rencontré une ne discussion quelque peu liée à cette question mentionner un virus apparent qui a été corrigé depuis 9.3. Je ne sais pas si cela est lié à mes ennuis ici. Pour citer la discussion liée:

Le code d'analyse d'analyse semble penser qu'avec ne peut être fixé qu'au niveau supérieur ou au niveau de la feuille, sélectionnez dans un arbre de fonctionnement défini; Mais la grammaire suit la norme SQL qui dit non une telle chose

J'utilise Postgres 10.3.

5
Yogesch

Aux fins de cette question, je suppose que employee_details.name À définir UNIQUE. Sinon, toute l'opération n'aurait pas de sens.

Vous ne pouvez pas nidifier de CTE modifiant des données comme vous avez essayé (comme vous l'avez déjà découvert de la dure) - et vous n'avez pas besoin de. Cette requête atteindrait votre objectif:

WITH e AS (
   SELECT name, employee_id
   FROM   employee_details
   WHERE  name = 'jack bauer'
   )
 , i1 AS (
   INSERT INTO employee             -- no target columns!
   SELECT                           -- empty SELECT list!
   WHERE NOT EXISTS (SELECT FROM e)
   RETURNING id
   )
 , i2 AS (
   INSERT INTO employee_details (name, employee_id) 
   SELECT 'jack bauer', id
   FROM   i1
   RETURNING name, employee_id
   )
SELECT employee_id, name FROM e
UNION ALL 
SELECT employee_id, name FROM i2;

La fonctionnalité principale est la INSERT sans colonnes cible et un vide SELECT. Postgres remplit toutes les colonnes non répertoriées dans la valeur SELECT avec des valeurs par défaut. De cette façon, nous pouvons remplacer l'inconditionnelle VALUES (default) avec un conditionnel INSERT. Le CTE i1 N'entre qu'une rangée si le nom donné n'a pas été trouvé.

le manuel :

Si aucune liste des noms de colonne [cible] n'est indiquée du tout, la valeur par défaut est toutes les colonnes du tableau dans leur ordre déclaré; [...]

Chaque colonne non présente dans la liste de colonnes explicite ou implicite sera remplie d'une valeur par défaut, sa valeur par défaut déclarée ou NULL s'il n'y en a pas.

Ceci est une extension spécifique des postgres de la norme:

En outre, le cas dans lequel une liste de noms de colonne est omise, mais toutes les colonnes ne sont pas remplies de la clause VALUES ou query, est interdit par le la norme.

Le dernier CTE i2 N'entre qu'une rangée si i1 Renvoyé une ligne. Voilá.

Ceci est Sous réserve des conditions de course sous la charge d'écriture concurrente aux mêmes tables. Si vous avez besoin de l'exclure, vous devez faire plus. En rapport:

Sans les complications de l'insert conditionnel dans la 2e table, cela bouillirait à un cas commun de Sélectionner ou insérer:

De côté

"id" text DEFAULT gen_random_uuid()

Je conseillerais vivement d'utiliser le type de données uuid pour stocker UUIDS.

7