Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UPDATE on seemingly key preserving view in Oracle raises ORA-01779

Problem

I'm trying to refactor a low-performing MERGE statement to an UPDATE statement in Oracle 12.1.0.2.0. The MERGE statement looks like this:

MERGE INTO t
USING (
  SELECT t.rowid rid, u.account_no_new
  FROM t, u, v
  WHERE t.account_no = u.account_no_old
  AND t.contract_id = v.contract_id
  AND v.tenant_id = u.tenant_id
) s
ON (t.rowid = s.rid)
WHEN MATCHED THEN UPDATE SET t.account_no = s.account_no_new

It is mostly low performing because there are two expensive accesses to the large (100M rows) table t

Schema

These are the simplified tables involved:

  • t The target table whose account_no column is being migrated.
  • u The migration instruction table containing a account_no_oldaccount_no_new mapping
  • v An auxiliary table modelling a to-one relationship between contract_id and tenant_id

The schema is:

CREATE TABLE v (
  contract_id NUMBER(18) NOT NULL PRIMARY KEY,
  tenant_id NUMBER(18) NOT NULL
);
CREATE TABLE t (
  t_id NUMBER(18) NOT NULL PRIMARY KEY,
  -- tenant_id column is missing here
  account_no NUMBER(18) NOT NULL,
  contract_id NUMBER(18) NOT NULL REFERENCES v
);
CREATE TABLE u (
  u_id NUMBER(18) NOT NULL PRIMARY KEY,
  tenant_id NUMBER(18) NOT NULL,
  account_no_old NUMBER(18) NOT NULL,
  account_no_new NUMBER(18) NOT NULL,

  UNIQUE (tenant_id, account_no_old)
);

I cannot modify the schema. I'm aware that adding t.tenant_id would solve the problem by preventing the JOIN to v

Alternative MERGE doesn't work:

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

Note, the self join cannot be avoided, because this alternative, equivalent query leads to ORA-38104:

MERGE INTO t
USING (
  SELECT u.account_no_old, u.account_no_new, v.contract_id
  FROM u, v
  WHERE v.tenant_id = u.tenant_id
) s
ON (t.account_no = s.account_no_old AND t.contract_id = s.contract_id)
WHEN MATCHED THEN UPDATE SET t.account_no = s.account_no_new

UPDATE view doesn't work:

ORA-01779: cannot modify a column which maps to a non-key-preserved table

Intuitively, I would apply transitive closure here, which should guarantee that for each updated row in t, there can be only at most 1 row in u and in v. But apparently, Oracle doesn't recognise this, so the following UPDATE statement doesn't work:

UPDATE (
  SELECT t.account_no, u.account_no_new
  FROM t, u, v
  WHERE t.account_no = u.account_no_old
  AND t.contract_id = v.contract_id
  AND v.tenant_id = u.tenant_id
)
SET account_no = account_no_new

The above raises ORA-01779. Adding the undocumented hint /*+BYPASS_UJVC*/ does not seem to work anymore on 12c.

How to tell Oracle that the view is key preserving?

In my opinion, the view is still key preserving, i.e. for each row in t, there is exactly one row in v, and thus at most one row in u. The view should thus be updatable. Is there any way to rewrite this query to make Oracle trust my judgement?

Or is there any other syntax I'm overlooking that prevents the MERGE statement's double access to t?

like image 804
Lukas Eder Avatar asked Aug 24 '18 09:08

Lukas Eder


3 Answers

Is there any way to rewrite this query to make Oracle trust my judgement?

I've managed to "convince" Oracle to do MERGE by introducing helper column in target:

MERGE INTO (SELECT (SELECT t.account_no FROM dual) AS account_no_temp,
                    t.account_no, t.contract_id 
            FROM t) t
USING (
  SELECT u.account_no_old, u.account_no_new, v.contract_id
  FROM u, v
  WHERE v.tenant_id = u.tenant_id
) s
ON (t.account_no_temp = s.account_no_old AND t.contract_id = s.contract_id)
WHEN MATCHED THEN UPDATE SET t.account_no = s.account_no_new;

db<>fiddle demo


EDIT

A variation of idea above - subquery moved directly to ON part:

MERGE INTO (SELECT t.account_no, t.contract_id FROM t) t
USING (
      SELECT u.account_no_old, u.account_no_new, v.contract_id
      FROM u, v
      WHERE v.tenant_id = u.tenant_id
    ) s
ON ((SELECT t.account_no FROM dual) = s.account_no_old
     AND t.contract_id = s.contract_id)
WHEN MATCHED THEN UPDATE SET t.account_no = s.account_no_new;

db<>fiddle demo2

Related article: Columns referenced in the ON Clause cannot be updated

EDIT 2:

MERGE INTO (SELECT t.account_no, t.contract_id FROM t) t
USING (SELECT u.account_no_old, u.account_no_new, v.contract_id
       FROM u, v
       WHERE v.tenant_id = u.tenant_id) s
ON((t.account_no,t.contract_id,'x')=((s.account_no_old,s.contract_id,'x')) OR 1=2) 
WHEN MATCHED THEN UPDATE SET t.account_no = s.account_no_new;

db<>fiddle demo3

like image 115
Lukasz Szozda Avatar answered Oct 13 '22 02:10

Lukasz Szozda


You may define a temporary table containing the pre-joined data from U and V.

Back it with a unique index on contract_id, account_no_old (which should be unique).

Then you may use this temporary table in an updateable join view.

create table tmp as
  SELECT v.contract_id, u.account_no_old, u.account_no_new
  FROM u, v
  WHERE  v.tenant_id = u.tenant_id;

create unique index tmp_ux1 on tmp ( contract_id, account_no_old);


UPDATE (
  SELECT t.account_no, tmp.account_no_new
  FROM t, tmp
  WHERE t.account_no = tmp.account_no_old
  AND t.contract_id = tmp.contract_id
)
SET account_no = account_no_new
;
like image 25
Marmite Bomber Avatar answered Oct 13 '22 00:10

Marmite Bomber


Trying to do this with a simpler update. Still requires a subselect.

update t
set t.account_no = (SELECT u.account_no_new
  FROM u, v
  WHERE t.account_no = u.account_no_old
  AND t.contract_id = v.contract_id
  AND v.tenant_id = u.tenant_id);

Bobby

like image 44
Bobby Durrett Avatar answered Oct 13 '22 00:10

Bobby Durrett