【问题标题】:UPDATE on seemingly key preserving view in Oracle raises ORA-01779Oracle 中看似关键的保留视图的更新引发 ORA-01779
【发布时间】:2019-01-30 17:39:22
【问题描述】:

问题

我正在尝试在 Oracle 12.1.0.2.0 中将性能较低的 MERGE 语句重构为 UPDATE 语句。 MERGE 语句如下所示:

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

这主要是因为对大型(100M 行)表t 进行了两次昂贵的访问

架构

这些是所涉及的简化表:

  • t 正在迁移其account_no 列的目标表。
  • u 包含account_no_oldaccount_no_new 映射的迁移指令表
  • v 模拟contract_idtenant_id 之间一对一关系的辅助表

架构是:

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

我无法修改架构。我知道添加t.tenant_id 可以通过阻止加入v 来解决问题

替代 MERGE 不起作用:

ORA-38104:ON 子句中引用的列无法更新

注意,自联接是无法避免的,因为这种替代的等效查询会导致 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 视图不起作用:

ORA-01779: 无法修改映射到非键保留表的列

直观地说,我会在这里应用传递闭包,这应该保证t 中的每个更新行,uv 中最多只能有 1 行。但显然,Oracle 不承认这一点,所以下面的 UPDATE 语句不起作用:

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

以上引发ORA-01779。添加未记录的提示 /*+BYPASS_UJVC*/ 在 12c 上似乎不再起作用。

如何告诉 Oracle 视图是密钥保留?

在我看来,视图仍然是保留键的,即对于t 中的每一行,v恰好一行,因此最多 u 中的一行。因此视图应该是可更新的。有没有办法重写这个查询,让 Oracle 相信我的判断?

或者是否有任何其他我忽略的语法阻止MERGE 语句对t 的双重访问?

【问题讨论】:

  • 你能让u.account_no_old独一无二吗?一个唯一的索引可以做到这一点。 (顺便说一句,bypass_ujvc 已在 11.2 中删除。)
  • @WilliamRobertson。不,account_no 值在每个租户的基础上都是唯一的,因此 v 的连接。
  • 嗯,这就是甲骨文认为它没有保留密钥的原因。通常的替代方法是 PL/SQL 游标,可以作为 Cursor FOR 循环或批量收集 + forall
  • 抱歉,我误读了V 上的PK,这实际上意味着每个contract_id 都有精确的一个 tenant_id。所以你的加入是 - 我称之为 - count保留。但恕我直言,Oracle 只有在将 FK 连接到 PK 时才能保留密钥,这可以在更多表上传递,但在您的设置中没有完整归档。
  • 添加了我的答案。不确定我是否遗漏了什么。

标签: sql oracle merge updatable-views


【解决方案1】:

您可以定义一个临时表,其中包含来自UV 的预连接数据。

contract_id, account_no_old 上的唯一 索引支持它(应该是唯一的)。

然后您可以在可更新的连接视图中使用这个临时表。

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
;

【讨论】:

  • 是的,当然可以。我曾尝试使用BULK FETCH INTO 进行内存收集,这比MERGE 解决方案要慢。还没有测试过这种方法...
  • @LukasEder 试一试;)启用parallel DML 的正确哈希连接可能会做得最好。
  • 在这种情况下,我不会使用并行 DML。我被一些神秘的虫子严重烧伤。在此处查看详细信息:twitter.com/AndrewSayer_/status/1017085117725016064
【解决方案2】:

有没有办法重写这个查询,让 Oracle 相信我的判断?

我已经设法通过在目标中引入帮助列来“说服”Oracle 进行 MERGE:

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


编辑

上述想法的变体——子查询直接移至ON 部分:

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

相关文章:Columns referenced in the ON Clause cannot be updated

编辑 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

【讨论】:

  • 哈,太搞笑了! :-) 试图聪明地防止某些 SQL 使用的数据库......去看看。
  • 感谢您链接到我的博客文章 :) 请注意,我认为您的两种解决方法都会阻止对 t.account_no 列的索引访问。应用链接文章中提到的其他解决方法之一可能会更好。特别是使用行值表达式ON ((t.account_no, 'dummy') = ((s.account_no_old, 'dummy')) AND t.contract_id = s.contract_id)
  • @LukasEder 看起来dummy 的方法不适用于这种情况。 demo
  • @LukasEder 呵呵,更可笑:) demo ON ((t.account_no,t.contract_id, 'dummy') = ((s.account_no_old,s.contract_id, 'dummy')) OR 1=2)
  • 这真是太古怪了!
【解决方案3】:

尝试通过更简单的更新来做到这一点。仍然需要一个子选择。

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

鲍比

【讨论】:

  • 非常感谢。它在我简化的示例中确实有效。我记得它在现实世界的例子中不起作用 - 但我不记得为什么了。
  • 我不确定原始声明中是否存在此更新未涵盖的内容。但我做了一个简单的例子,它在这种情况下有效。
猜你喜欢
  • 2023-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多