Create Unique Constraint over function based index with existing duplicate values - oracle

I have a table with wrong data and I'd like to prevent new wrong data from beeing inserted while I fix data and find out what process or where is the sentence making this happen.
I first made a UQ constraint over the columns that shouldn't be duplicated, but this gets me into another problem: I need to apply uniqueness only when all the columns have value, if there are nulls I need duplicate records over these columns. Something like this:
CREATE TABLE MYTAB (COL1 NUMBER, COL2 NUMBER, COL3 NUMBER, COL4 NUMBER); --EXAMPLE TABLE. I NEED NO DUPS OVER (COL1, COL3, COL4)
INSERT INTO MYTAB VALUES (1, 1, 1, 1); --OK
INSERT INTO MYTAB VALUES (1, 2, 1, 1); -- NOOK
INSERT INTO MYTAB VALUES (1, 3, NULL, NULL); --OK
INSERT INTO MYTAB VALUES (1, 4, NULL, NULL); --OK
If I create a constraint like this:
ALTER TABLE MYTAB
ADD CONSTRAINT U_CONSTRAINT UNIQUE (COL1, COL3, COL4) NOVALIDATE;
Last insert will crash.
I've tried with
CREATE UNIQUE INDEX FN_UIX_MYTAB
ON MYTAB (CASE WHEN COL2 IS NOT NULL THEN COL1 ELSE null END,
CASE WHEN COL2 IS NOT NULL THEN COL3 ELSE null END
CASE WHEN COL2 IS NOT NULL THEN COL4 ELSE null END) ;
But the create crashes because table has duplicate data. I'll need to create this index without validating existing data, wich means index will be applied only to new records inserted.
I've tried also with:
CREATE INDEX FN_IX_MYTAB
ON MYTAB (CASE WHEN COL2 IS NOT NULL THEN COL1 ELSE null END,
CASE WHEN COL2 IS NOT NULL THEN COL3 ELSE null END,
CASE WHEN COL2 IS NOT NULL THEN COL4 ELSE null END) ;
ALTER TABLE MYTAB
ADD CONSTRAINT FN_UIX_MYTAB UNIQUE (COL1, COL3, COL4) USING INDEX FN_IX_MYTAB NOVALIDATE;
But this gives me error:
ORA-14196: Specified index cannot be used to enforce the constraint.
Is there a way to do what I've explained, or should I prevent wrong inserts in another way while I look for the origin of the problem? Any advice will be appreciated also.

Here's one possible approach. Create a materialized view, with refresh on commit (preferably fast refresh, if the circumstances permit; in this case, they should). The MV would be something like
create materialized view mymv
refresh fast on commit
as
select col1, col3, col4
from mytab
where col1 is not null and col3 is not null and col4 is not null
;
And then put a unique constraint on (col1, col3, col4) on the MV.

Related

Convert MERGE to UPDATE

I have the following MERGE statement.Is there a way to convert this into an update statement without using MERGE?
MERGE INTO tab1
USING (SELECT tab1.col1, tab2.col2
FROM tab1, tab2
WHERE tab1.col1 = tab2.col1) tab3
ON (tab1.col1 = tab3.col1)
WHEN MATCHED THEN UPDATE SET col2 = tab3.col2
What you are asking about is called "update through join", and contrary to a widely held belief, it is possible in Oracle. But there is a catch.
Obviously, the update - no matter how you attempt to perform it - is not well defined unless column col1 is unique in table tab2. That column is used for lookup in the update process; if its values are not unique, the update will be ambiguous. I ignore here idiotic retorts such as "uniqueness is needed only for those values also found in tab1.col1", or "there is no ambiguity as long as all values in tab2.col2 are equal when the corresponding values in tab2.col1 are equal".
The "catch" is this. The uniqueness of tab2.col1 may be a matter of data (you know it when you inspect the data), or a matter of metadata (there is a unique constraint, or a unique index, or a PK constraint, etc., on tab2.col1, which the parser can inspect without ever looking at the actual data).
merge will work even when uniqueness is only known by inspecting the data. It will still throw an error if uniqueness is violated - but that will be a runtime error (only after the data in tab2 is accessed from disk). By contrast, updating through a join requires the same uniqueness to be known ahead of time, through the metadata (or in other ways: for example if the second rowset - not a table but the table-like result of a query - is the result of an aggregation grouping on the join column; then the uniqueness is guaranteed by the definition of "aggregation").
Here is a brief example to show the difference.
Test data:
create table tab1 (col1 number, col2 number);
insert into tab1 (col1, col2) values (1, 3);
create table tab2 (col1 number, col2 number);
insert into tab2 (col1, col2) values (1, 6);
commit;
merge statement (with check at the end):
merge into tab1
using(
select tab1.col1,
tab2.col2
from tab1,tab2
where tab1.col1 = tab2.col1) tab3
on(tab1.col1 = tab3.col1)
when matched then
update
set col2 = tab3.col2;
1 row merged.
select * from tab1;
COL1 COL2
---------- ----------
1 6
Now let's restore table tab1 to its original data for the next test(s):
rollback;
select * from tab1;
COL1 COL2
---------- ----------
1 3
Update through join - with no uniqueness guaranteed in the metadata (will result in error):
update
( select t1.col2 as t1_c2, t2.col2 as t2_c2
from tab1 t1 join tab2 t2 on t1.col1 = t2.col1
)
set t1_c2 = t2_c2;
Error report -
SQL Error: ORA-01779: cannot modify a column which maps to a non key-preserved table
01779. 00000 - "cannot modify a column which maps to a non key-preserved table"
*Cause: An attempt was made to insert or update columns of a join view which
map to a non-key-preserved table.
*Action: Modify the underlying base tables directly.
Now let's add a unique constraint on the lookup column:
alter table tab2 modify (col1 unique);
Table TAB2 altered.
and try the update again (with the same update statement), plus verification:
update
( select t1.col2 as t1_c2, t2.col2 as t2_c2
from tab1 t1 join tab2 t2 on t1.col1 = t2.col1
)
set t1_c2 = t2_c2;
1 row updated.
select * from tab1;
COL1 COL2
---------- ----------
1 6
So - you can do it, if you use the correct syntax (as I have shown here) AND - very important - you have a unique or PK constraint or a unique index on column tab2.col1.

Add a primary key column to an old table

So I have a table with some 50+ rows. And currently this tables doesnot have any primary key/ID column in it. Now if I have to add a primary key column, its not allowing me to because already data are present in the table and there is as such no unique column or combination of columns. Can anyone suggest me how to add a primary column to an existing table with data in it.
(From 12.1) You can add a new auto-incremented surrogate key to a table with either:
alter table t
add ( t_id integer generated by default as identity );
Or
create sequence s;
alter table t
add ( t_id integer default s.nextval );
These set the value for all the existing rows. So may take a while on large tables!
You should also look to add a unique constraint on the business keys too though. To do that, take the steps Marmite Bomber suggests.
In your case when the table due to missing PK definition suffers some duplicated records, you may do a stepwise recovery.
In the first step you disables the creation of the new duplicated rows.
Let's assume your PK candidate columns are col1, col2 such as in the example below:
CREATE TABLE test_pk as
SELECT 'A' col1, 1 col2 FROM dual UNION ALL
SELECT 'A' col1, 2 col2 FROM dual UNION ALL
SELECT 'B' col1, 1 col2 FROM dual UNION ALL
SELECT 'B' col1, 1 col2 FROM dual;
You can not define the PK because of the existing duplications
ALTER table test_pk ADD CONSTRAINT my_pk UNIQUE (col1, col2);
-- ORA-02299: cannot validate (xxx.MY_PK) - duplicate keys found
But you can crete an index on the PK columns and set up a constraint in the state ENABLE NOVALIDATE.
This will tolerate existing duplicates, but reject the new once.
CREATE INDEX my_pk_idx ON test_pk(col1, col2);
ALTER TABLE test_pk
ADD CONSTRAINT my_pk UNIQUE (col1,col2) USING INDEX my_pk_idx
ENABLE NOVALIDATE;
Now you may insert new unique rows ...
INSERT INTO test_pk (col1, col2) VALUES ('A', 3);
-- OK
... but you can't create new duplications:
INSERT INTO test_pk (col1, col2) VALUES ('A', 1);
-- ORA-00001: unique constraint (xxx.MY_PK) violated
Later in the second step you may decide to clenup the table and VALIDATE the constraint, which will make a perfect primary key as expected:
-- cleanup
DELETE FROM TEST_PK
WHERE col1 = 'B' AND col2 = 1 AND rownum = 1;
ALTER TABLE test_pk MODIFY CONSTRAINT my_pk ENABLE VALIDATE;

Oracle Unique Key - get PK or RowID

Is it possible to catch the master key or the record rowID that triggered the duplication exception?
table1 have PK: col1 and Unique1: col2
e.g.
begin
insert into table1(col1, col2, col3)
values (1, 2, 3);
exception
when dup_val_on_index then
--- here, can you somehow indicate either PK or ROWID of the record that generated the exception of uniqueness?
e.g.
update table1 set
col3 = 100
where rowid = "GETROWID" or col1 = "GETPK";
end;
In "normal" code you don't use constants to insert values; you'd normally have the value in a variable so your code would look more like:
DECLARE
strVar1 TABLE1%TYPE;
nVar2 NUMBER;
nVar3 NUMBER;
begin
SELECT s1, n2, n3
INTO strVar1, nVar2, nVar3
FROM SOME_TABLE;
insert into table1(col1, col2, col3)
values (strVar1, nVar2, nVar3);
exception
when dup_val_on_index then
update table1
set col3 = 100
where col1 = strVar1;
end;
But a better idea is to avoid the exception in the first place by using a MERGE statement:
MERGE INTO TABLE1 t1
USING (SELECT S1, N2, N3
FROM SOME_TABLE) s
ON (t1.COL1 = s.S1)
WHEN MATCHED THEN
UPDATE SET COL3 = 100
WHEN NOT MATCHED THEN
INSERT (COL1, COL2, COl3)
VALUES (s.S1, s.N2, s.N3);
The LOG ERRORS INTO could help here. You have to prepare an error table before using this clause:
begin
dbms_errlog.create_error_log('table1');
end;
That will create err$_table1 table. Now run the insert using additional feature
insert into table1(col1, col2, col3) values (1, 2, 3)
log errors into err$_table1 ('some_tag_to_look_for') reject limit unlimited;
Querying the err$_table1 with
select * from err$_table1;
won't give you the rowid (it gets filled for updates and deletes only) but you'll get the exact column values causing the error and the index violated.
If that is not enough you can find the indexed columns here
select * from all_ind_columns where owner='OWNER_NAME_from_error_table' and index_name = 'Name_from_err_table';
Thus you'll know which column(s) in the destination table already has the value you are trying to insert --> this is how you'll find the rowid or whatever you need

Insert data listing columns with partitioning field in Hive

First of all let's setup a test environment:
CREATE TABLE IF NOT EXISTS source_table (
`col1` TIMESTAMP,
`col2` STRING
);
CREATE TABLE IF NOT EXISTS dest_table (
`col1` TIMESTAMP,
`col2` STRING,
`col3` STRING
)
PARTITIONED BY (day STRING)
STORED AS AVRO;
INSERT INTO TABLE source_table VALUES ('2018-03-21 17:08:04.401', 'test1'), ('2018-03-22 12:02:04.222', 'test2'), ('2018-03-22 07:21:04.111', 'test3');
How could I list the column names during insertion and put the partition value dynamically? The following command doesn't work:
INSERT INTO TABLE dest_table(col1, col2) PARTITION(day) SELECT col1, col2, date_format(col1, 'yyyy-MM-dd') FROM source_table;
By the way, without listing the columns of dest_table inside the INSERT INTO command, having two tables with the same columns number, everything works fine. What if my dest_table has more fields than the source_table?
Thank you for helping me.
P.S.
Ok, if I hardcode NULL this works. I leave the question opened because there might be better ways to achieve that.
INSERT INTO TABLE dest_table PARTITION(day) SELECT col1, col2, NULL, date_format(col1, 'yyyy-MM-dd') FROM source_table;
Anyway, this method is strictly bounded with columns order? In a real-life scenario, how could I handle lots of columns specifying a mapping, to avoid mistakes?
The syntax for inserting into a partitioned table when you want to list the specific columns is shown below. You don't need to put null on col3 since Hive will put a default value NULL since it is not in the column list during insert.
INSERT INTO TABLE dest_table PARTITION (day)(col1, col2, day)
SELECT col1, col2, date_format(col1, 'yyyy-MM-dd') FROM source_table;
Result:
col1 col2 col3 day
2018-03-22 12:02:04.222 test2 NULL 2018-03-22
2018-03-22 07:21:04.111 test3 NULL 2018-03-22
2018-03-21 17:08:04.401 test1 NULL 2018-03-21

two triggers on one table

I am very new to oracle database my office is using oracle 10g. My question is
I have two tables one is current_cases having columns as case_id, col1, col2 col3..... another table backup_cases have backup_id, case_id, col1,col2,col3...
where case_id of current_cases is the same as case_id of backup_cases
I would like to create a trigger before update current_cases to insert all row the data into backup_cases, but there is already one more trigger on backup_cases to insert backup_sequence next value. Then how to create the update trigger, will the nextval trigger on backup_cases will automatically fill or should I over ride and take the sequence.next val an insert into the backup_cases. please give some idea about this small problem.....
...will the nextval trigger on backup_cases will automatically fill?
Trigger on backup_cases will work, but you must explicitly list all the inserted values, not this way: insert ... select * ....
Test: (everything is simplified, no primary keys, indices, foreign keys, constraints, just to address your question in short, readable way):
-- tables creation
create table current_cases (case_id number, col1 varchar2(20),
col2 varchar2(20));
create table backup_cases (backup_id number, case_id number, col1 varchar2(20),
col2 varchar2(20));
-- sequences creation
create sequence cc_seq;
create sequence bc_seq;
-- triggers
create or replace trigger bc_trg before insert on backup_cases
for each row
begin
select bc_seq.nextval into :new.backup_id from dual;
end;
create or replace trigger cc_trg before insert or update on current_cases
for each row
begin
if inserting then
select cc_seq.nextval into :new.case_id from dual;
else
insert into backup_cases (case_id, col1, col2)
values (:old.case_id, :old.col1, :old.col2);
end if;
end;
-- inserts and update sample data
insert into current_cases (col1, col2) values ('a1', 'a1');
insert into current_cases (col1, col2) values ('b1', 'b1');
insert into current_cases (col1, col2) values ('c1', 'c1');
update current_cases set col1 = 'b2a', col2='b2b' where case_id=2;
Results:
select * from current_cases;
CASE_ID COL1 COL2
---------- -------------------- --------------------
1 a1 a1
2 b2a b2b
3 c1 c1
select * from backup_cases;
BACKUP_ID CASE_ID COL1 COL2
---------- ---------- -------------------- --------------------
1 2 b1 b1
It doesn't look like you have anything to worry about. I assume there is an insert trigger on the backup table to generate the backup ID. I also assume there is an insert trigger on the current table to insert the incoming row also to the backup table. It may also be generating the current ID.
If you add an update trigger on the current table, it can write the NEW row to the backup table and everything should work normally. You don't need to make any changes to any existing trigger on either table.
If you have any doubts, this is a very easy operation to test.

Resources