파티션과 대량 DML
파티션 Pruning과 인덱스 구분, EXCHANGE와 DROP PARTITION 및 Direct Path를 이용한 대량 DML
#핵심 요약
파티셔닝은 테이블, 인덱스 데이터를 파티션 키 값에 따라 별도 세그먼트로 나눠 저장하는 것임
파티션 단위 백업, 추가, 삭제로 관리가 쉬워지고, 필요한 파티션만 읽는 Pruning으로 I/O가 줌
Range(날짜 등 순서 있는 값, 가장 많이 씀), Hash(해시 함수로 균등 분산, 변별력 좋고 고르게 분포된 컬럼), List(불연속 값 목록을 직접 그룹핑)
로컬 인덱스는 테이블 파티션과 1:1로 대응해 파티션 구조를 바꿔도 자동으로 맞춰짐
글로벌 파티션 인덱스와 비파티션 인덱스는 파티션 구조를 바꾸면 UNUSABLE이 되어 REBUILD해야 함
대량 UPDATE는 수정된 값으로 임시 테이블을 만들어 파티션과 EXCHANGE하고, 대량 DELETE는 DROP PARTITION으로, 대량 INSERT는 해당 인덱스 파티션만 UNUSABLE로 둔 채 Direct Path로 넣은 뒤 그 파티션만 REBUILD함
#개념과 동작 원리
| 인덱스 구분 | 설명 |
|---|---|
| 로컬 파티션 인덱스 | 테이블 파티션과 1:1 add/drop/exchange 해도 자동으로 맞춰짐 |
| 글로벌 파티션 인덱스 | 테이블과 다른 기준으로 파티셔닝 테이블 파티션 구조를 바꾸면 UNUSABLE Prefixed만 지원 |
| 비파티션 인덱스 | 여러 테이블 파티션을 가리키므로 "글로벌 비파티션 인덱스"라고도 함 구조 변경 시 UNUSABLE |
| Prefixed / Nonprefixed | 인덱스 파티션 키 컬럼이 인덱스 선두에 있는지 여부 |
실습의 중요한 제약: Unique 인덱스를 파티셔닝하려면 파티션 키가 인덱스 컬럼에 포함되어야 함
PK에 파티션 키가 없으면 PK 인덱스를 로컬로 만들 수 없어 파티션 DROP, TRUNCATE, EXCHANGE마다 PK 인덱스가 UNUSABLE이 되므로, 대량 DML을 파티션으로 처리하려면 설계 단계부터 PK에 파티션 키를 넣어야 함
실습의 Pruning 실패 판별: 파티션 키 조건에 함수, 형변환을 쓰거나 다른 컬럼 조건만 있으면 PARTITION RANGE ALL, PARTITION HASH ALL처럼 모든 파티션을 읽고, Pstart/Pstop이 특정 파티션으로 좁혀지지 않음
Hash 파티션은 등치 조건이어야 Pruning됨
실습의 EXCHANGE 설명: 데이터를 물리적으로 복사하지 않고 딕셔너리 정보만 바꾸므로 건수와 무관하게 순식간에 끝남
인덱스가 로컬이어야 하고 임시 테이블 구조(컬럼, 제약)가 대상 파티션과 정확히 같아야 함
실습에서는 UPDATE 비중이 손익분기점(약 5%)을 넘으면 이 방식을 검토하라고 적었음
#필수 명령어 또는 SQL
#세 가지 파티션 만들기
CREATE TABLE emp_partition2
PARTITION BY RANGE (deptno)
( PARTITION p1 VALUES LESS THAN (20),
PARTITION p2 VALUES LESS THAN (30),
PARTITION p3 VALUES LESS THAN (40) )
AS SELECT * FROM emp;
CREATE TABLE 주문_해쉬파티션
( 주문번호 NUMBER, 주문일자 VARCHAR2(8), 고객ID VARCHAR2(5), 주문금액 NUMBER )
PARTITION BY HASH (고객id) PARTITIONS 8;
CREATE TABLE emp_job_p
( empno NUMBER(4) NOT NULL, ename VARCHAR2(10), job VARCHAR2(9), mgr NUMBER(4),
hiredate DATE, sal NUMBER(7,2), comm NUMBER(7,2), deptno NUMBER(2),
CONSTRAINT emp_job_p_pk PRIMARY KEY (empno) )
PARTITION BY LIST (job)
( PARTITION p_president VALUES ('PRESIDENT'),
PARTITION p_manager VALUES ('MANAGER'),
PARTITION p_analyst VALUES ('ANALYST'),
PARTITION p_salesman VALUES ('SALESMAN'),
PARTITION p_clerk VALUES ('CLERK'),
PARTITION p_def VALUES (DEFAULT) );SELECT partition_name, num_rows
FROM user_tab_partitions
WHERE table_name = 'EMP_JOB_P';결과 해석:
DEFAULT 파티션에는 목록에 없는 값(예: job = 'hr')이 들어감NUM_ROWS는 통계를 모은 뒤 채워짐
| 실습 비교 | 튜닝 전 | 튜닝 후 |
|---|---|---|
emp vs emp_partition2, deptno = 10 |
Buffers 6 | PARTITION RANGE SINGLE, Buffers 2 |
50만 건 비파티션 vs 해시 8개, 고객id = '00010' 건수 |
Buffers 2082 | PARTITION HASH SINGLE, Buffers 276 |
같은 해시 테이블, 고객id BETWEEN '00010' AND '00020' |
PARTITION HASH ALL(Pstart 1, Pstop 8), Buffers 2448 |
실습의 분기별 Range 파티션 예(주문일자 BETWEEN '20170401' AND '20170630')는 비파티션 테이블 Buffers 4, 파티션 테이블 Buffers 27이었음
두 실행계획 모두 A-Rows 50으로 첫 Fetch에서 멈춘 상태라 전체 조회 비교가 아님Pstart/Pstop을 보려면 실습에서는 DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'allstats last +partition')을 썼음
#파티션 DROP과 인덱스 상태
CREATE INDEX 주문_x01 ON 주문 (주문일자, 주문금액) LOCAL; -- 로컬 Prefixed
CREATE INDEX 주문_x02 ON 주문 (고객id, 주문일자) LOCAL; -- 로컬 Nonprefixed
CREATE INDEX 주문_x03 ON 주문 (주문금액, 주문일자) GLOBAL
PARTITION BY RANGE (주문금액)
( PARTITION p_1 VALUES LESS THAN (10000),
PARTITION p_mx VALUES LESS THAN (MAXVALUE) ); -- 글로벌 Prefixed
CREATE INDEX 주문_x04 ON 주문 (고객id, 배송일자); -- 비파티션ALTER TABLE 주문 DROP PARTITION P2017_Q1;SELECT index_name, status FROM user_indexes WHERE table_name = '주문';
SELECT index_name, partition_name, status
FROM user_ind_partitions
WHERE index_name = '주문_X03'
ORDER BY partition_name;ALTER INDEX 주문_X03 REBUILD PARTITION p_1;
ALTER INDEX 주문_X03 REBUILD PARTITION p_mx;결과 해석: 로컬 인덱스(x01, x02)는 그대로 쓸 수 있고, 글로벌(x03)과 비파티션(x04)은 UNUSABLE이 되어 그 인덱스를 쓰는 조회가 실패함(오류 메시지 확인 불가)
실습에서는 DROP PARTITION ... UPDATE INDEXES로 인덱스를 함께 유지하는 방법도 실습했음
비파티션 인덱스 x04의 재생성 문장은 기재 없음
#대량 UPDATE: EXCHANGE
실습 환경: 거래 500만 건, Range 파티션 P201412, P9999MX, 로컬 인덱스 거래_pk(Unique), 거래_x1, 거래_x2(상태코드)
일반 UPDATE(약 320만 건 대상)는 2분 31초였음
CREATE TABLE 거래_t NOLOGGING AS SELECT * FROM 거래 WHERE 1 = 2;
INSERT /*+ append */ INTO 거래_t
SELECT 고객번호, 거래일자, 거래순번,
CASE WHEN 상태코드 <> 'ZZZ' THEN 'ZZZ' ELSE 상태코드 END
FROM 거래
WHERE 거래일자 < '20150101';
COMMIT;
CREATE UNIQUE INDEX 거래_t_pk ON 거래_t (고객번호, 거래일자, 거래순번) NOLOGGING;
CREATE INDEX 거래_t_x1 ON 거래_t (거래일자, 고객번호) NOLOGGING;
CREATE INDEX 거래_t_x2 ON 거래_t (상태코드) NOLOGGING;
ALTER TABLE 거래 EXCHANGE PARTITION p201412 WITH TABLE 거래_t
INCLUDING INDEXES WITHOUT VALIDATION;DROP TABLE 거래_t;ALTER TABLE 거래 MODIFY PARTITION p201412 LOGGING;
ALTER INDEX 거래_pk MODIFY PARTITION p201412 LOGGING;
ALTER INDEX 거래_x1 MODIFY PARTITION p201412 LOGGING;
ALTER INDEX 거래_x2 MODIFY PARTITION p201412 LOGGING;결과 해석:
실습에서는 임시 테이블에 먼저 넣고 UPDATE하는 방법(1분 9초)을 거쳐, INSERT할 때부터 바꾼 값으로 넣는 위 방식으로 정리했음
EXCHANGE 방식의 소요 시간은 기재 없음WITHOUT VALIDATION은 임시 테이블 행이 파티션 범위에 맞는지 검사하지 않으므로, 임시 테이블에 범위 밖 행이 없도록 넣는 쪽이 책임짐NOLOGGING으로 만든 부분은 백업 전까지 매체 복구로 되살릴 수 없다는 점도 기재 없음
실습에서는 공간을 늘리려고 SYSTEM, UNDO, USERS 테이블스페이스에 데이터 파일을 추가했음
#대량 DELETE: DROP PARTITION
-- 튜닝 전: 행 단위 Undo/Redo, 인덱스 리프 삭제 (실습: 20만 건 중 68902건)
DELETE FROM 거래이력 WHERE 거래일자 < '20150101';
ROLLBACK;
-- 튜닝 후
ALTER TABLE 거래이력 DROP PARTITION P201412;결과 해석:
퀴즈의 요건: 삭제 기준 컬럼이 파티션 키와 같고, 파티션 단위와 삭제 주기가 맞고, 인덱스가 모두 로컬이어야 함
연습 문제(알림발송이력)의 조건은 발송일자 < TO_DATE('20240228', 'RRRRMMDD')인데 답은 1월 파티션(P202401)만 DROP해 2월 1일~27일 데이터가 남음
또 VARCHAR2 컬럼을 DATE와 비교함
#대량 INSERT: 인덱스 파티션만 UNUSABLE
ALTER TABLE target_t MODIFY PARTITION P201712 NOLOGGING;
ALTER INDEX target_t_x01 MODIFY PARTITION P201712 UNUSABLE;
INSERT /*+ append */ INTO target_t
SELECT * FROM source_t WHERE 주문일자 BETWEEN '20171201' AND '20171231';
COMMIT;
ALTER INDEX target_t_x01 REBUILD PARTITION P201712 NOLOGGING;
ALTER TABLE target_t MODIFY PARTITION P201712 LOGGING;
ALTER INDEX target_t_x01 MODIFY PARTITION P201712 LOGGING;결과 해석: 일반 INSERT(30만 건)는 약 4초였음(튜닝 후 시간은 확인 불가)
해당 인덱스 파티션만 UNUSABLE/REBUILD하므로 다른 파티션 서비스에는 영향이 없음
#비파티션 테이블: 제약과 인덱스 해제
ALTER TABLE target MODIFY CONSTRAINT target_pk DISABLE DROP INDEX;
ALTER INDEX target_x1 UNUSABLE;
INSERT /*+ append */ INTO target SELECT * FROM source;
COMMIT;
ALTER TABLE target MODIFY CONSTRAINT target_pk ENABLE NOVALIDATE;
ALTER INDEX target_x1 REBUILD ONLINE;결과 해석:
실습 기록: 1000만 건(10행 × 100만) 입력이 제약과 인덱스를 둔 채 3분 51초, 해제 후 29초ENABLE NOVALIDATE는 기존 데이터를 검사하지 않으므로(제약조건) 연습 문제가 요구한 "정합성 보장"을 이 문장만으로 충족하는지는 수업 자료에 근거가 없음