Skip to Main Content
  • Questions
  • Unique Function Based Index with NOVALIDATE

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question, Ed.

Asked: February 13, 2009 - 3:32 pm UTC

Last updated: December 08, 2010 - 1:23 pm UTC

Version: 10.2.0

Viewed 1000+ times

You Asked

I have a table upon which I'd like to place a unique index on two columns (call them A & B). However I already have some non-unique values. Great, so I can create an explicit or implicit non-unique index, and then create the NOVALIDATE unique constraint specified to use this index.

Here's the problem - I need the unique index to be function based so that it only checks uniqueness where a third column (call it column C) has a specific value (basically it's a versioned table, where we are only checking uniqueness on head records, as denoted by column C).

I tried creating the non-unique function based index first, but then the creation of the constraint fails with:

ORA-14196: Specified index cannot be used to enforce the constraint.

Is there any way to do this?

and Tom said...

... Here's the problem - I need the unique index to be function based so that it only checks uniqueness where a third column (call it column C) has a specific value (basically it's a versioned table, where we are only checking uniqueness on head records, as denoted by column C). ...

ah, what I call selective uniqueness.


create unique index I on t( case when C = that_value then f(a,b) end );

only index the rows of interest, and index them uniquely...


You don't even want to consider the novalidate path - what happens in the future when you need to rebuild this index for whatever reason? You cannot say "do not validate stuff before this date, only after this other date"

Rating

  (7 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Problem is Existing Non-Unique data

Ed, February 17, 2009 - 1:05 am UTC

I'm not sure this will completely meet my needs - this index would be perfect if I was starting from scratch (and I wish it had been there when the table was created). However we have data in the table which will cause the creation of this index to fail (i.e. there are currently rows where C=that_value, and A & B are not unique, and we'd strongly prefer not to get rid of these rows, as that would pose internal audit issues). This is why I had hoped to use a unique constraint with NOVALIDATE instead. Are you saying the only way to deal with this is to somehow wipe out the existing duplicate data?

Secondly, if I could somehow get the NOVALIDATE to work, I'm not sure the rebuilding of the index somewhere down the line should concern me. Presumably, in whatever situation would arise that would prompt me to want to rebuild this index, the scenario would be such that the index as I'd like to construct it would have done its job, and ensured that no further non-unique data should have entered the table from the time the index is deployed until the time that the need for rebuilding the index arises, so I don't see why rebuilding it with NOVALIDATE on the full data set that existed at that point would pose a problem. Or am I missing something
Tom Kyte
February 17, 2009 - 8:34 am UTC

think about this, what happens six months from now when you need to - for whatever reason - rebuild this index?

Is there something in the data that tells you - say a DATE - that tells you "this is old stuff"

If so, just add that to your predicate in the CASE statement.

Maybe you have a sequence in there as a primary key. Add "where that_column = special_value AND pk > 14123421" where 14123421 is the current "magic" high value of the sequence.


(to drop and create the index in the future would permit 'bad' data in, you want everything that exists as of TODAY to not be validated. everything into the FUTURE must be - meaning IT MUST be in the unique index - we have to know what to put in there and what NOT to put in there)

Selective Uniqueness

A reader, September 25, 2009 - 11:57 am UTC

Is there anyway to enforce selective uniqueness across tables? For example, for the table structures below, b.column2 has to be unique within the same a.aID relationship.

a.aID     b.bID      b.column2
--------  ---------  ---------
       1  1          abc      --> ok
       2  2          abc      --> ok
       1  2          abc      --> not ok!


CREATE TABLE a (
   aID     INTEGER NOT NULL PRIMARY KEY
);

CREATE TABLE b (
   bID     INTEGER NOT NULL PRIMARY KEY,
   column2 VARCHAR2(10) NOT NULL
);

CREATE TABLE ab_intersection (
   aID     INTEGER NOT NULL,
   bID     INTEGER NOT NULL,
   CONSTRAINT ab_intersection_fk1 FOREIGN KEY (aID) REFERENCES a(aID),
   CONSTRAINT ab_intersection_fk2 FOREIGN KEY (bID) REFERENCES b(bID),
   CONSTRAINT ab_intersection_pk  PRIMARY KEY (aID, bID)
);


INSERT INTO a VALUES (1);
INSERT INTO a VALUES (2);

INSERT INTO b VALUES (1, 'abc');
INSERT INTO b VALUES (2, 'abc');

-- --------------------------------------------------------------------
-- This is ok.
-- --------------------------------------------------------------------
INSERT INTO ab_intersection (aID, bID) VALUES (1, 1);

-- --------------------------------------------------------------------
-- This is ok.
-- --------------------------------------------------------------------
INSERT INTO ab_intersection (aID, bID) VALUES (2, 2);

-- --------------------------------------------------------------------
-- This is not ok since there is already a record with the same 
-- b.column2 value associated with the current a.aID (1).
-- --------------------------------------------------------------------
INSERT INTO ab_intersection (aID, bID) VALUES (1, 2);


Tom Kyte
September 29, 2009 - 11:13 am UTC

I don't not see a declarative way to do this.

What is the requirement driving this - give us the "spec" (not your data model, could be your data model is just plain wrong and the right one would do the right thing).


Selective Uniquess #2

A reader, September 29, 2009 - 1:00 pm UTC

I have a "product" and a "product group" entities. The relationship between these two entities is many-to-many. However, within the same product group, no two products can have the same name.
Tom Kyte
October 07, 2009 - 7:31 am UTC

how can two products have the same name? something seems fishy there.

Answer to Selective Uniqueness

Volker, October 01, 2009 - 12:51 pm UTC

To "A Reader":

Have you tried to replicate b.column2 into a new ab_intersection.b_column2 and create a ab_intersection-UK for aID and column2?
Having this and your existing ab_intersection_pk, you cannot have for one aID a bID twice (your PK) or a b_column2 twice (my UK).

But you have to support this replication from both sides.
In a draft version you may do it with two trigggers:
- one b."if updating column2"-trigger that pushes the update into corr. ab_intersection.b_column2-values.
- one a."if inserting or updating bID"-trigger that populates ab_intersection.b_column2 from b.column2

In a final version you should avoid this trigger-stuff and use packaged procedures instead (no write-grants on tables for app, only exec-grant for package).


correction

Volker, October 01, 2009 - 12:55 pm UTC

sorry, there is an error in ma last review:
second trigger has to be on ab_intersection, not on a

unique Functional Based Index

A reader, December 08, 2010 - 1:00 pm UTC

Hi,
We have similar Requirement, for a conditional check, and we proposed a Unique Functional based Index to handle and when this was forwarded to Development DBA,
CREATE UNIQUE INDEX TESTCONDLUNIQUECONSTRAINT ON REMITTER
(CASE WHEN ("RTE_NB_TX" IS NOT NULL AND "ACCT_NB_TX" IS NOT NULL) THEN "RTE_NB_TX"||"ACCT_NB_TX"||TO_CHAR("LOCKBOX_DDA_ID") WHEN ("RTE_NB_TX" IS NULL OR "ACCT_NB_TX" IS NULL) THEN "RMIT_NM"||"RMIT_CUST_ID_TX"||TO_CHAR("LOCKBOX_DDA_ID") END )

HE replied
"Conditional Function based Index has performance Issues :-

1. It will degrade the DML operations performance
2. It will never use index for any application SQLS

. I will recommend to use this logic in the package "

Tom, can you please highlight what are the possible scenarios a conditional Unique Function Based Index can impact performance,
Table in Question here in PROD can have some where between 30-40 mill rows.

Version is:- Oracle Database 10g Enterprise Edition Release 10.2.0.4.0 - 64bit

Table Structure in case you want to look.

CREATE TABLE REMITTER
(
REMITTER_ID NUMBER,
RTE_NB_TX VARCHAR2(100 BYTE) CONSTRAINT NN_RMT_RTNBTX NOT NULL,
ACCT_NB_TX VARCHAR2(100 BYTE) CONSTRAINT NN_RMT_ACCNBTX NOT NULL,
LOCKBOX_DDA_ID NUMBER CONSTRAINT NN_RMT_LOCDDID NOT NULL,
REMITTER_CUST_ID_TX VARCHAR2(100 BYTE),
ALT_CUST_ID1_TX VARCHAR2(100 BYTE),
ALT_CUST_ID2_TX VARCHAR2(100 BYTE),
STR_ADDR1_TX VARCHAR2(100 BYTE),
STR_ADDR2_TX VARCHAR2(100 BYTE),
CITY_TX VARCHAR2(100 BYTE),
ZIP_CD CHAR(10 BYTE),
STATE_CD VARCHAR2(50 BYTE),
CNTRY_TX VARCHAR2(100 BYTE),
STATUS_CD CHAR(1 BYTE) CONSTRAINT NN_RMT_STTCD NOT NULL,
OPT_OUT_IN CHAR(1 BYTE) CONSTRAINT NN_RMT_OPTOUTIN NOT NULL,
OCCUR_NB NUMBER DEFAULT 0 CONSTRAINT NN_RMT_OCCNB NOT NULL,
CRE_DT DATE CONSTRAINT NN_RMT_CRDT NOT NULL,
CRE_USR_ID NUMBER CONSTRAINT NN_RMT_CRUSRID NOT NULL,
LAST_UPDT_USR_ID NUMBER CONSTRAINT NN_RMT_LASUPDUSRID NOT NULL,
ATTN1_NM VARCHAR2(100 BYTE),
ATTN2_NM VARCHAR2(100 BYTE),
ALT_CUST_ID3_TX VARCHAR2(100 BYTE),
PARNT_NB_TX VARCHAR2(100 BYTE),
GL_ID_TX VARCHAR2(100 BYTE),
PARNT_RELTNSHP_NM VARCHAR2(100 BYTE),
REMITTER_NM VARCHAR2(100 BYTE),
CUST_CLASS_ID NUMBER,
LAST_PYMT_DT DATE,
LAST_UPDT_TS DATE
)

CREATE UNIQUE INDEX XPK_RMT ON R1APP18.REMITTER (REMITTER_ID)

CREATE UNIQUE INDEX XAK_RMT_RTENBTX_ACCNBTX_LOCDD ON R1APP18.REMITTER
(RTE_NB_TX, ACCT_NB_TX, LOCKBOX_DDA_ID)


ALTER TABLE R1APP18.REMITTER ADD (CONSTRAINT NN_RMT_LASUPDTS CHECK ("LAST_UPDT_TS" IS NOT NULL), CONSTRAINT XPK_RMT PRIMARY KEY (REMITTER_ID);


Thanks
Tom Kyte
December 08, 2010 - 1:23 pm UTC

... "Conditional Function based Index has performance Issues :-
...


tell him "so what?"

I mean, come on - a unique index on column X has performance issues.

INSERT has performance issues.

SELECTS - have performance issues.

Everything has a performance impact.

Ask this DBA how badly the application will perform due to the LOCK TABLE command it would have to do to implement this rule safely??? I mean, if that DBA is really truly saying "implement a unique constraint in your application code" - then they have missed the proverbial boat.

It will affect the modifications (DML operations by the way include SELECT) performance - sure, that is true.

However, it will affect their performance by some small percentage when compared to DOING IT YOURSELF.


This makes me so sad - this DBA cannot see the forest for the trees.

You have a data rule you need to enforce.

You can enforce it right in the database.
You could attempt to write code to do it.

Guess which one will be faster? Guess...


Who cares if the selects will not use it? It won't hurt them at all and it will make the modifications MUCH FASTER than if you tried to do this in code (ask the DBA - how will we do this in code - we'll need TWO MORE INDEXES to make it efficient - one on

rte_nb_tx, acct_nb_)tx, lockbox_dda_id

and one on

rmit_nm, rmit_cust_id_tx, localbox_dda_id

and those indexes will be HUGE - with as many entries as the table probably. Whereas your function based index will only index rows where one of those two columns is NULL.

In order to efficiently see if someone else has inserted that data yet - and that is AFTER we have to apply a lock table command - since reads don't block writes and writes don't block reads.



There you go - if you do it yourself - you need:

a) to execute the functions anyway - your procedural code would have to do the is null checking (so you haven't avoided the functions)

b) to lock the table

c) have TWO indexes

d) make sure everyone always uses your package to insert or update - forever, no table access ever - since the rules are not in the database


If you use the function based index (which would be smart) - you need:

a) to execute a function during insert or update of the affected column(s) and maintain a single index



Ask him "so how will your do it yourself approach be FASTER again???"



I don't know your data, but you would want to code like one of the two following:



CREATE UNIQUE INDEX TESTCONDLUNIQUECONSTRAINT ON REMITTER
(
 CASE WHEN ("RTE_NB_TX" IS NOT NULL AND "ACCT_NB_TX" IS NOT NULL)
      THEN "RTE_NB_TX"
      WHEN ("RTE_NB_TX" IS NULL OR "ACCT_NB_TX" IS NULL)
      THEN "RMIT_NM"
  END ,
 CASE WHEN ("RTE_NB_TX" IS NOT NULL AND "ACCT_NB_TX" IS NOT NULL)
      THEN "ACCT_NB_TX"
      WHEN ("RTE_NB_TX" IS NULL OR "ACCT_NB_TX" IS NULL)
      THEN "RMIT_CUST_ID_TX"
  END ,
 CASE WHEN ("RTE_NB_TX" IS NOT NULL AND "ACCT_NB_TX" IS NOT NULL)
      THEN "LOCKBOX_DDA_ID"                                    
      WHEN ("RTE_NB_TX" IS NULL OR "ACCT_NB_TX" IS NULL) 
      THEN "LOCKBOX_DDA_ID"                                       
  END 
)


CREATE UNIQUE INDEX TESTCONDLUNIQUECONSTRAINT ON REMITTER
(CASE WHEN ("RTE_NB_TX" IS NOT NULL AND "ACCT_NB_TX" IS NOT NULL) 
      THEN "RTE_NB_TX"||'/'||"ACCT_NB_TX"||'/'||TO_CHAR("LOCKBOX_DDA_ID") 
      WHEN ("RTE_NB_TX" IS NULL OR "ACCT_NB_TX" IS NULL) 
      THEN "RMIT_NM"||'/'||"RMIT_CUST_ID_TX"||'/'||TO_CHAR("LOCKBOX_DDA_ID")
END 


you have to make sure that the columns don't 'run into themselves'

eg: if rmit_nm = 'AA' and RMIT_CUST_ID_TX = 'B'
and some other:
rmit_nm = 'A' and RMIT_CUST_ID_TX = 'AB'

then they would appear "equal" unless you delimit them.

Thanks a Lot

A reader, December 08, 2010 - 4:12 pm UTC

Thanks a Lot for your Reply.