web-dev-qa-db-ja.com

ORA-38104:ON句で参照されている列は更新できません

削除フラグ付きのシンプルなテーブルがあります(レコードは、削除するのではなく、この列で更新​​する必要があります)。

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

新しいレコードを挿入するときに、主キーに一致するレコードがすでに存在するかどうかを確認する必要がありますが、ISDELETED = 1です。その場合、ISDELETEDを0に変更して、他の列を更新する必要があります。したがって、次のMerge-Statementを使用しています。

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);

Sql-Serverではうまく機能しますが、Oracleは次のように述べています。

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

IDELETED = 0の一致するレコードがある場合、例外として主キー違反が必要です。そのため、 "TARGET.ISDELETED = 1"をon-clauseからupdate-statementに移動できません。

28
FreeAndNil

この場合は、撮影後のルックアップアルゴリズムを使用する方がよいでしょう。

あなたがより頻繁なケースであると予想するものに応じて、次のいずれか:

  • 更新し、行が更新されない場合は挿入します。または
  • 挿入し、キー違反がある場合は更新します。
3
Adam Musch

受け入れられた応答とは対照的に、実際にはこれを回避する方法があります。問題のビットをON句から外して、更新ステートメントのWHERE句に移動します。

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

列を何らかの式に入れて名前を変更すると、うまくいくようです。以下の例では、ISDELETED_ISDELETEDは事実上同じものです:

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);

通知:

  • 名前を変更するだけでは機能しません。パーサーは、それがまだ同じ列であることを検出するのに十分「スマート」であるようです。しかし、名前を変更して「ばかげた」表現にすることは、パーサーよりも優れています。
  • これには明らかにコストがかかります。名前が変更された列でインデックスを簡単に使用できない場合があります。実行プランを確認してください。この特定の例では、うまくいくかもしれません
  • Oracleは将来的にこれを「修正」する可能性があり(そして、ORA-38104の検出をより一貫性のあるものにする可能性があります)、この回避策は機能しなくなる可能性があります。

これも機能しているようですが、適切なインデックスの使用は許可されていないようです(ご使用の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);

これでも機能します(これにより、ORA-38104チェック全体について深刻な疑問が生じます)。

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);

これらの回避策(および実行計画)については、こちらでブログに投稿しています

2
Lukas Eder

これはうまくいきませんか?

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

以下のシナリオも考慮する必要があります。

IDELETED = 0と一致するレコードがある場合、例外として主キー違反が必要です。そのため、「TARGET.ISDELETED = 1」をon-clauseからupdate-statementに移動できません。

正確な解決策は次のとおりです、

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