I have a situation where a field can be NULL when another field is certain values and for others it should be NOT NULL.
"Type" VARCHAR2(30) NOT NULL,
BestandLocatie VARCHAR2(150) NULL,
I made two constraints, the first one makes sure that only certain values in "Type" can be entered.
CONSTRAINT TypeCheck
CHECK ("Type" IN ('Tab', 'Bass Tab', 'Chords', 'Power Tab', 'Guitar Pro',
'Video Lesson', 'Drum Tab', 'Ukulele Chords')),
The other constraint (which gives an error, missing right parenthesis) should make sure that BestandLocatie is NOT NULL when "Type" is certain types:
CONSTRAINT BestandLocatieCheck
CHECK (BestandLocatie IS NOT NULL WHERE ("Type" IN ('Power Tab', 'Guitar Pro'
'Video Lesson')))
When I searched for the Where clause I only found examples of it in select statements. Does this mean that I can't use it here, is there an other method of doing this, or do I have to check this in the end application or can it only be done in PLSQL?
You can do something like this:
alter table foo add (constraint check_b
check ( (a in ('a', 'b') and b is not null)
or (a not in ('a', 'b') /* and b is null */)
)
);
The commented and b is null should be there depending on whether you want to require the value to be null in the other cases or not.
Demo:
SQL> create table foo (a varchar(2) not null, b varchar(2));
SQL> alter table foo add (constraint check_b check (
(a in ('a', 'b') and b is not null) or (a not in ('a', 'b') and b is null))
);
Table altered.
SQL> insert into foo values ('a', 'b');
1 row created.
SQL> insert into foo values ('a', null);
insert into foo values ('a', null)
*
ERROR at line 1:
ORA-02290: check constraint (MAT.CHECK_B) violated
SQL> insert into foo values ('c', null);
1 row created.
SQL> insert into foo values ('c', 'b');
insert into foo values ('c', 'b')
*
ERROR at line 1:
ORA-02290: check constraint (MAT.CHECK_B) violated
Related
I am having the below index defined on one of the tables in Oracle.
CASE WHEN C IS NOT NULL THEN A||','||B||','||C END
Initially, I thought the combination of A,B,C separated by commas should be unique when C is not null, but seems like I'm wrong. I did a bit of a research on this as well but could not find a good explanation.
Could someone kindly help me with this?
Thanks in Advance!!
Yes, it can be unique.
But don't forget - uniqueness is an additional cost.
There will be little benefit from such an index, since there will be duplicates of rows for fields A & B when C is NULL.
I suppose it might be worth changing the index function as :
a||','||b||case when c is not null then ','||c end
This will control duplicates if only columns A and B are used.
Of course, unless your table has other unique indexes using columns A and B.
For demonstration - a short example, with a unique functional index.
N.B. Please, when describing the problem /task, provide a description of the relevant objects (tables,indexes, procedures...etc.).)
For a test,try:
--ddl
drop table test_tab;
create table test_tab
( a varchar2(3) not null,
b varchar2(3) not null,
c varchar2(3),
idx varchar2(12) generated always as (case when c is not null then a||','||b||','||c end) virtual
);
--
create unique index test_tab_i1 on test_tab(idx);
--dml
insert into test_tab(a,b,c) values('a1','b1','c1');
insert into test_tab(a,b,c) values('a1','b2','c2');
insert into test_tab(a,b,c) values('a1','b1',null);
commit;
select * from test_tab;
ANALYZE TABLE test_tab COMPUTE STATISTICS FOR ALL INDEXES;
--get data of idx
select INDEX_NAME,NUM_ROWS,DISTINCT_KEYS from user_indexes i where i.table_name=upper('test_tab');
--dubt
insert into test_tab(a,b,c) values('a1','b1',null);
commit;
--
ANALYZE TABLE test_tab COMPUTE STATISTICS FOR ALL INDEXES;
--pass2
select * from test_tab;
--get data of idx
select INDEX_NAME,NUM_ROWS,DISTINCT_KEYS from user_indexes i where i.table_name=upper('test_tab');
--pass3
--dubt
insert into test_tab(a,b,c) values('a1','b1','c1');
Result:
Table created.
Index created.
1 row(s) inserted.
1 row(s) inserted.
1 row(s) inserted.
Statement processed.
Result Set 31
A B C IDX
a1 b1 c1 a1,b1,c1
a1 b2 c2 a1,b2,c2
a1 b1 - -
Download CSV
3 rows selected.
Statement processed.
Result Set 32
INDEX_NAME NUM_ROWS DISTINCT_KEYS
TEST_TAB_I1 2 2
Download CSV
1 row(s) inserted.
Statement processed.
Statement processed.
Result Set 33
A B C IDX
a1 b1 c1 a1,b1,c1
a1 b2 c2 a1,b2,c2
a1 b1 - -
a1 b1 - -
Download CSV
4 rows selected.
Result Set 34
INDEX_NAME NUM_ROWS DISTINCT_KEYS
TEST_TAB_I1 2 2
Download CSV
ORA-00001: unique constraint (SQL_XKMHFTSRXNSKMGVEDOYSVXZGL.TEST_TAB_I1) violated ORA-06512: at "SYS.DBMS_SQL", line 1721
Even if your question is vague, it is interesting. I will try to provide you a possible explanation.
A||','||B is unique if you can guarantee that for 2 distinct values of (A,B), the function A||','||B is not returning the same value.
You need to guarantee that TO_CHAR(A) ||','|| TO_CHAR(B) is unique.
I am considering that A and B are NOT NULL.
A||','||B is unique if A and B are VARCHAR but it is not the case if you have DATE or TIMESTAMP datatypes. This is not TRUE because TO_CHAR function has default parameters that depend on nls_parameters from your session.
Testcase - executing the same queries but changing the nls_date_format and try to create a unique index
drop table test;
create table test (a DATE, b VARCHAR2(10), C VARCHAR2(10));
alter session set nls_date_format= 'dd/mm/yyyy hh24:mi:ss';
drop index test;
truncate table test;
insert into test(a,b,c) values(sysdate+(1/60),'1','c1');
insert into test(a,b,c) values(sysdate+(1/160),'1','c1');
insert into test(a,b,c) values(sysdate+(1/80),'2','c2');
insert into test(a,b,c) values(sysdate+(1/180),'2','c2');
commit;
create unique index test on test (case when c is not null then a||','||b||','||c end);
select (case when c is not null then a||','||b||','||c end) valfn from test order by B;
alter session set nls_date_format= 'dd/mm/yyyy';
truncate table test;
drop index test;
insert into test(a,b,c) values(sysdate+(1/60),'1','c1');
insert into test(a,b,c) values(sysdate+(1/160),'1','c1');
insert into test(a,b,c) values(sysdate+(1/80),'2','c2');
insert into test(a,b,c) values(sysdate+(1/180),'2','c2');
create unique index test on test (case when c is not null then a||','||b||','||c end);
select (case when c is not null then a||','||b||','||c end) valfn from test order by B;
output with nls_date_format= 'dd/mm/yyyy hh24:mi:ss'
Index TEST created.
VALFN
------------------------
15/05/2021 08:40:35,1,c1
15/05/2021 08:25:35,1,c1
15/05/2021 08:34:35,2,c2
15/05/2021 08:24:35,2,c2
output with nls_date_format= 'dd/mm/yyyy'
ORA-01452: cannot CREATE UNIQUE INDEX; duplicate keys found
VALFN
---------------
15/05/2021,1,c1
15/05/2021,1,c1
15/05/2021,2,c2
15/05/2021,2,c2
Then we can see what happened when the indexes were created. We will query the index definition to explain this behavior.
output with nls_date_format= 'dd/mm/yyyy hh24:mi:ss'
alter session set nls_date_format= 'dd/mm/yyyy hh24:mi:ss';
drop table test;
create table test (a DATE, b VARCHAR2(10), C VARCHAR2(10));
create unique index test on test (case when c is not null then a||','||b||','||c end);
select index_name, column_expression from all_ind_expressions where table_name = 'TEST'
INDEX_NAME COLUMN_EXPRESSION
---------- ---------------------------------------------------------------------------------------------
TEST CASE WHEN "C" IS NOT NULL THEN TO_CHAR("A",'dd/mm/yyyy hh24:mi:ss')||','||"B"||','||"C" END
output with nls_date_format= 'dd/mm/yyyy'
alter session set nls_date_format= 'dd/mm/yyyy';
drop table test;
create table test (a DATE, b VARCHAR2(10), C VARCHAR2(10));
create unique index test on test (case when c is not null then a||','||b||','||c end);
select index_name, column_expression from all_ind_expressions where table_name = 'TEST'
INDEX_NAME COLUMN_EXPRESSION
---------- ----------------------------------------------------------------------------------
TEST CASE WHEN "C" IS NOT NULL THEN TO_CHAR("A",'dd/mm/yyyy')||','||"B"||','||"C" END
As you can see, the index is not the same because it depends on your nls_date_format when the index was created. I always suggest to validate what was the expression used by Oracle when creating the object.
This is the same when creating virtual columns
alter session set nls_date_format= 'dd/mm/yyyy hh24:mi:ss';
drop table test;
create table test (a DATE, b VARCHAR2(10), C VARCHAR2(10)
,idx varchar2(50) generated always as (case when c is not null then a||','||b||','||c end) virtual
);
select DATA_DEFAULT from all_tab_cols where table_name = 'TEST' and column_name = 'IDX';
DATA_DEFAULT
---------------------------------------------------------------------------------------------
CASE WHEN "C" IS NOT NULL THEN TO_CHAR("A",'dd/mm/yyyy hh24:mi:ss')||','||"B"||','||"C" END
alter session set nls_date_format= 'dd/mm/yyyy';
drop table test;
create table test (a DATE, b VARCHAR2(10), C VARCHAR2(10)
,idx varchar2(50) generated always as (case when c is not null then a||','||b||','||c end) virtual
);
select DATA_DEFAULT from all_tab_cols where table_name = 'TEST' and column_name = 'IDX';
DATA_DEFAULT
----------------------------------------------------------------------------------
CASE WHEN "C" IS NOT NULL THEN TO_CHAR("A",'dd/mm/yyyy')||','||"B"||','||"C" END
Now, you can add C to this equation and validate why in your case : CASE WHEN C IS NOT NULL THEN A||','||B||','||C END is not unique
Most of the time, by default the nls_date_format is not including the time.
Is there any way in Oracle that we can set Default column value when NULL only for New inserts? I don't want to change the Existing records if they have NULL.
I want to do this at table level. Not with NVL insert logic.
As far as I know, if you alter the table and set a default value for a column, it should only affect new records which would come in via an insert, not existing records.
ALTER TABLE yourTable MODIFY (col VARCHAR(100) DEFAULT 'some value');
Using the above approach, col values which are already NULL should remain NULL, at least from the point of view of inserts not changing those NULL values. And newly inserted records which do not specify a value for col should receive the default value some value.
Here's a demonstration which shows what's going on.
First, a test table and some inserts:
SQL> create table test (id number, col varchar2(10));
Table created.
SQL> insert into test (id, col) values (1, null);
1 row created.
SQL> insert into test (id, col) values (2, 'Littlefoot');
1 row created.
SQL> select * from test;
ID COL
---------- ----------
1
2 Littlefoot
Alter the table so that newly added rows contain 'some value' for the COL column:
SQL> alter table test modify col default 'some value';
Table altered.
OK; and now, important part of the story: pay attention to following:
SQL> -- this won't work as you wanted, because I explicitly inserted NULL into COL
SQL> insert into test (id, col) values (3, null);
1 row created.
SQL> -- this will work, because COL is omitted from the INSERT statement
SQL> insert into test (id) values (4);
1 row created.
SQL> select * From test;
ID COL
---------- ----------
1
2 Littlefoot
3
4 some value
SQL>
See? If you explicitly put NULL into a column, it won't get the default value.
However, if you were on 12c (I know, you aren't - just saying, for future reference), there's yet another option: DEFAULT ON NULL. It goes like this:
SQL> alter table test modify col default on null 'some value';
alter table test modify col default on null 'some value'
*
ERROR at line 1:
ORA-02296: cannot enable (SCOTT.) - null values found
Ooops! Won't work if there are NULLs in the column. I know #2 that you don't want to modify existing rows, but - for this demonstration, I'll do that:
SQL> update test set col = 'x' where col is null;
2 rows updated.
SQL> alter table test modify col default on null 'some value';
Table altered.
OK; let's see how it behaves: I'm explicitly inserting NULL into the column. In the previous example, it didn't put 'some value' in there, but left it NULL. How about now?
SQL> insert into test (id, col) values (5, null);
1 row created.
SQL> select * From test;
ID COL
---------- ----------
1 x
2 Littlefoot
3 x
4 some value
5 some value
Nice; we have 'some value' in the column.
Now you have some more info about the issue; see if it helps.
As Littlefoot mentioned, If you explicitly put NULL into a column, it won't get the default value.
If no value is mentioned for the column in the insert query, it uses DEFAULT. But, an explicit NULL overrides the default expression.
For 12c and above you can use the DEFAULT ON NULL option.
For prior versions, only way as far as I can tell is to replicate that functionality through a TRIGGER
CREATE TABLE YOURTABLE ( yourcolumn VARCHAR(100) );
CREATE OR REPLACE TRIGGER trg_mod_yourtabcol BEFORE
INSERT ON yourtable
FOR EACH ROW
WHEN ( new.yourcolumn IS NULL )
BEGIN
:new.yourcolumn := 'SOME DEFAULT VALUE';
END;
/
INSERT INTO YOURTABLE(yourcolumn) VALUES(NULL);
select * from YOURTABLE;
Table YOURTABLE created.
Trigger TRG_MOD_YOURTABCOL compiled
1 row inserted.
YOURCOLUMN
----------------------------------------------------------------------------------------------------
SOME DEFAULT VALUE
1 row selected.
I have a simple hierarchy structure in a table (Using Oracle Database 11g)
A
|-B
|-B1
|-C
|-D
Each node has a property associated with it (Y or N).
Traversing from the root node (parent), I want to get the first node in each branch that has the property Y.
For example:
A
|-B (N)
|-B1 (Y)
|--B2 (N)
|-C (Y)
|-C1 (Y)
|--C2 (N)
|-D (Y)
This should return B1, C and D.
Please give some ideas on how this can be done (code needs to be optimized for time).
There's probably a fancier way than using sys_connect_by_path, but I find it very easy to understand.
create table hier_test (id varchar2(2), parent_id varchar2(2), prop varchar2(1));
insert into hier_test values ('A', null, 'N');
insert into hier_test values ('B', 'A', 'N');
insert into hier_test values ('C', 'A', 'Y');
insert into hier_test values ('D', 'A', 'Y');
insert into hier_test values ('B1', 'B', 'Y');
insert into hier_test values ('B2', 'B1', 'N');
insert into hier_test values ('C1', 'C', 'Y');
insert into hier_test values ('C2', 'C1', 'N');
select id from (
select id, prop, sys_connect_by_path(prop, '>') as priors
from hier_test
connect by parent_id = prior id
start with id = 'A'
)
where prop = 'Y' and priors not like '%Y>%';
Edit: sqlfiddle
I have a table in Oracle with several constraints. When I insert a new record and not all constraints are valid, then Oracle raise only the "first" error. How to get all violations of my record?
CREATE TABLE A_TABLE_TEST (
COL_1 NUMBER NOT NULL,
COL_2 NUMBER NOT NULL,
COL_3 NUMBER NOT NULL,
COL_4 NUMBER NOT NULL
);
INSERT INTO A_TABLE_TEST values (1,null,null,2);
ORA-01400: cannot insert NULL into ("USER_4_8483C"."A_TABLE_TEST"."COL_2")
I would like to get something like this:
Column COL_2: cannot insert NULL
Column COL_3: cannot insert NULL
This would be also sufficient:
Column COL_2: not valid
Column COL_3: not valid
Of course I could write a trigger and check each column individually, but I like to prefer constraints rather than triggers, they are easier to maintain and don't require manually written code.
Any idea?
There no straightforward way to report all possible constraint violations. Because when Oracle stumble on first violation of a constraint, no further evaluation is possible, statement fails, unless that constraint is deferred one or the log errors clause has been included in the DML statement. But it should be noted that log errors clause won't be able to catch all possible constraint violations, just records first one.
As one of the possible ways is to:
create exceptions table. It can be done by executing ora_home/rdbms/admin/utlexpt.sql script. The table's structure is pretty simple;
disable all table constraints;
execute DMLs;
enable all constraints with exceptions into <<exception table name>> clause. If you executed utlexpt.sql script, the name of the table exceptions are going to be stored would be exceptions.
Test table:
create table t1(
col1 number not null,
col2 number not null,
col3 number not null,
col4 number not null
);
Try to execute an insert statement:
insert into t1(col1, col2, col3, col4)
values(1, null, 2, null);
Error report -
SQL Error: ORA-01400: cannot insert NULL into ("HR"."T1"."COL2")
Disable all table's constraints:
alter table T1 disable constraint SYS_C009951;
alter table T1 disable constraint SYS_C009950;
alter table T1 disable constraint SYS_C009953;
alter table T1 disable constraint SYS_C009952;
Try to execute the previously failed insert statement again:
insert into t1(col1, col2, col3, col4)
values(1, null, 2, null);
1 rows inserted.
commit;
Now, enable table's constraints and store exceptions, if there are any, in the exceptions table:
alter table T1 enable constraint SYS_C009951 exceptions into exceptions;
alter table T1 enable constraint SYS_C009950 exceptions into exceptions;
alter table T1 enable constraint SYS_C009953 exceptions into exceptions;
alter table T1 enable constraint SYS_C009952 exceptions into exceptions;
Check the exceptions table:
column row_id format a30;
column owner format a7;
column table_name format a10;
column constraint format a12;
select *
from exceptions
ROW_ID OWNER TABLE_NAME CONSTRAINT
------------------------------ ------- ------- ------------
AAAWmUAAJAAAF6WAAA HR T1 SYS_C009951
AAAWmUAAJAAAF6WAAA HR T1 SYS_C009953
Two constraints have been violated. To find out column names, simply refer to user_cons_columns data dictionary view:
column table_name format a10;
column column_name format a7;
column row_id format a20;
select e.table_name
, t.COLUMN_NAME
, e.ROW_ID
from user_cons_columns t
join exceptions e
on (e.constraint = t.constraint_name)
TABLE_NAME COLUMN_NAME ROW_ID
---------- ---------- --------------------
T1 COL2 AAAWmUAAJAAAF6WAAA
T1 COL4 AAAWmUAAJAAAF6WAAA
The above query gives us column names, and rowids of problematic records. Having rowids at hand, there should be no problem to find those records that cause constraint violation, fix them, and re-enable constraints once again.
Here is the script that has been used to generate alter table statements for enabling and disabling constraints:
column cons_disable format a50
column cons_enable format a72
select 'alter table ' || t.table_name || ' disable constraint '||
t.constraint_name || ';' as cons_disable
, 'alter table ' || t.table_name || ' enable constraint '||
t.constraint_name || ' exceptions into exceptions;' as cons_enable
from user_constraints t
where t.table_name = 'T1'
order by t.constraint_type
You would have to implement a before-insert trigger to loop through all the conditions that you care about.
Think about the situation from the database's perspective. When you do an insert, the database can basically do two things: complete the insert successfully or fail for some reason (typically a constraint violation).
The database wants to proceed as quickly as possibly and not do unnecessary work. Once it has found the first complaint violation, it knows that the record is not going into the database. So, the engine wisely returns an error and stops checking further constraints. There is no reason for the engine to get the full list of violations.
In the meantime I found a lean solution using deferred constraints:
CREATE TABLE A_TABLE_TEST (
COL_1 NUMBER NOT NULL DEFERRABLE INITIALLY DEFERRED,
COL_2 NUMBER NOT NULL DEFERRABLE INITIALLY DEFERRED,
COL_3 NUMBER NOT NULL DEFERRABLE INITIALLY DEFERRED,
COL_4 NUMBER NOT NULL DEFERRABLE INITIALLY DEFERRED
);
INSERT INTO A_TABLE_TEST values (1,null,null,2);
DECLARE
CHECK_CONSTRAINT_VIOLATED EXCEPTION;
PRAGMA EXCEPTION_INIT(CHECK_CONSTRAINT_VIOLATED, -2290);
REF_CONSTRAINT_VIOLATED EXCEPTION;
PRAGMA EXCEPTION_INIT(REF_CONSTRAINT_VIOLATED , -2292);
CURSOR CheckConstraints IS
SELECT TABLE_NAME, CONSTRAINT_NAME, COLUMN_NAME
FROM USER_CONSTRAINTS
JOIN USER_CONS_COLUMNS USING (TABLE_NAME, CONSTRAINT_NAME)
WHERE TABLE_NAME = 'A_TABLE_TEST'
AND DEFERRED = 'DEFERRED'
AND STATUS = 'ENABLED';
BEGIN
FOR aCon IN CheckConstraints LOOP
BEGIN
EXECUTE IMMEDIATE 'SET CONSTRAINT '||aCon.CONSTRAINT_NAME||' IMMEDIATE';
EXCEPTION
WHEN CHECK_CONSTRAINT_VIOLATED OR REF_CONSTRAINT_VIOLATED THEN
DBMS_OUTPUT.PUT_LINE('Constraint '||aCon.CONSTRAINT_NAME||' at Column '||aCon.COLUMN_NAME||' violated');
END;
END LOOP;
END;
It works with any check constraint (not only NOT NULL). Checking FOREIGN KEY Constraint should work as well.
Add/Modify/Delete of constraints does not require any further maintenance.
I'm stuck and can't figure out how to add the condition to the CHECK constraint in oracle...
Basically I have a table with the following structure:
CREATE TABLE TEST_TBL
(
COL_1 VARCHAR2(100) NOT NULL,
COL_2 VARCHAR2(100) NOT NULL,
COL_3 VARCHAR2(100) NOT NULL,
COL_4 VARCHAR2(100) NOT NULL,
DEF_COL CHAR(1) DEFAULT 'Y',
CONSTRAINT def_check_const
CHECK (???????)
);
There may be multiple values in this table, however I require that only 'Y' or 'N' can be entered in the DEF_COL column. Also i want to add a constraint to restrict the number of 'Y' in the DEF_COL column to only one. There may be multiple 'N' entries allowed for DEF_COL column. Please could someone guide me as to what must be done to add the constraint in the check section of the create table query?
Thanks in advance....
Oracle Setup:
CREATE TABLE TEST_TBL(
COL_1 VARCHAR2(100) NOT NULL,
COL_2 VARCHAR2(100) NOT NULL,
COL_3 VARCHAR2(100) NOT NULL,
COL_4 VARCHAR2(100) NOT NULL,
DEF_COL CHAR(1) DEFAULT 'Y'
CONSTRAINT def_check_const
CHECK ( DEF_COL IN ('Y', 'N' ) )
);
CREATE UNIQUE INDEX def_col_only_one_y__U
ON TEST_TBL( CASE DEF_COL WHEN 'Y' THEN 'Y' END );
Insert a row:
INSERT INTO TEST_TBL VALUES ( 'A1', 'A2', 'A3', 'A4', 'Y' );
1 rows inserted.
Insert a second column with DEF_COL = 'Y':
INSERT INTO TEST_TBL VALUES ( 'B1', 'B2', 'B3', 'B4', 'Y' )
Error report -
SQL Error: ORA-00001: unique constraint (TEST.DEF_COL_ONLY_ONE_Y__U) violated
00001. 00000 - "unique constraint (%s.%s) violated"
*Cause: An UPDATE or INSERT statement attempted to insert a duplicate key.
For Trusted Oracle configured in DBMS MAC mode, you may see
this message if a duplicate entry exists at a different level.
*Action: Either remove the unique restriction or do not insert the key.
Update:
To have only one Y for each unique combination of 3 columns then try:
CREATE UNIQUE INDEX C1_2_3__def_col_only_one_y__U
ON TEST_TBL( COL1, COL2, COL3, CASE DEF_COL WHEN 'Y' THEN 'Y' END );