테이블 레코드를 입력하면 인덱스에도 입력해야 한다.
테이블은 Freelist를 통해 블록을 할당받으나 인덱스는 정렬된 자료구조라 수직적 탐색을 통해 입력할 블록을 찾아야 한다.
인덱스에 입력하는 과정이 더 복잡하므로 DML 성능에 미치는 영향은 크다.
테이블에서 레코드 하나 삭제시 인덱스 레코드를 모두 찾아서 삭제 해야한다.
변경된 컬럼을 참조하는 인덱스만 찾아서 변경해주면 된다.
대신, 테이블에서 한 건을 변경할 때마다 인덱스에는 두 개 오퍼레이션이 발생한다.
인덱스는 정렬된 자료구조이기 때문에 만약, A가 K로 변경되면 저장위치도 달라져서 삭제 후 삽입하는 방식으로 처리한다.
인덱스 갯수가 DML 성능에 미치는 영향이 매우 커서 인덱스 설계에 심혈을 기울여야 한다.
핵심 트랜잭션 테이블에서 인덱스 하나라도 줄이면 TPS는 그만큼 향상됨.
create table source
as
select b.no, a.*
from (select * from emp where rownum <= 10) a
, (select rownum as no from dual connect by level <= 100000) b;
-------------------------------------------------
create table target
as
select * from source where 1 = 2;
-------------------------------------------------
alter table target add
constraint target_pk primary key(no, empno);
set timing on;
insert into target
select * from source;
100000 개의 행이 만들어졌습니다.
경 과 : 00.00.04.95
truncate table target;
create index target_x1 on target(ename);
create index target_x2 on target(deptno, mgr);
insert into target
select * from source;
100000 개의 행이 만들어졌습니다.
경 과 : 00.00.38.98
데이터 무결성 규칙은 아래 4가지가 있다.
이들 규칙을 애플리케이션으로 구현할 수 있지만, DBML에서 PK, FK, Check, Not Null 같은 제약을 설정하면 완벽히 데이터 무결성을 지킬 수 있다.
PK, FK 제약은 Check, Not Null 제약보다 성능에 더 큰 영향을 미친다. Check, Not Null과 달리 PK, FK는 데이터를 실제로 조회해봐야 하기 때문.
앞의 테스트를 이어 일반 인덱스, PK 제약을 모두 제거하고 100만을 입력하면 속도가 빨라짐.
drop index target_x1;
drop index target_x2;
alter table target drop primary key;
truncate table target;
insert into target
select * from source;
100000개의 행이 만들어졌습니다.
경 과 : 00:00:01.32
아래는 조건절만 포함하는 가장 기본적인 DML문과 실행계획이다.
set autotrace traceonly exp
update emp set sal = sal * 1.1 where deptno = 40;
delete from emp where deptno = 40;
아래는 서브쿼리를 포함하는 DML 문과 실행계획이다.
update emp e set sal = sal * 1.1
where exists
(select 'x' from dept where deptno = e.deptno and loc = 'CHICAGO');
delete from emp e
where exists
(select 'x' from dept where deptno = e.deptno and loc = 'CHICACO');
insert into emp
select e.*
from emp_t e
where exists
(select 'x' from dept where deptno = e.deptno and loc = 'CHICAGO')
오라클은 데이터파일과 컨트롤 파일에 가해지는 모든 변경사항을 Redo 로그에 기록
Redo 로그는 트랜잭션 데이터가 어떤 이유로 유실됐을 때, 트랜잭션을 재현하여 유실 이전 상태로 북구하는데 사용된다.
DML을 수행할 때마다 Redo 로그를 생성해야 해서 Redo 로깅이 DML 성능에 영향을 줌.
insert 작업에 Redo 로깅 생략 기능이 있는데 왜 제공 되는지 이유가 있다.
과거에 롤백이라는 용어를 사용함. 오라클 9i부터 Undo 라는 용어를 씀
DML을 수행할때 Undo는 필수적으로 로깅이 되는데, 이것은 DML 성능에 영향을 미치나 Undo를 안남기는 방법은 없다.
Lock은 DML 성능에 매우 크고 직접적인 영향을 미친다.
동시성 제어
동시에 실행되는 트랜잭션 수를 최대화하고 입력, 수정, 삭제, 검색시 데이터 무결성을 유지하기 위해 노력하는 것을 말한다.
DML을 끝내려면 커밋까지 완료해야한다.
DML이 Lock에 의해 블로킹 된 경우 커밋은 DML 성능과 직결된다.
모든 DBMS가 Fast Commit을 구현하고 있다. 갱신한 데이터가 아무리 많아도 커밋은 빨리 처리한다.
Fast Commit의 도움으로 커밋을 순간적으로 처리하긴 하지만 커밋은 가벼운 작업이 아니다.
DB에 접속한 사용자를 대신해 모든 일을 처리하는 서버프로세스는 버버캐시를 읽고 쓴다.
버퍼캐시에서 변경된 블록을 모아 주기적으로 데이터파일에 일괄 기록하는 작업은 DBWR 프로세스가 함.
버퍼캐시는 휘발성이라 DBWR 프로세스가 Dirty 블록들을 데이터파일에 반영할때까지 불안한 상태이다.
하지만 버퍼캐시에 가한 변경사항을 Redo 로그에도 기록하면 된다. 버퍼캐시 데이터가 유실되어도 Redo 로그를 이용해 언제든 복구할 수 있다.
Redo 로그도 파일이다.
디스크 I/O는 느리다. Redo 로깅 성능 문제를 해결하기 위해 오라클은 로그 버퍼를 사용.
⇒ Redo 로그 파일에 기록하기 전 먼저 로그버퍼에 기록하는 방식임. 로그버퍼에 기록되고 난 후 LGWR 프로세스가 Redo 로그파일에 일괄(Batch) 기록함.
오라클은 데이터를 변경하기 전에 항상 로그부터 기록한다.
서버 프로세스가 버퍼블록에서 데이터를 변경(②)하기 전에 Redo 로그버퍼에 로그를 먼저 기록(①)하는 이유다.
DBWR 프로세스가 Dirty 블록을 디스크에 기록(⑤)하기 전에 LGWR 프로세스가 Redo 로그파일에 로그를 먼저 기록(④)하는 이유이기도 하다.
이를 'Write Ahead Logging' 이라고 부른다.
잠자던 DBWR와 LGWR 프로세스는 '주기적으로'깨어나 각각 Dirty 블록과 Redo 로그버퍼를 파일에 기록한다.
LGWR 프로세스는 서버 프로세스가 커밋을 발행했다가 '신호를 보낼때도' 깨어나서 활동을 시작한다.
'적어도 커밋시점에는' Redo 로그버퍼 내용을 로그파일에 기록한다는 뜻이다.
이를 'Log Force at Commit' 이라고 부른다.
서버 프로세스가 변경한 버퍼블록들을 디스크에 기록하지 않았더라도 커밋 시점에 Redo 로그를 디스크에 안전하게 기록했다면, 그 순간부터 트랜잭션 영속성은 보장된다.
커밋은 서버 프로세스가 그때까지 했던 작업을 디스크에 기록하라는 명령어다.
저장을 완료할 때까지 서버 프로세스는 다음 작업을 진행할 수 없다.
Redo 로그버퍼에 기록된 내용을 디스크에 기록하도록 LGWR 프로세스에 신호를 보낸 후 작업을 완료했다는 신호를 받아야 다음 작업을 진행할 수 있다. Sync 방식.
LGWR 프로세스가 Redo 로그를 기록하는 작업은 디스크 I/O 작업이다.
그래서 커밋은 생각보다 느리다.
오래 커밋하지 않은 채 데이터를 계속 갱신하면 Undo 공간이 부족해져 시스템 장애 상황을 유발할 수 있음.
루프를 돌면서 건건이 커밋한다면, 프로그램 자체 성능이 매우 느려짐.
트랜잭션을 논리적으로 잘 정의함으로써 불필요한 커밋이 발생하지 않도록 구현해야 한다.
2022년 6월 20일
SQL은 3단계로 나누어 실행된다.
Call이 어디서 발생하느냐에 따라 User Call과 Recursive Call로 나눌 수 있다.
User Call은 네트워크를 경유해 DBMS 외부로부터 인입되는 Call이다.
DBMS 입장에서 사용자는 WAS이다. 3Tier 아키텍처에서 User Call은 WAS 서버에서 발생하는 Call이다.
DBMS 내부에서 발생하는 Call
SQL 파싱과 최적화 과정에서 발생하는 데이터 딕셔너리 조회, PL/SQL로 작성한 사용자 정의 함수, 프로시저, 트리거에 내장된 SQL을 실행할 때 발생하는 Call이다.
위의 두 Call은 Parse, Execute, Fetch 단계를 거친다. 데이터베이스는 Call이 많을수록 성능은 느릴 수 밖에 없다. User Call이 성능에 미치는 영향은 매우 크다.
create table source
as
select b.no, a.*
from (select * from emp where rownum <= 10) a,
(select rownum as no from dual connect by level <= 100000) b;
create table target
as
select * from source where 1 = 2;
set timing on
begin
for s in (select * from source)
loop
insert into target values ( s.no, s.empno, s.ename,
s.job, s.mgr, s.hiredate,
s.sal, s.comm, s.deptno );
end loop;
commit;
end;
경 과 : 00:00:29:31
루프를 돌면서 건건히 call이 발생하지만, 네트워크를 경유하지 않은 Recursive Call이므로 29초만에 수행을 마침.
insert into target
select * from source;
-- 01.46초
업무로직이 복잡해지면 절차적으로 처리할 수 밖에 없지만, 그렇지 않으면 One SQL로 구현하는게 좋다.
One SQL은 구현하기 힘든데 Array Processing 쓰면 One SQL 안쓰고도 Call의 부하를 줄일 수 있다.
Array Processing 기능을 활용하면 한 번의 SQL (INSERT/UPDATE/DELETE) 다량의 레코드를 동시에 처리할 수 있다.
이는 네트워크를 통한 데이터베이스 Call을 줄이고, 궁극적으로 SQL 수행시간과 CPU 사용량을 획기적으로 줄여준다.
인덱스와 무결성 제약 조건은 DML 성능에 큰 영향을 끼친다.
근데 OLTP에서 이들 기능을 해제할 수 없다. 하지만 Batch 프로그램에서는 이 기능을 해제해서 큰 성능개선 효과를 얻을 수 있다.
총 : 20.87초
총 : 18.1초
다음과 같이 첫번째 쿼리를 두번째 쿼리처럼 고칠 수 있다.
하지만 아래의 쿼리들 모두 전통적인 update 방식이라 완전한 비효율을 없앨 수 없다.
update 고객 c
set 최종거래일시 = (select max(거래일시) from 거래
where 고객번호 = c.고객번호
and 거래일시 >= trunc(add_months(sysdate, -1)))
, 최근거래횟수 = (select count(*) from 거래
where 고객번호 = c.고객번호
and 거래일시 >= trunc(add_months(sysdate, -1)))
, 최근거래금액 = (select sum(거래금액) from 거래
where 고객번호 = c.고객번호
and 거래일시 >= trunc(add_months(sysdate, -1)))
where exists (select 'x' from 거래
where 고객번호 = c.고객번호
and 거래일시 >= trunc(add_months(sysdate, -1)));
update 고객 c
set (최종거래일시, 최근거래횟수, 최근거래금액) =
(select max(거래일시), count(*), sum(거래금액)
from 거래
where 고객번호 = c.고객번호
and 거래일시 >= trunc(add_months(sysdate, -1)))
where exists (select 'x' from 거래
where 고객번호 = c.고객번호
and 거래일시 >= trunc(add_months(sysdate, -1)));
update 고객 c
set (최종거래일시, 최근거래횟수, 최근거래금액) =
(select max(거래일시), count(*), sum(거래금액)
from 거래
where 고객번호 = c.고객번호
and 거래일시 >= trunc(add_months(sysdate, -1)))
where exists (select **/*+ unnest hash_sj */** 'x' from 거래
where 고객번호 = c.고객번호
and 거래일시 >= trunc(add_months(sysdate, -1)));
update 고객 c
set (최종거래일시, 최근거래횟수, 최근거래금액 ) =
(select nvl(max(거래일시), c.최종거래일시)
,decode(count(*), 0, c.최근거래횟수, count(*))
,nvl(sum(거래금액), c.최근거래금액)
from 거래
where 고객번호 = c.고객번호
and 거래일시 >= trunc(add_months(sysdate, -1)));
update
(select /*+ ordered use_hash(c) no_merge(t) */
c.최종거래일시, c.최근거래횟수, c.최근거래금액
, t.거래일시, t.거래횟수, t.거래금액
from (select 고객번호, max(거래일시) 거래일시, count(*) 거래횟수, sum(거래금액) 거래금액
from 거래
where 거래일시 >= trunc(add_months(sysdate, -1))
group by 고객번호) t, 고객 c
where c.고객번호 = t.고객번호
)
set 최종거래일시 = 거래일시
, 최근거래횟수 = 거래횟수
, 최근거래금액 = 거래금액 ;
create table emp as select * from scott.emp;
create table dept as select * from scott.dept;
select e.rowid emp_rid, e.*, d.rowid dept_rid, d.dname, d.loc
from emp e, dept d
where e.deptno = d.deptno;
update emp_dept_view set loc = 'SEOUL' where job = 'CLERK'
EMPNO ENAME JOB SAL DEPTNO DNAME LOC
---------- ---------- --------- ---------- ---------- -------------- -------------
7902 FORD ANALYST 3000 20 RESEARCH DALLAS
7788 SCOTT ANALYST 3000 20 RESEARCH DALLAS
7934 MILLER **CLERK** 1300 **10** ACCOUNTING NEW YORK
7369 SMITH **CLERK** 800 **20** RESEARCH DALLAS
7876 ADAMS **CLERK** 1100 **20** RESEARCH DALLAS
7900 JAMES **CLERK** 950 **30** SALES CHICAGO
7782 CLARK MANAGER 2450 10 ACCOUNTING NEW YORK
7566 JONES MANAGER 2975 20 RESEARCH DALLAS
7698 BLAKE MANAGER 2850 30 SALES CHICAGO
7839 KING PRESIDENT 5000 10 ACCOUNTING NEW YORK
7654 MARTIN SALESMAN 1250 30 SALES CHICAGO
7844 TURNER SALESMAN 1500 30 SALES CHICAGO
7521 WARD SALESMAN 1250 30 SALES CHICAGO
7499 ALLEN SALESMAN 1600 30 SALES CHICAGO
update emp_dept_view set comm = nvl(comm, 0) + (sal * 0.1) where sal <= 1500
=> ORA-01779: 키-보존된것이 아닌 테이블로 대응한 열을 수정할 수 없습니다.
이와 같이 PK 제약을 설정하면 EMP 테이블은 ‘키-보존 테이블’이 되고 DEPT는 ‘비 키-보존 테이블’이 된다.
alter table dept add constraint dept_pk primary key(deptno);
update emp_dept_view set comm = nvl(comm, 0) + (sal * 0.1) where sal <= 1500;
키 보존 테이블이란, 조인된 결과 집합을 통해서도 중복 값 없이 unique하게 식별히 가능한 테이블,그리고 뷰에 rowid를 제공하는 테이블이다.
-- 1번 쿼리
update
(select d.deptno, d.avg_sal d_avg_sal, e.avg_sal e_avg_sal
from (select deptno, round(avg(sal), 2) avg_sal from emp group by deptno) e
, dept d
where d.deptno = e.deptno)
set d_avg_sal = e_avg_sal;
**> ORA-01779: 키-보존된것이 아닌 테이블로 대응한 열을 수정할 수 없습니다**
-- 2번 쿼리 (bypass_ujvc 힌트)
update /*+ bypass_ujvc */
(select d.deptno, d.avg_sal d_avg_sal, e.avg_sal e_avg_sal
from (select deptno, round(avg(sal), 2) avg_sal from emp group by deptno) e
, dept d
where d.deptno = e.deptno)
set d_avg_sal = e_avg_sal;
DW에서 가장 흔히 발생하는 오퍼레이션은 기간계 시스템에서 가져온 신규 트랜잭션 데이터를 반영함으로써 두 시스템간 데이터를 동기화 하는 작업이다.
DW에 데이터 적재 작업을 효과적으로 지원하게 위해 오라클 9i부터 merge 문이 도입됨.
create table customer_delta
as
select * from customer
where mod_dt >= trunc(sysdate)-1
and mod_dt < trunc(sysdate);
merge into customer t using customer_delta s on (t.cust_id = s.cust_id)
when matched then update
set t.cust_nm = s.cust_nm, t.email = s.email, ...
when not matched then insert
(cust_id, cust_nm, email, tel_no, region, addr, reg_dt) values
(s.cust_id, s.cust_nm, s.email, s.tel_no, s.region, s.addr, s.reg_dt);
update와 insert를 선택적으로 처리할 수 있다.
merge into customer t using customer_delta s on (t.cust_id = s.cust_id)
**when matched then update**
set t.cust_nm = s.cust_nm, t.email = s.email, ...;
merge into customer t using customer_delta s on (t.cust_id = s.cust_id)
**when not matched then insert**
(cust_id, cust_nm, email, tel_no, region, addr, reg_dt) values
(s.cust_id, s.cust_nm, s.email, s.tel_no, s.region, s.addr, s.reg_dt);
on절에 기술한 조인문 외에 추가로 조건절을 기술할 수도 있다.
merge into customer t using customer_delta s on (t.cust_id = s.cust_id)
when matched then update
set t.cust_id = s.cust_id, t.cust_nm = s.cust_nm, t.email = s.email, ...
**where reg_dt >= to_char('20000101','yyyymmdd')**
when not matched then insert
(cust_id, cust_nm, email, tel_no, region, addr, reg_dt) values
(s.cust_id, s.cust_nm, s.email, s.tel_no, s.region, s.addr, s.reg_dt)
**where reg_dt < trunc(sysdate);**
merge into customer t using customer_delta s on (t.cust_id = s.cust_id)
when matched then update
set t.cust_id = s.cust_id, t.cust_nm = s.cust_nm, t.email = s.email, ...
**delete where t.withdraw_dt is not null** --탈퇴일시가 null이 아닌 레코드 삭제
when not matched then insert
(cust_id, cust_nm, email, tel_no, region, addr, reg_dt) values
(s.cust_id, s.cust_nm, s.email, s.tel_no, s.region, s.addr, s.reg_dt);