* ** B-tree 인덱스 설명은 http://wiki.oracleclub.com/pages/viewpage.action?pageId=1507450 에서 사진 및 내용을 인용하였습니다.^^** ====== 제2장 인덱스의 유형과 특징 ====== * 인덱스란 말 그대로 전체 내용물 중에서 특정한 부분을 바로 찾을 수 있는 목차(目次)나 색인(索引)의 개념. * 테이블에 저장되어있는 데이터를 좀 더 효율적이고 빠르게 찾기 위해 사용. * 관계형 데이터베이스에서 적용하고 있는 인덱스 유형은 아래와 같음. * **B-Tree 인덱스** * **비트맵(Bitmap) 인덱스** * B-tree 클러스트 인덱스 * 해쉬(hash)클러스터 인덱스 * **리버스 키(Reverse key) 인덱스** * 비트맵 조인(Bitmap join) 인덱스 * **함수기반(Function-based) 인덱스** ===== 2.1 B-tree 인덱스 ===== * 생성 명령어 : **CREATE INDEX 인덱스명 ON 테이블명(컬럼명)** * 나무구조(Tree)처럼 가지에 해당하는 브랜치 블럭과 잎사귀에 해당하는 리프 블럭이 있다. ==== 2.1.1 B-tree 인덱스의 구조 ==== {{:study:oracle:datadb:b-tree1.jpg|}} :------------------------------------------------------------------------------------------------------ * Unique Scan일 경우 {{:study:oracle:datadb:b-tree2.jpg|}} * range Scan일 경우 - 인덱스는 구성컬럼과 ROWID로 정렬되어 있다. - ROWID에는 로우가 저장된 블록의 주소, 로우의 슬롯번호가 기록되어 있다. - 처리할 범위의 첫번째 인덱스로우를 브랜체블록을 통해 리프블록을 액세스한 다음 거기의 ROWID로 블록을 찾는다. - SGA부터 찾아보고 없으면 디스크에 액세스해서 복사본을 SGA에 복사한다. - 새롭게 액세스한 데이터블록정보를 PGA로 가져와서 해당 SQL의 처리버퍼에 넣는다 (일명 원 버퍼). - 인덱스블록을 액세스한 내용을 PGA버퍼에서 찾아 다른 조건까지 검증하여 성공한 로우만 운반단위로 보낸다 만약 클러스터링 팩터가 좋다면 여러건의 인덱스 스캔을 하나의 버퍼에서 처리할 수 있다. - PGA버퍼에서 찾을 수 없다면 다시 다른 블록을 SGA에 액세스하여 새로운 블럭을 담고 위의 처리를 반복수행한다. - 리프블록에서 액세스한 로우가 처리범위를 넘으면 처리를 종료한다. === ROWID의 구성 === * rowid라는 가상 컬럼에 저장된다. * Single row access를 위한 row 고유한 주소 * 10자리로 구성 {{:study:oracle:datadb:rowid.jpg|}} :------------------------------------------------------------------------------------------------------ ==== 2.1.2 B-tree 인덱스의 조작(Operation) ==== ===가.인덱스 생성(Creation)=== {{:study:oracle:datadb:b-tree3.jpg|}} - 테이블을 액새스하여 정렬을 수행. - 정렬된 결과를 인덱스 세그먼트의 리프 블록에 기록하기 시작. - 리프블록이 PCTFREE에 도달하여 새로운 블록을 요구하게 되면 브랜치블록을 생성. - 리프블록이 증가함에 따라 브랜치블록도 채워져 가다 블랜치블록이 PCTFREE에 도달하면 새로운 리프블록과 새로운 브랜치블록을 생성할때 루트블록이 생성된다. - 새로운 브랜치블록이 만들어질 때마다 루트블록에 브랜치블록의 DBA정보를 기록한다. * 위와 같은 방법으로 인덱스가 저장되기 때문에 인덱스 블럭에 많은 로우를 담게 될 경우 리프 블럭 감소, 브랜치 블럭 증가 둔화(블럭의 깊이 감소) == 인덱스 블록에 보다 많은 로우를 담기 위해 취할 수 있는방법 == * 최대한 인덱스 컬럼의 수를 줄인다. * 큰 블럭 사이즈(DB_BLOCK_SIZE)를 지정. (데이터베이스 전체, 혹은 테이블스페이스에 대한 정의이다.) * PCTFREE를 최소로 정의. (0으로 해도 무관함) * 압축을 활용하는 방법 CREATE INDEX ord_customer_idx ON orders (customer_id, sales_id) COMPRESS 1; :------------------------------------------------------------------------------------------------------ ===나.인덱스 블럭의 분할(SPILT)=== * 인덱스 로우는 정렬이 되어 저장 되어야 하는 이유 때문에 이미 생성된 구조에 새로운 로우가 삽입되면 기존의 위치에 파고 들어가는 문제 발생 {{:study:oracle:datadb:b-tree4.jpg|}} * 마지막 값이 삽입되는 경우 : 그림처럼 PCTFREE에 도달한 블럭일 경우엔 새로운 블럭에 등록된다. * 중간 값이 삽입되는 경우 : PCTFREE가 초과하게 되므로 분할이 발생하는데 어차피 2개의 블럭으로 수정되므로 2/3만 채우도록 하면서 양쪽을 모두 재편하게 됨(또 다른 중간 값이 들어와 계속 분할 되는 것을 방지하기 위함). * 하지만 분할이 일어난 블럭에 새로운 값이 들어오지 않으면 저장공간 낭비를 초래하게 되고 이를 해결하기 위해선 인덱스를 재생성하는 방법뿐이다. :------------------------------------------------------------------------------------------------------ ===다.데이터의 삭제 및 갱신=== * 데이터를 삭제했을 경우 테이블의 로우는 제거되지만 인덱스의 로우는 삭제되었다는 표시(flag)만 추가된다.(저장공간낭비와 스캔시 액세스 블럭 증가) * 한 리프 블럭이 모두 삭제 되었을 경우 브랜치 블럭에 해당 리프 블럭을 가리키는 로우도 삭제 표시가 된다. * 갱신할 경우 삭제와 삽입이 발생. * 이러한 이유로 수정이 빈번하지 않는 컬럼을 인덱스로 사용권장. * 데이터처리(DML)가 많이 수행되는 테이블은 정기적으로 재생성을 할 필요가 있다. :------------------------------------------------------------------------------------------------------ ===라.인덱스를 경유한 검색=== {{:study:oracle:datadb:b-tree5.jpg|}} * lmc는 브랜치 블럭의 첫 번째 로우의 값보다 적은 값을 갖는 하위의 블럭의 주소정보(dba:data block address)를 말한다. 1.루트 블록을 찾는다. 2.주어진 값보다 같거나 최소값을 찾는다.(찾는값 >= 인덱스값) 찾는값 < 인덱스 값 (Lmc에 있는 dba로 이동) 찾는 값 = 인덱스 값 (해당 dba로 이동) 찾는 값 > 인덱스 값 (검색 후 찾는 값 = 인데스 값이면 해당 dba로 이동 그렇지 않으면, 찾는 값 < 인데스 값을 만족하는 인덱스 최소값) 3.리프 블록을 찾을 때까지 ② 의 단계를 반복해서 수행. 4.리프 블록에서 주어진 값을 가진 키를 찾아 존재하면 ROWID를 이용해 테이블을 액세스하고, 그렇지 않으면 'No Data found'로 결과 반환 만약, col2의 조건을 'ACC'가 아닌 'AC%'로 바꾸면 Col1 = 'B'이면서 Col2 = 'AC'보다 같거나 큰 것에서 스캔을 시작, 'AD' 보다 작으면 테이블을 액세스하고, 그렇지 않으면 종료한다. :------------------------------------------------------------------------------------------------------ === 마.B-tree인덱스의 문제점 === * B-TREE인덱스에서는 실제 컬럼 값을 인덱스에도 보관하고 있어야 한다는 점이 대용량 데이터를 관리할 때 부담이 된다. * B-TREE인덱스 컬럼값의 분포도가 좋아야 한다는 점 * 결합인덱스에서 조건을 사용하지 않는 컬럼이나 =조건이 아닌 컬럼이 결합인덱스 중간에 있으면 액세스효율성이 떨어진다는 점 * 다양한 액세스패턴을 수용하기 위해 많은 인덱스가 필요할 수 있다는 점 * NOT이나 NULL을 사용하거나 복잡한 OR조건에서는 인덱스의 성능을 보장받지 못한다는 점 ===== 2.2 비트맵(Bitmap) 인덱스 ===== * 컴퓨터에서 사용하는 최소의 단위인 비트를 사용하여 컬럼 값을 저장하고 이를 이용하여 rowid를 자동으로 생성하는 인덱스 방법으로 비트를 직접 관리하므로 저장공간이 감소하고 비트연산이 용이 하다는 장점이 있다. * 생성 명령어 : **CREATE BITMAP INDEX … ON … ** ==== 2.2.1 비트맵(Bitmap) 인덱스의 탄생 배경 ==== * B-TREE인덱스가 가지는 문제점을 독립적으로 구성된 각각의 비트맵 인덱스가 조합되어 해결할 수 있다. ==== 2.2.2 비트맵(Bitmap) 인덱스의 구조와 특성 ==== === 구조 === * 루트 블록이나 브랜치 블록은 B-tree인덱스와 같은 구조로 되어 있으나 리프블록은 비트맵으로 구성되어 있다. {{:study:oracle:datadb:bitmap1.jpg|}} {{:study:oracle:datadb:k-4.jpg|}} === 특성 === * 비트를 저장할 때는 '선분 형태(Start rowid ~ End rowid)'로 저장을 하므로 일종의 압축 개념이 되며, 키 압축(**Key Compression**)이 적용되어 저장 공간이 매우 절약된다. * 비트맵 연산을 통해 처리하므로 OR 연산이 가능하다. * NULL 값도 BIT를 구성하고 있어 NULL 및 NOT 조건검색이 가능하다. * 수정이 빈번하게 발생하는 컬럼은 인덱스의 크기가 크게 증가하고 블록레벨 잠금(Block Level Locking)으로 인해 많은 부하가 유발될 수 있다. * 데이터 웨어하우스 업무에 주로 활용된다. ** >> 데이터 웨어하우스 ** [[http://100.naver.com/100.nhn?docid=717310]] === 생성절차 === - 인덱스를 생성하고자 하는 컬럼의 값들을 찾기 위해 테이블 스캔을 한 후 - bitmap generator에 의해 컬럼값, start rowid, end rowid , bitmap을 갖는 인덱스 엔트리를 생성한다. - 2단계에서 생성된 Bitmap들을 B-tree구조에 넣기 쉽도록 key값과 start rowid 순으로 정렬한다. - 마지막 단계에서는 정렬된 인덱스 엔트리들을 단순히 B-tree구조로 삽입한다. === 제한사항 == * Partition table에서 Global Index에는 Bitmap Index를 만들 수 없다. * Local Index를 구성할 경우에는 Bitmap Index로 만들 수 있으나, 여러 파티션에 걸쳐있는 Global Index를 구성할 때는 Bitmap Index를 만들 수 없다. * Bitmap Index는 RBO(rule base optimizer) mode에서는 사용될 수 없다. * Bitmap Index는 통계치에 의하여 산출된 cost를 기반으로 Bitmap Index의 사용여부를 결정하기 때문에 Optimizer Mode는 반드시 CBO(cost based optimizer) Mode인 ALL_ROWS 또는 FIRST_ROWS로 지정되거나 , CHOOSE mode인 경우 해당 object는 ANALYZE 명령을 통하여 통계정보를 생성해 주어야 한다. * IOT(Index Organized Table)에는 rowid를 가지고 있지 않기 때문에 rowid range로 표현되는 Bitmap Index를 사용할 수 없다. 그러나 Oracle9i부터는 ALTER TABLE .. MOVE MAPPING TABLE과 같이 mapping table을 이용하여 Bitmap Index를 사용할 수 있다. ==== 2.2.3 비트맵(Bitmap) 인덱스의 엑세스 ==== === 비트맵 인덱스 사용시 실행계획 오퍼레이션 === {{:study:oracle:datadb:bitmap2.jpg|}} === NOT,OR 이 포함된 쿼리에 실행계획 === {{:study:oracle:datadb:btmap3.jpg|}} - COL1 인덱스에서 '123'인 비트맵을 엑세스하고,COL2인덱스에서 'ABC'인 비트맵을 엑세스하여 감산연산을 수행한다(BITMAP MINUS) - COL2에서 NULL인 비트맵과 1의 결과를 다시 감산연산을 수행한다(BITMAP MINUS) - COL3 < 100 인 비트맵을 읽어 머지(BITMAP MERGE)를 수행하여 하나의 비트맵을 만든다. - **2**와 **3**에서 수행한 결과에 대해 OR연산(BITMAP OR)을 수행하여 조건을 만족하는 최종결과 비트맵을 만들고 ROWID로 변형하여 테이블을 엑세스 한다. ===== B-Tree 인덱스와 비트맵(Bitmap)인덱스의 비교 ===== {{:study:oracle:datadb:k-7.jpg|}} * OLTP : 네트워크상의 여러 이용자가 실시간으로 데이터베이스의 데이터를 갱신하거나 조회하는 등의 단위 작업을 처리하는 방식 ===== 2.3 리버스 키 인덱스 (Reverse key index) ===== {{:study:oracle:datadb:reverse.jpg|}} * 어떤 컬럼값의 각 바이트의 위치를 역전시키는 것을 말한다. * 리버스키 인덱스에서는 정렬이 크게 달라지므로 클러스터링 팩터가 나빠진다는 것을 의미한다. * '='로만 액세스하는 경우에라면 크게 문제되것이 없다 ===== 2.4 함수기반 인덱스(FBI, Function-Based index) ===== * 테이블의 컬럼들을 가공한 논리적 컬럼을 인덱스로 생성한 것. {{:study:oracle:datadb:44.jpg|}} ==== 2.3.1 함수기반 인덱스의 제약사항 ==== *비용기준 옵티마이져(CBO)에서만 사용가능한다. *함수기반 인덱스를 생성한 후 반드시 통계정보를 생성해야 한다. *사용자지정함수는 반드시 DETERMINISTIC으로 선언되어야 한다. *QUERY_REWRITE_ENABLE parameter 는 TRUE로 선언되어야 한다. * SQL>alter session set QUERY_REWRITE_ENABLE = TRUE; *QUERY_REWRITE_INTEGRITY parameter는 TRUSTED로 선언되어야 한다. * SQL>alter session set QUERY_REWRITE_INTEGRITY=TRUSTED; * 다음의 사용자 권한을 가져야한다. *인덱스 생성권한 : INDEX CREATE / ANY INDEX CREATE *쿼리 재생성권한 : QUERY REWRITE / GLOBAL QUERY REWRITE *함수나 수식의 결과가 NULL인 경우는 이 인덱스를 통해 액세스할 수 없다. *사용자 지정함수를 사용한 경우에는 종속성 유지에 주의해야 한다. *인덱스 정의에 사용된 사용자 함수가 재정의되었을 때 사용불가 상태가 된다. *인덱스 소유자의 실행 권한이 취소 되면 사용불가 상태가 된다. *옵티마이져가 사용불가 상태가 된 인덱스를 선택하면 SQL의 실행은 실패한다. *인덱스를 가용상태로 만들거나 재구축해야 한다. *인덱스를 미사용상태로 만든다 이때 SKIP_UNUSABLE_INDEXES파라미터가 TRUE로 선언되어 있어야 한다. *스칼라 서브쿼리로 표현된 컬럼은 함수기반 인덱스를 사용할 수 없다. *값이 상황에 따라 달라질 수 있는 SYSDATE, USER, ROWNUM 등의 가상컬럼이 포함되면 이 인덱스를 생성할 수 없다. *파티션을 적용한 경우에 파티션 키를 함수기반 인덱스에 사용할 수 없다. *숫자 컬럼을 문자 연산하거나 문자컬럼을 수치연산하는 수식의 경우에는 직접 기술하지 않았더라도 내부적으로 TO_CHAR, TO_NUMBER가 추가되어 처리된다. *함수기반 인덱스에서는 NLS파라메터를 현재 기준으로 적용하기 때문에 만약 세션 레벨에서 재정의를 한다면 잘못된 결과를 얻을 수도 있으므로 주의해야 한다. *WHERE 절에 기술된 컬럼이 표현된 것과 인덱스에 지정된 표현이 다르더라도 논리적으로 교환법칙이 성립하는 경우라면 같은 결과를 얻을 수 있다. ==== 2.3.2 함수기반 인덱스의 활용 ==== === 가.테이블 설계상의 문제를 해결 === == 컬럼의 중간부분을 검색 == * 컬럼의 분할 원칙을 준수하지 않았기 때문에 발생하는 문제에 대한 해결방안 . create index from_loc_idx on orders (substr(ship_id,5,3)); create index repair_loc_idx on orders (substr(ship_id,3,2), ord_date); == 조인 연결고리 컬럼이 대응하지 않는 경우의 해결 == * 정상적인 데이터 모델링을 수행했다면 나타날 수 없겠지만 현실에서는 가끔 등장하는 형태. 상위테이블의 품목분류테이블(itemgrop)에서는 상세한 관리를 위해 컬럼을 대분류(class1), 중분류(class2), 소분류(class3)으로 나누었으나 하위 테이블(items)에서는 컬럼이 늘어날 것을 염려하여 그룹코드(group_cd)라는 하나의 컬럼으로 생성한 예이다. 다음과 같이 조인을 할 경우 한쪽 연결고리에 이상이 발생하여 조인에 나쁜 영향을 미칠 수 있다. select ... from item_group x, items y where x.class1 || x.class2 || x.class3 = y.group_cd OR select ... from item_group x, items y where x.class1 = substr(y.group_cd,1,2) and x.class2 = substr(y.group_cd,3,2) and x.class3 = substr(y.group_cd,5,3) 위와 같은 경우 다음과 같은 함수기반 인덱스로 해결할 수 있다. create index group_cd_idx on item_group(x.class1 || x.class2 || x.class3 ); == 일자컬럼이 분할 된 경우의 해결 == * 일자컬럼이 함부로 년,월,일로 분할 되어 관리되는 경우 년,월,일을 결하여 비교할 수 밖에 없으므로 조인 연결고리 컬럼이 대응하지 않는 경우과 같아진다. create index sal_date_idx on sales (sal_yyyy || sal_mm || sal_dd); == 데이터 타입이 상이한 조인 컬럼 == * 표준화를 준수하고 도메인을 사전에 체계적으로 관리하였다면 발생하지 않을 확률이 높겠지만 어떻한 이유든 조인되어야 하는 테이블간의 조인키 컬럼의 데이터타입이 일치하지 않는 경우 컬럼의 내부적 변형이 발생하여 어느 한쪽의 인덱스가 사용될 수 없는 상태가 될 수 있다. 이런 경우 함수기반인덱스에서 한쪽 컬럼의 데이터형을 미리 가공하여 인덱스를 생성하면 해결할 수 있다. create index deptno_idx on emp (to_number(deptno)); == 조인컬럼이 경우에 따라 달라지는 경우의 조인 == * 아래와 같이 업무규칙에 따라 배타적으로 조인하는 경우가 발생했다면 조인시 컬럼이 달라질 수 있다. * 이때 드라이빙을 원하는대로 하기 위해 식에 함수기반 인덱스를 사용하면 가능하게 유도할 수 있다. select ... from sales s, departments d where d.deptno = (case when sal_type = 1 then sal_dept else agent_no end) and d.location = 'PUSAN'; create index deptno_idx on sales (case when sal_type = 1 then sal_dept else agent_no end); == 부모테이블의 컬럼과 결합한 인덱스 생성 == * 인덱스의 사용 시 두개의 인덱스를 동시에 쓰기어려운 제한 조건이 있었는데 하나의 선행인덱스가 사용된다면 다른 인덱스는 체크조건으로 사용된다는 점이다. (인덱스머지는 제외) select ... from movement x, movement_trans y where x.mov_order = y.mov_order and x.deptno = '12310' and y.mov_date like '200512%' 위와 같은 sql이 있다면 x.deptno = '12310'이 처리 주관 조건이 된다면 move_date like '200512%'는 체크기능의 역할만 하게 된다. 이때 두개의 조건이 함께 처리 주관조건이 된다면 아주 양호한 수행속도를 얻을 수 있다면 다음과 같은 함수기반인덱스를 이용하여 해결 할 수 있다. create or replace function get_deptno(v_mov_order in number) return varchar2 DETERMINISTIC is ret_val varchar2(5); begin select deptno into ret_val from movement where mov_order = v_mov_order; return ret_val; end get_deptno; create index dept_date_idx on movement_trans(get_deptno(mov_order),mov_date); * ** 단 함수내에 참조 테이블이 있을 경우 버전 별로 차이가 있으므로 주의해서 사용해야한다. ** === 나. 오류데이터의 검색문제를 해결 === == 대소문자나 공백이 혼재된 컬럼의 검색 == create index ename_upper_idx on employees (upper (ename)); == 불필요한 공백을 제거한 후 비교를 해야하는 경우 create index ename_trim_idx on employees(replace(ename,' ')); == 접두사를 채워서 검색 == * 전화번호 앞번호가 018이면 그냥 두고 아니면 016을 붙여서 비교할 때 create index call_number_idx on call_data (decode(substr(call_number,1,3),'018','','016')||call_number); === 다. 가공처리 결과의 검색 === == 복잡한 계산결과의 검색 == * 상품별로 데이터량이 많을 때 주문단가에서 할인금액을 뺀 실제 적용단가에 주문수량을 곱하여 금액별로 100개의 상품을 조회할때 복잡한 계산식을 함수기반인덱스로 만들어 결과값을 논리컬럼으로 저장해두면 힌트에서 인덱스를 사용할 수 있다. create index order_amount_idx on order_items(item_cd, (order_price-nvl(order_discount,0),0) * order_count)); select /*+ index_desc(x order_amount_idx) */ * from order_items x where item_cd = :b1 and rownum <= 100 * 말일, 단가, 율의 검색 : 해당월에 가장 높은 sal_amount가 발생한 건을 찾는 경우 create index sal_amount_idx on sales (last_day(sal_date), sal_amount); * 기간을 검색하는 경우 create index term_idx on activities (expire_date - start_date) * 길이를 검색하는 경우 . create index source_length_idx on print_media(text_length(source_text)); === 라. 오브젝트 타입의 인덱스 검색 === * 육면체(cube)를 오브젝트타입으로 생성하오 이것을 이용하여 부피를 구하는 예 길이,폭,높이 세가지 컬럼을 이용하여 부피를 구하는 volume()메소드를 정의 create type cube AS object ( length number, width number, heigth number, member function volume return number DETERMINISTIC ); create or replace type body cube as member function volume return number is begin return (length * width * height); end; end; cube유형(TYPE)으로된 CUBE_TAB 테이블을 생성하고, volumn() 메소드로된 함수기반 인덱스 생성 create table cube_tab of cube; create index volume_idx on cube_tab x (x.volume()); 인덱스를 경유하여 엑세스를 수행 select * from cube_tab x where x.volume() > 100 === 마. 베타적 관계의 인덱스 검색 === * 여기서 말하는 배타적(Exclusive)관계에 있는 컬럼이란 데이터 모델링 상에서 서로 다른 서브타입(Subtype)에 있는 컬럼들 중에서 동일한 속성으로 통합할 수 있는 컬럼들의 관계를 말한다. == 배타적 관계의 유일성 보장 == * 어떤 배타적 관계에 있는 속성들을 관찰하면 동일한 속성(subtype)으로 볼 수 있으며 이들을 결합했을 경우 반드시 존재해야한다거나 유일해야 한다는 원칙이 있을 수 있다. 이런경우 함수기반 인덱스를 생성하여 처리의 단순화 및 시스템을 통한 제약조건의 검증까지 보장받을 수 있다. * 배타적 관계에 있는 주민등록번호와 사업자등록번호를 결합하여 유일성을 체크할 수 있을 뿐 아니라 검색의 단순화를 얻을수 있는 예 create unique index official_id_idx on customers(case when cust_type =1 then resident_id else business_id end); select * from customers where (case when cust_type =1 then resident_id else business_id end) = :b1; * 특정한 범위의 데이터에 대해서만 유일성을 체크할도록 할 수 있는 사례. * 예를들어 'A01'의 보험상품에서는 고객이 한 번만 가입할 수 있다면 다음과 같은 방법으로 유일성을 체크할 수 있다. create unique index contract_idx on insurance ( case when then ins_type = 'A01' then customer_id else null end, case when then ins_type = 'A01' then ins_type else null end ); * 이 인덱스는 INS_TYPE ='A01'인 경우가 아니면 모두 NULL 값이 되므로 유일성을 체크하지 않는다. * 다음과 같은 입력이 발생하면 에러가 나타단다. insert into contact_person insuran_id, ..., customer_id, ins_type) values (122101, ..., 2101, 'A01'); 1row created. insert into contact_person (insuran_id, ..., customer_id, ins_type) values (122102, ..., 2101, 'A01'); ERROR at line 1; unique constraint (OE.CONTRACT_IDX) violated == 배타적관계의 결합인덱스 == * 어떤 구분값을 기준으로 결합인덱스의 구성이 달라진다면 함수기반인덱스로 해결할 수 있다. create index order_deliver_idx1 on order_delivery( order_dept, //고정된 선행컬럼 case when ord_type=1 then delivery_date else shipping_date end), // 중간 컬럼인덱스 item_type )