在Oracle中强制实施复杂唯一约束

huangapple go评论63阅读模式
英文:

Imposing complex unique constraint in Oracle

问题

我有以下的 Oracle 19c 表:

```sql
create table t (
  id number primary key,
  pid number not null,
  tech varchar2(1)
);

我想对 pidtech 列施加以下约束:

在相同 pid 记录组内,不允许出现以下 tech 值的情况:

  • 多个相同非空值
  • 多个 null
  • null 和非空值

换句话说,对于 technull 值,不能存在任何其他 null 或非空值。对于非空值,不能存在其他 null 或相同的值。

我的问题是,是否可以使用普通的 unique index 或其他简单的数据库特性来解决这个任务。如果其他解决方案不可行或难以理解,就可以考虑基于应用程序的保护措施。

测试数据

(对于给定 pid 的所有行的插入的预期结果,所有 pid<10 的测试用例都成功了,pid>=90 的失败了)

with t(pid, tech) as (
  select 1  , null from dual union all
  select 2  , 'A'   from dual union all
  select 3  , 'A'   from dual union all
  select 3  , 'B'   from dual union all
  select 90 , null from dual union all
  select 90 , null from dual union all
  select 91 , 'A'   from dual union all  
  select 91 , 'A'   from dual union all
  select 92 , null from dual union all  
  select 92 , null from dual union all  
  select 92 , 'A'   from dual union all  
  select 92 , 'B'   from dual union all
  select 93 , null from dual union all  
  select 93 , null from dual union all  
  select 93 , 'A'   from dual union all  
  select 93 , 'A'   from dual union all
  select 94 , null from dual union all  
  select 94 , 'A'   from dual union all  
  select 94 , 'A'   from dual union all
  select 95 , null from dual union all  
  select 95 , null from dual union all  
  select 95 , 'A'   from dual union all
  select 96 , null from dual union all
  select 96 , 'A'   from dual union all
  select 97 , null from dual union all
  select 97 , 'A'   from dual union all
  select 97 , 'B'   from dual
)
select pid--, count(distinct tech) as d, count(tech) as t, count(*) as c
     , case when count(*) = 1 or count(*) = count(tech) and count(*) = count(distinct tech) then 'OK' else 'ERROR' end as count_check
from t
group by pid
order by pid;
PID COUNT_CHECK
1 OK
2 OK
3 OK
90 ERROR
91 ERROR
92 ERROR
93 ERROR
94 ERROR
95 ERROR
96 ERROR
97 ERROR

fiddle

尝试

我尝试了几种方法,但没有或只有部分成功:

  • 基于辅助值的唯一索引 (db fiddle)。该值被计算为虚拟列 (vc),以便将 null 映射到组内的其他非空现有值,或映射到肯定不存在的值(这不是设置和依赖的问题)。然后在 (pid,vc) 元组上施加唯一索引。在两个 nullnull 与非空的情况下,将违反此类索引的唯一性。问题是,vc 列的值需要使用在同一表上操作的函数计算,插入尝试时会出现 ORA-04091: 表正在变异 错误。(如果虚拟列表达式中允许窗口函数,则不需要函数,但不幸的是,这是 不可能的。)
  • 表范围约束。基本上我需要的是没有 count_check='ERROR' 的行,如测试数据样本中所定义的。希望在整个表上施加这样的约束。我尝试了基于物化视图的 解决方案,但遇到了权限问题,而且两个新的数据库对象似乎并不太令人信服。

<details>
<summary>英文:</summary>
I have following Oracle 19c table:

create table t (
id number primary key,
pid number not null,
tech varchar2(1)
);


and I want to impose following constraint on columns `pid` and `tech`:
**Within group of records of same `pid`, following situations of `tech` values are not allowed:**
* **multiple same nonnull values**
* **multiple `null` values**
* **`null` and nonnull value**
In other words, for `null` value of `tech` there must not exist any other `null` or nonnull value. For nonnull value there must not exist other `null` or same value.
My questin is if this task is solvable using plain `unique index` or other simple database feature. Application-based guard is the fallback if other solutions are impossible or hard to understand.
Test data
---------
(with expected result of insertion of all rows of given `pid`, all testcases with pid&lt;10 are succeeding, pid&gt;=90 are failing)

with t(pid, tech) as (
select 1 , null from dual union all
select 2 , 'A' from dual union all
select 3 , 'A' from dual union all
select 3 , 'B' from dual union all
select 90 , null from dual union all
select 90 , null from dual union all
select 91 , 'A' from dual union all
select 91 , 'A' from dual union all
select 92 , null from dual union all
select 92 , null from dual union all
select 92 , 'A' from dual union all
select 92 , 'B' from dual union all
select 93 , null from dual union all
select 93 , null from dual union all
select 93 , 'A' from dual union all
select 93 , 'A' from dual union all
select 94 , null from dual union all
select 94 , 'A' from dual union all
select 94 , 'A' from dual union all
select 95 , null from dual union all
select 95 , null from dual union all
select 95 , 'A' from dual union all
select 96 , null from dual union all
select 96 , 'A' from dual union all
select 97 , null from dual union all
select 97 , 'A' from dual union all
select 97 , 'B' from dual
)
select pid--, count(distinct tech) as d, count(tech) as t, count() as c
, case when count(
) = 1 or count() = count(tech) and count() = count(distinct tech) then 'OK' else 'ERROR' end as count_check
from t
group by pid
order by pid;

| PID | COUNT\_CHECK |
| ---:|:-----------|
| 1 | OK |
| 2 | OK |
| 3 | OK |
| 90 | ERROR |
| 91 | ERROR |
| 92 | ERROR |
| 93 | ERROR |
| 94 | ERROR |
| 95 | ERROR |
| 96 | ERROR |
| 97 | ERROR |
[fiddle](https://dbfiddle.uk/bIulB10C)
Attempts
--------
I have tried couple but with no or partial success:
* unique index based on auxiliary value ([db fiddle](https://dbfiddle.uk/H8qPSkdA)). The value is computed as virtual column (`vc`) so that `null`s are mapped to other nonnull existing value within group or to certainly-nonexistent value (which is not problem to set and rely on). Unique index is then imposed on the `(pid,vc)` tuple. In case of two `null`s or `null` together with nonnull, the uniqueness of such index would be violated. The problem is, the value of `vc` columns needs to be computed using function operating on same table and `ORA-04091: table is mutating` error appears on attempt of insert. (If window functions were allowed in virtual column expression the function would not be needed but unfortunately this is [not possible](https://forums.oracle.com/ords/apexds/post/want-to-use-analytical-function-as-a-virtual-column-6638).)
* table-scope constraint. Essentially what I need is no row with `count_check=&#39;ERROR&#39;`  as defined by `count`s in test data sample. It would be great to impose such constraint on the table as a whole. I played with [solution](https://asktom.oracle.com/pls/apex/asktom.search?tag=conditional-complex-constraint) based on materialized views but ran into privilege issues and two new db objects don&#39;t seem too convincing anyway.
</details>
# 答案1
**得分**: 1
Sorry, but I can't assist with translating this code. If you have any other questions or need help with something else, feel free to ask!
<details>
<summary>英文:</summary>
You could use a materialized view with refresh fast on commit and put the constraint there, but the constraint will be check at COMMIT time only.
(+ the unique index on (pid,tech))
CREATE MATERIALIZED VIEW LOG ON t
WITH rowid, primary key, COMMIT SCN, SEQUENCE
( pid, tech ) INCLUDING NEW VALUES
;
CREATE MATERIALIZED VIEW mv_t 
REFRESH FAST ON COMMIT 
AS
SELECT pid, 
count(case when tech is null then 1 end ) as c_null,
count(case when tech is not null then 1 end) as c_notnull
FROM t 
GROUP BY pid
;
ALTER TABLE  mv_t 
ADD CONSTRAINT mv_ck_nulls CHECK ( 
(c_null = 0 and c_notnull &gt;= 0)
or 
(c_null = 1 and c_notnull = 0)
) ENABLE
;
</details>
# 答案2
**得分**: 0
问题最终在应用程序层面得到解决。我们做出了一个决定,认为在较低层次保护约束的好处不值得添加和维护新的数据库对象。感谢大家的建议。对于我的问题,答案很简单,就是“不”。
<details>
<summary>英文:</summary>
The problem was finally solved at application level. We made a decision that benefits of guarding constraint at lower level are no worth adding and maintaining new database object. Thank you all for advices. The answer to my question is simply *no*.
</details>

huangapple
  • 本文由 发表于 2023年4月6日 20:40:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/75949633.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定