web-dev-qa-db-fra.com

ORA-38104: Les colonnes référencées dans la clause ON ne peuvent pas être mises à jour

j'ai une table simple avec un indicateur de suppression (les enregistrements doivent être mis à jour dans cette colonne au lieu d'être supprimés):

create table PSEUDODELETETABLE
(
  ID        NUMBER(8) not null, -- PKEY
  NAME      VARCHAR2(50) not null,
  ISDELETED NUMBER(1) default 0 not null
)

Lors de l'insertion de nouveaux enregistrements, je dois vérifier s'il existe déjà un enregistrement correspondant à la clé primaire mais ayant ISDELETED = 1. Dans ce cas, je dois remplacer ISDELETED par 0 et mettre à jour les autres colonnes. Par conséquent, j'utilise la déclaration de fusion suivante:

merge into ET.PSEUDODELETETABLE TARGET
using (select 1 as ID, 'Horst' as NAME from sys.dual) SOURCE
on (TARGET.ISDELETED = 1 and SOURCE.ID = TARGET.ID)
when matched then
  update set ISDELETED = 0, NAME = SOURCE.NAME
when not matched then
  insert values (SOURCE.ID, SOURCE.NAME, 0);

Sur Sql-Server, cela fonctionne très bien, mais Oracle dit:

ORA-38104: Columns referenced in the ON Clause cannot be updated: TARGET.ISDELETED

S'il existe un enregistrement correspondant avec IDELETED = 0, je souhaite que la violation de clé primaire soit une exception, c'est pourquoi je ne peux pas déplacer "TARGET.ISDELETED = 1" de la clause on vers l'instruction update.

28
FreeAndNil

Je suppose que vous êtes mieux dans ce cas avec un algorithme de tir puis de regard.

Selon ce que vous pensez être le cas le plus fréquent, soit:

  • Mettre à jour et si aucune ligne n'est mise à jour, insérer; ou
  • Insérez, et s'il y a une violation de clé, mettez à jour.
3
Adam Musch

Contrairement à la réponse acceptée, il existe en fait un moyen de retirer cela: déplacez le bit incriminé hors de la clause ON et dans la clause WHERE de l'instruction de mise à jour:

merge into ET.PSEUDODELETETABLE TARGET
using (select 1 as ID, 'Horst' as NAME from sys.dual) SOURCE
on (SOURCE.ID = TARGET.ID)
when matched then
  update 
      set ISDELETED = 0, 
      NAME = SOURCE.NAME
  where TARGET.ISDELETED = 1 -- Magic!
when not matched then
  insert 
      values (SOURCE.ID, SOURCE.NAME, 0);
43
David Marx

Mettre la colonne dans une expression et la renommer semble fonctionner. Dans l'exemple ci-dessous, ISDELETED_ et ISDELETED sont en fait la même chose:

merge into (
  select nvl(ISDELETED, ISDELETED) as ISDELETED_, ISDELETED, ID, 
  from ET.PSEUDODELETETABLE
) TARGET
using (select 1 as ID, 'Horst' as NAME from sys.dual) SOURCE
on (TARGET.ISDELETED_ = 1 and SOURCE.ID = TARGET.ID) -- Use the renamed version here
when matched then
  update set ISDELETED = 0, NAME = SOURCE.NAME       -- Use the original version here
when not matched then
  insert values (SOURCE.ID, SOURCE.NAME, 0);

Remarquer:

  • Le simple changement de nom ne fonctionne pas. L'analyseur semble être suffisamment "intelligent" pour détecter qu'il s'agit toujours de la même colonne. Mais le renommer et le mettre dans une expression "idiote" surpasse l'analyseur.
  • Cela a évidemment un coût. Les index peuvent ne pas être facilement utilisables sur la colonne renommée, vérifiez le plan d'exécution. Dans cet exemple particulier, cela pourrait fonctionner
  • Oracle pourrait "corriger" cela à l'avenir (et rendre la détection ORA-38104 plus cohérente), de sorte que cette solution de contournement pourrait ne pas fonctionner.

Cela semble également fonctionner, mais ne semble certainement pas permettre une utilisation raisonnable de l'index (vérifiez à nouveau sur votre version d'Oracle):

merge into ET.PSEUDODELETETABLE TARGET
using (select 1 as ID, 'Horst' as NAME from sys.dual) SOURCE
on ((select TARGET.ISDELETED from dual) = 1 and SOURCE.ID = TARGET.ID)
when matched then
  update set ISDELETED = 0, NAME = SOURCE.NAME
when not matched then
  insert values (SOURCE.ID, SOURCE.NAME, 0);

Même cela fonctionne (ce qui soulève de sérieux doutes sur le chèque ORA-38104 dans son ensemble)!

merge into ET.PSEUDODELETETABLE TARGET
using (select 1 as ID, 'Horst' as NAME from sys.dual) SOURCE
on ((TARGET.ISDELETED, 'dummy') = ((1, 'dummy')) and SOURCE.ID = TARGET.ID)
when matched then
  update set ISDELETED = 0, NAME = SOURCE.NAME
when not matched then
  insert values (SOURCE.ID, SOURCE.NAME, 0);

J'ai blogué sur ces solutions de contournement (et plans d'exécution) ici .

2
Lukas Eder

Cela ne fonctionnera-t-il pas simplement?

merge into (select * from ET.PSEUDODELETETABLE where ISDELETED = 1) TARGET
using (select 1 as ID, 'Horst' as NAME from sys.dual) SOURCE
on (SOURCE.ID = TARGET.ID)
when matched then
  update set ISDELETED = 0, NAME = SOURCE.NAME
when not matched then
  insert values (SOURCE.ID, SOURCE.NAME, 0);
1
Toon Koppelaars

Nous devons également considérer le scénario ci-dessous,

S'il existe un enregistrement correspondant avec IDELETED = 0, Je veux que la violation de la clé primaire fasse exception, c'est pourquoi je ne peux pas déplacer "TARGET.ISDELETED = 1" de la clause on vers l'instruction update.

Donc, la solution exacte est comme ci-dessous,

begin 
    update ET.PSEUDODELETETABLE set ISDELETED = 0, NAME = 'Horst' 
    where ISDELETED = 1 and ID = 1; 
    if (sql%rowcount = 0) then 
        insert into ET.PSEUDODELETETABLE values (1, 'Horst', 0); 
    end if; 
end;
1
Mani Kandan