inblog logo
|
Hye-Min Son
    SQL TUNING

    [TUNING-2] 튜닝2 (시스템 튜닝 및 성능 진단)

    쏘니's avatar
    쏘니
    Mar 31, 2024
    [TUNING-2] 튜닝2 (시스템 튜닝 및 성능 진단)
    Contents
    4장. 시스템 튜닝 및 성능진단■ 인덱스(Index) 튜닝INDEX UNIQUE SCANUNIQUE INDEX 그림으로 보기■ 조인 튜닝검색 조건이 있었을 때의 해쉬조인 순서는?SORT MERGE JOINJOIN 방법 섞어서 사용하기인라인뷰(from절의 서브쿼리)와 조인■ 서브쿼리(Sub-query) 튜닝■ SQL 재작성하여 튜닝하기■ 파티션 테이블(Partition table)Partition Table(파티션 테이블)
     
    📢 첨부된 사진들 출처
    • 초록색 테두리의 사진은 전부 itwill 유연수 강사님의 자료임을 알려드립니다.
     

     
    💡

    ★인덱스를 통해서 테이블 엑세스를 할 때 가장 염두하여야 할 것★

    “테이블 랜덤 엑세스를 줄일 수 있는 방법”을 생각해야 한다는 것
    💡
    인덱스 스캔 방법(힌트를 잘알아두면 80%의 sql은 튜닝 가능)
    1. INDEX UNIQUE SCAN
      1. : /*+ index( [테이블명] [인덱스명] ) */
         
    1. INDEX RANGE SCAN
      1. : /*+ index( [테이블명] [인덱스명] ) */
         
    1. INDEX FULL SCAN - 튜닝예제 29
      1. : /*+ index_fs( [테이블명] [인덱스명] ) */ 힌트 없어짐
         
    1. INDEX FAST FULL SCAN -튜닝예제 30
      1. : /*+ index_ffs( [테이블명] [인덱스명] ) */
         
    1. INDEX SKIP SCAN - 튜닝예제 26
      1. : /*+ index_ss( [테이블명] [인덱스명] ) */
         
    1. INDEX MERGE SCAN - 튜닝예제 22
      1. : /*+ and_equal( [테이블명] [인덱스명1] [인덱스명2] ) */
         
    1. INDEX BITMAP MERGE SCAN - 튜닝예제31
      1. : /*+ index_combine( [테이블명]) */
         
    1. INDEX JOIN SCAN - 튜닝예제32
      1. : /*+ index_join( [테이블명] [인덱스명1] [인덱스명2] ) */
     
    SQL튜너들은 이 내용을 A4용지에 넣어 머리에 잘 암기해 두어야 함
     
    ■ 주의사항
    힌트 사용시 /*+ */ 의 내부에 작성하는 내용과 앞뒤로 공백 하나씩 있어야함
     

    4장. 시스템 튜닝 및 성능진단

    ■ 인덱스(Index) 튜닝

     

    튜닝27. 인덱스의 클러스터링 팩터가 뭔지 알아야해요(DBA 필수 지식 ★)

    📖

    클러스터링 팩터(Clustering Factor)

    인덱스를 통해서 테이블의 데이터를 액세스 할 때. 액세스 하는 데이터가 얼마나 같은 블록안에 모여있는지 나타내는 수치
     
    • 클러스터링 팩터를 알아야하는 이유?
      • 이걸 알아야 실행계획을 볼 때 좋은 계획인지 아닌지 알 수 있음
      • 옵티마이져가 좋은 인덱스를 선택할 수 있도록 dba 가 도와주려면 이 내용을 잘알아야 합니다.
      •  
    클러스터링 팩터가 좋은 인덱스는 인덱스를 통해서 액세스 되는 테이블의 데이터가 같은 블록 안에 모여있는 경우이다.
    클러스터링 팩터가 작을 수록 좋은 인덱스. 클러스터링 팩터가 num_rows(테이블 데이터 건수)에 가까울 수록 안좋은 인덱스 왜? 만약 클러스터링 팩터가 테이블 데이터 건수와 같다면, 각 데이터가 각각 다른 블록 안에 들어 있는 것이기 때문
    notion image

    클러스터링 팩터 설명 그림

    https://cafe.daum.net/oracleoracle/Snri/101
    빨간 인덱스와 파란 인덱스 총 두 개의 인덱스 존재
     
    • 빨간 인덱스
      • : 101이라는 6건의 데이터를 찾기 위해서 테이블 내의 보라색으로 된 블록(오라클은 데이터를 저장할 때 블록이라는 저장단위에 저장함)에 접근!
        5개 씩 블록에 저장되어 있는데, 빨간 인덱스는 101을 찾기 위해 2개의 블록을 읽음.
    • 파란 인덱스
      • : 940101이라는 3간의 데이터를 찾기 위해서 테이블의 블록을 3개 읽게 됨.
        블록을 적게 읽을수록 buffer의 수가 작은거니까 빨간 인덱스 보다 상대적으로 좋지 않은 인덱스임을 알 수 있음
     
    따라서 적은 블록을 읽어오는 빨간 인덱스를 더 좋은 인덱스라고 볼 수 있음
     
    “내가 원하는 데이터들이 같은 블록 안에 모여있다 = 클러스터링 팩터가 좋다는 의미” (집단 팩터, 응집된 팩터 …)
     

    실습

    • 실습 환경 구성(클러스터링 팩터 환경 구성)
      • * clustering factor 테스트 create table emp5000 pctfree 0 as select t2.*, lpad('x', 630) x from dba_objects t2 --많은 데이터가 들은 데이터 사전 order by object_id; --object_Id를 기준으로 예쁘게 asc하게 정렬 즉, 데이터가 블록에 차례로 들어감 create table emp6000 pctfree 0 as select t2.*, lpad('x', 630) x from dba_objects t2 order by dbms_random.value; --데이터를 랜덤하게 생성하게하여 데이터를 흩어지게 즉, 여러 블록에 흩어지게함 create index emp5000_object_id on emp5000(object_id); --인덱스 통해서 데이터를 엑세스할 때 효과가 좋은 인덱스 create index emp6000_object_id on emp6000(object_id); --인덱스 통해서 데이터를 엑세스할 때 효과가 좋지 않은 인덱스 * 위의 인덱스의 클러스터링 팩터를 조사한다. select i.index_name, t.blocks, i.clustering_factor, t.num_rows from user_indexes i, user_tables t where i.table_name = t.table_name and t.table_name in ('EMP5000','EMP6000'); ================================================================ INDEX_NAME |BLOCKS|CLUSTERING_FACTOR|NUM_ROWS| -----------------+------+-----------------+--------+ EMP5000_OBJECT_ID| 7929| 7797| 76183| EMP6000_OBJECT_ID| 7753| 76172| 76184| ================================================================ **CLUSTERING_FACTOR : 작을 수록 좋은 것. **CLUSTERING_FACTOR < NUM_ROWS : num_rows보다 작으면 작을 수록 좋은 인덱스 EMP6000_OBJECT_ID 는 num_rows(테이블 건수)와 거의 비슷하기 때문에 데이터가 모든 블록에 흩어졌다고 볼 수 있음
        notion image
     
     

    문제풀며 익히기

     
    Q1.
    emp 테이블에 deptno와 empno에 각각 인덱스를 생성하고, 두 개의 인덱스 중에서 어느 인덱스가 클러스터링 팩터가 좋은지 확인하시오.
    @demo create index emp_deptno on emp(deptno); create index emp_empno on emp(empno); select i.index_name, t.blocks, i.clustering_factor, t.num_rows from user_indexes i, user_tables t where i.table_name = t.table_name and t.table_name = 'EMP';
    Q2.
    아래의 환경을 만들고 아래의 인덱스들의 클러스터링 팩터를 확인하시오.
    drop table mcustsum purge; create table mcustsum as select rownum custno , '2008' || lpad(ceil(rownum/100000), 2, '0') salemm , decode(mod(rownum, 12), 1, 'A', 'B') salegb , round(dbms_random.value(1000,100000), -2) saleamt from dual connect by level <= 1200000 ; create index m_indx1 on mcustsum(custno); create index m_indx2 on mcustsum(salemm); create index m_indx3 on mcustsum(salegb);
    select i.index_name, t.blocks, i.clustering_factor, t.num_rows from user_indexes i, user_tables t where i.table_name = t.table_name and t.table_name = 'MCUSTSUM'; INDEX_NAME|BLOCKS|CLUSTERING_FACTOR|NUM_ROWS| ----------+------+-----------------+--------+ M_INDX1 | 3899| 3829| 1200000| M_INDX2 | 3899| 3830| 1200000| M_INDX3 | 3899| 7658| 1200000|
    notion image
     
     
     
     

    튜닝28. 클러스터 테이블 생성하기

    📖

    클러스터 테이블 생성하기

    검색이 정말 중요한 테이블이면, 테이블을 구성할 때 클러스터 테이블로 생성할 수 있다. DBA는 검색 속도가 중요한 테이블을 클러스터 테이블로 생성할 것을 고려해야함.
     
    클러스터 팩터를 좋게 하려고 아예 테이블로 만든것
    옵티마이저는 클러스터 인덱스를 좋아함
     

    클러스터 테이블

    notion image
    https://cafe.daum.net/oracleoracle/Snri/103
    클러스터링 팩터가 좋게 아예 테이블로 만들어버림
     
    HEAP TABLE = 일반 테이블(범위?테이블)
    CLUSTER TABLE = 위와 그림과 같음
    CLUSTER TABLE ‘S INDEX = 값들이 UNIQUE함(그래서 인덱스 유니크스캔?)
    CLUSTER BLOCK = 오라클 보라색 블록 뒤에 보이는 네모들이 클러스터 블록
     
    클러스터 인덱스에 대한 키를 기준으로 데이터들이 모여있음
    클러스터 블록 안에 동일한 범주의 값들이 나눠져 들어가있음
     
     

    실습—여러 테이블 생성 후

    실습 환경 구성(클러스터 테이블 생성하기1) —실습 안함
    튜닝후 : 1. 클러스터를 생성한다. alter table sales modify prod_id number; alter table products modify prod_id number; create cluster c_prod_id ( prod_id number ) index; 2. 클러스터 인덱스를 생성한다. create index i_prod_id on cluster c_prod_id ; 3. 클러스터 테이블을 생성한다. create table sales_cluster cluster c_prod_id(prod_id) as select * from sales; create table products_cluster cluster c_prod_id(prod_id) as select * from products ; select * from emp_cluster; select * from dept_cluster; 튜닝전 : SELECT /*+ gather_plan_statistics leading(p s) use_nl(s) */ p.prod_name, sum(s.amount_sold) from sales s, products p where s.prod_id = p.prod_id and p.prod_id = 40 group by p.prod_name; 튜닝후 : SELECT /*+ gather_plan_statistics leading(p s) use_nl(s) */ p.prod_name, sum(s.amount_sold) from sales_cluster s, products_cluster p where s.prod_id = p.prod_id and p.prod_id = 40 group by p.prod_name;
    다중 테이블 인덱스 클러스터—실습 안함
    ■ 다중 테이블 인덱스 클러스터 여러 테이블들의 레코드를 하나의 물리적 공간에 같이 저장해두는 방식 " 여러 테이블들을 서로 조인된 상태로 저장해둔다 " * emp 와 dept 를 가지고 다중 클러스터 테이블 구현 1. 클러스터를 생성한다. alter table emp modify deptno number(10); alter table dept modify deptno number(10); create cluster c_deptno ( deptno number(10) ) index; 2. 클러스터 인덱스를 생성한다. create index i_deptno on cluster c_deptno ; 3. 클러스터 테이블을 생성한다. create table emp_cluster cluster c_deptno(deptno) as select * from emp; create table dept_cluster cluster c_deptno(deptno) as select * from dept ;
    해쉬 클러스터 테이블 생성—실습 안함
    ▣ 해쉬 클러스터 테이블 생성 1. 해쉬 클러스터 생성 create cluster username_cluster# ( username varchar2(30) ) hashkeys 100 size 50; 2. 클러스터 테이블 생성 create table user_cluster cluster username_cluster# ( username ) as select * from all_users; 3. 일반 테이블을 생성( 일반 테이블과 해쉬 테이블의 성능을 비교하려고) create table user_regular as select * from all_users; create unique index user_regular_idx on user_regular(username); alter table user_regular modify user_id null; alter table user_cluster modify user_id null; 4. 일반 테이블과 해쉬 클러스터 테이블의 성능 비교 테스트 PL/SQL t.sql 과 tf.sql 과 trace_file.sql 과 tk.sql 이 필요한데 모두 SQL trace 를 뜨는 스크립트입니다. @t.sql declare l_user_id user_regular.user_id%type; begin for c in (select owner from all_objects where owner <> 'PUBLIC') loop select user_id into l_user_id from user_regular where username = c.owner; select user_id into l_user_id from user_cluster where username = c.owner; end loop; end; / @tf.sql @trace_file.sql @tk
    클러스터 테이블 만들기 총정리 스크립트—-이것만 사용
    -- 테이블 통계 계산 ANALYZE TABLE emp COMPUTE STATISTICS; -- 테이블 평균 로우 길이 조회 SELECT AVG_ROW_LEN FROM user_tables WHERE table_name='EMP'; -- 클러스터 키당 평균 ROW 수 조회 SELECT AVG(COUNT(*)) FROM emp GROUP BY job; -- CLUSTER_SIZE 계산 -- (AVG_ROW_LEN * AVG(COUNT(*))) * 1.1 = 123.2 -- 클러스터 및 클러스터 인덱스 생성 CREATE CLUSTER empclujob(job VARCHAR2(9)) PCTFREE 10 PCTUSED 60 SIZE 123; CREATE INDEX empclujobi ON CLUSTER empclujob PCTFREE 5 STORAGE(INITIAL 20k NEXT 10k); -- 클러스터 테이블 생성 CREATE TABLE emp_cluster( 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) ) CLUSTER empclujob(job); -- 데이터 복사 INSERT INTO emp_cluster SELECT * FROM emp; -- 테이블 및 클러스터 정보 조회 SELECT table_name, cluster_name FROM user_tables WHERE table_name='EMP_CLUSTER'; SELECT * FROM user_clusters; -- 예제 실습 -- 직업이 'ANALYST'인 직원 조회 SELECT * FROM emp_cluster WHERE job = 'ANALYST'; -- SUM(sal) 결과 조회 SELECT SUM(sal) FROM emp_cluster WHERE job = 'SALESMAN';
     
    직업으로 데이터를 빠르게 검색하는게 중요하다고 하면, emp 테이블을 다음과 같이 JOB을 클러스터 인덱스로 한 클러스터 테이블을 생성함
     
    예제1) 클러스터 테이블 만들기
    #1. 클러스터 테이블을 만들려면 클러스터 사이즈를 계산해야함(클러스터 블록 사이즈) -- 테이블 통계 계산(EMP테이블에 대한 통계정보 수집. 즉, EMP테이블 분석) ANALYZE TABLE emp COMPUTE STATISTICS; -- 테이블 평균 로우 길이 조회(EMP테이블의 가로사이즈 구하기) SELECT AVG_ROW_LEN FROM user_tables WHERE table_name='EMP'; /* AVG_ROW_LEN ----------- 40 USER_TABLE : 데이터 사전 */ -- 클러스터 키당 평균 ROW 수 조회(JOB의 평균 건수) SELECT AVG(COUNT(*)) FROM emp GROUP BY job; /* AVG(COUNT(*)) ------------- 2.8 */ -- CLUSTER_SIZE 계산 SQL> SELECT 40 * 2.8 * 1.1 FROM DUAL; 40*2.8*1.1 ---------- 123.2 -- (AVG_ROW_LEN * AVG(COUNT(*))) * 1.1 = 123.2 -- 클러스터 및 클러스터 인덱스 생성 CREATE CLUSTER empclujob(job VARCHAR2(9)) PCTFREE 10 PCTUSED 60 SIZE 123; /* 직업을 담는 EMPCLUJOB이라는 클러스터를 만들고 SIZE는 계산한 값의 소수점 이하자리는 날림 */ CREATE INDEX empclujobi ON CLUSTER empclujob PCTFREE 5 STORAGE(INITIAL 20k NEXT 10k); /* CREATE INDEX [클러스터이름] ON CLUSTER PCTFREE 5 STORAGE(INITIAL 20k NEXT 10k); */ -- 클러스터 테이블 생성 CREATE TABLE emp_cluster( 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) ) CLUSTER empclujob(job); /* 클러스터 테이블 이름을 정하고 EMP테이블과 같이 8개 컬럼 넣고 CLUSTER empclujob(job) 즉, CLUSTER [클러스터이름(KEY가 될 컬럼명)]; 즉, 직업을 기준으로 예쁘게 정렬된 클러스터 테이블 만드는 것 */ -- 데이터 복사(EMP테이블 내용 -> EMP_CLUSTER에 넣음) INSERT INTO emp_cluster SELECT * FROM emp; -- 테이블 및 클러스터 정보 조회 <방법 1> SELECT table_name, cluster_name FROM user_tables WHERE table_name='EMP_CLUSTER'; TABLE_NAME |CLUSTER_NAME| -----------+------------+ EMP_CLUSTER|EMPCLUJOB | <방법 2> SELECT * FROM user_clusters; CLUSTER_NAME|TABLESPACE_NAME|PCT_FREE|PCT_USED|KEY_SIZE|INI_TRANS|MAX_TRANS|INITIAL_EXTENT|NEXT_EXTENT|MIN_EXTENTS|MAX_EXTENTS|PCT_INCREASE|FREELISTS|FREELIST_GROUPS|AVG_BLOCKS_PER_KEY|CLUSTER_TYPE|FUNCTION|HASHKEYS|DEGREE |INSTANCES |CACHE|BUFFER_POOL|FLASH_CACHE|CELL_FLASH_CACHE|SINGLE_TABLE|DEPENDENCIES| ------------+---------------+--------+--------+--------+---------+---------+--------------+-----------+-----------+-----------+------------+---------+---------------+------------------+------------+--------+--------+----------+----------+-----+-----------+-----------+----------------+------------+------------+ EMPCLUJOB |USERS | 10| | 123| 2| 255| 65536| 1048576| 1| 2147483645| | | | |INDEX | | 0| 1| 1| N|DEFAULT |DEFAULT |DEFAULT | N |DISABLED | -- 예제 실습 --EMP_CLUSER 조회 EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO ---------- -------------------- ------------------ ---------- -------- ---------- ---------- ---------- 7839 KING PRESIDENT 81/11/17 5000 10 7698 BLAKE MANAGER 7839 81/05/01 2850 30 7782 CLARK MANAGER 7839 81/05/09 2450 10 7566 JONES MANAGER 7839 81/04/01 2975 20 7654 MARTIN SALESMAN 7698 81/09/10 1250 1400 30 7499 ALLEN SALESMAN 7698 81/02/11 1600 300 30 7844 TURNER SALESMAN 7698 81/08/21 1500 0 30 7900 JAMES CLERK 7698 81/12/11 950 30 7521 WARD SALESMAN 7698 81/02/23 1250 500 30 7902 FORD ANALYST 7566 81/12/11 3000 20 7369 SMITH CLERK 7902 80/12/09 800 20 7788 SCOTT ANALYST 7566 82/12/22 3000 20 7876 ADAMS CLERK 7788 83/01/15 1100 20 7934 MILLER CLERK 7782 82/01/11 1300 10 -- 직업이 'ANALYST'인 직원 조회 SELECT * FROM emp_cluster WHERE job = 'ANALYST'; EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO ---------- -------------------- ------------------ ---------- -------- ---------- ---------- ---------- 7902 FORD ANALYST 7566 81/12/11 3000 20 7788 SCOTT ANALYST 7566 82/12/22 3000 20 -- SUM(sal) 결과 조회 SELECT SUM(sal) FROM emp_cluster WHERE job = 'SALESMAN'; SUM(SAL) ---------- 5600 EMP테이블에서 조회한 것과 다르게 BUFFER의 개수가 확연하게 다를 것 아래 문제풀며 익히기에서 확인해보기기
    notion image

    문제풀며 익히기

     
    Q1.
    EMP테이블에서 직업이 SALESMAN인 사원들의 이름과 월급과 직업을 출력하는 쿼리문과 EMP_CLUSTER에서 조회하는 쿼리문의 버퍼의 개수의 차이를 보시오
    <EMP> select ename, sal, job from emp where job='SALESMAN'; @P ------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 7 | |* 1 | TABLE ACCESS FULL| EMP | 1 | 3 | 4 |00:00:00.01 | 7 | ------------------------------------------------------------------------------------
    notion image
    버퍼의 개수 : 7개
    <EMP CLUSTER> select ename, sal, job from emp_cluster where job='SALESMAN'; @P ---------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 3 | | 1 | TABLE ACCESS CLUSTER| EMP_CLUSTER | 1 | 4 | 4 |00:00:00.01 | 3 | |* 2 | INDEX UNIQUE SCAN | EMPCLUJOBI | 1 | 1 | 1 |00:00:00.01 | 1 | ----------------------------------------------------------------------------------------------
    notion image
    버퍼의 개수 : 3개
    클러스터 인덱스를 타서 클러스터 테이블에 access한 것
    옵티마이저는 클러스터 인덱스를 좋아함
     
    Q2.
    deptno에 대한 검색이 중요한 클러스터 테이블을 emp_cluster2라는 이름으로 생성하시오.
    -- 테이블 통계 계산 ANALYZE TABLE emp COMPUTE STATISTICS; -- 테이블 평균 로우 길이 조회 SELECT AVG_ROW_LEN FROM user_tables WHERE table_name='EMP'; AVG_ROW_LEN ----------- 40 -- 클러스터 키당 평균 ROW 수 조회 SELECT AVG(COUNT(*)) FROM emp GROUP BY deptno; AVG(COUNT(*)) ------------- 4.66666667 -- CLUSTER_SIZE 계산 SQL> SELECT 40 * 4.66666667 *1.1 FROM DUAL; 40*4.66666667*1.1 ----------------- 205.333333 -- (AVG_ROW_LEN * AVG(COUNT(*))) * 1.1 = 123.2 -- 클러스터 및 클러스터 인덱스 생성 CREATE CLUSTER empcludeptno(deptno NUMBER(2)) --emp테이블의 부서번호 크기랑 똑같이! PCTFREE 10 PCTUSED 60 SIZE 205; CREATE INDEX empcludeptnoi ON CLUSTER empcludeptno PCTFREE 5 STORAGE(INITIAL 20k NEXT 10k); -- 클러스터 테이블 생성 CREATE TABLE emp_cluster2( 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) ) CLUSTER empcludeptno(deptno); -- 데이터 복사 INSERT INTO emp_cluster2 SELECT * FROM emp; -- 테이블 및 클러스터 정보 조회 SELECT table_name, cluster_name FROM user_tables WHERE table_name='EMP_CLUSTER2'; TABLE_NAME |CLUSTER_NAME| ------------+------------+ EMP_CLUSTER2|EMPCLUDEPTNO| SELECT * FROM user_clusters; CLUSTER_NAME|TABLESPACE_NAME|PCT_FREE|PCT_USED|KEY_SIZE|INI_TRANS|MAX_TRANS|INITIAL_EXTENT|NEXT_EXTENT|MIN_EXTENTS|MAX_EXTENTS|PCT_INCREASE|FREELISTS|FREELIST_GROUPS|AVG_BLOCKS_PER_KEY|CLUSTER_TYPE|FUNCTION|HASHKEYS|DEGREE |INSTANCES |CACHE|BUFFER_POOL|FLASH_CACHE|CELL_FLASH_CACHE|SINGLE_TABLE|DEPENDENCIES| ------------+---------------+--------+--------+--------+---------+---------+--------------+-----------+-----------+-----------+------------+---------+---------------+------------------+------------+--------+--------+----------+----------+-----+-----------+-----------+----------------+------------+------------+ EMPCLUDEPTNO|USERS | 10| | 205| 2| 255| 65536| 1048576| 1| 2147483645| | | | |INDEX | | 0| 1| 1| N|DEFAULT |DEFAULT |DEFAULT | N |DISABLED | EMPCLUJOB |USERS | 10| | 123| 2| 255| 65536| 1048576| 1| 2147483645| | | | |INDEX | | 0| 1| 1| N|DEFAULT |DEFAULT |DEFAULT | N |DISABLED |
     

    튜닝29. INDEX FULL SCAN을 하세요!

    📖

    TABLE INDEX FULL SCAN

    인덱스의 구조에 따라 전체를 스캔하는 방법
    대용량 테이블의 데이터를 빠르게 COUNT하고 싶을 때 주로 사용
    (즉, 인덱스를 처음부터 끝까지 쭉 읽는 것)
    --인덱스 full scan은 정렬된 결과가 보장 --왜? 인덱스에서 그대로 full scan을 했기 때문
    • INDEX FULL SCAN/INDEX FAST FULL SCAN하는 방법
        1. NOT NULL 제약조건 주며 NOT NULL 보장하기
          1. 해당 컬럼이 NULL이 없음을 보장해줘야 이 두가지의 스캔을 타게 됨
        1. WHERE절에 NOT NULL 조건 주며 NOT NULL 보장하기
          1. 그런데, NOT NULL 제약을 걸 수 없다면(DBA권한이 없는) 다음과 같이 where절에서 not null을 보장하는 SQL을 작성해야함
            1. select job, count(*) from emp where job is not null group by job;
     

    TABLE INDEX FULL SCAN

    https://cafe.daum.net/oracleoracle/Snri/110
     
    직업별 인원수를 뽑고싶다.
    그런데 JOB에 대한 인덱스가 없다면 테이블 FULL스캔을 해야한다.
     
    그래서 JOB에 대한 인덱스를 만들어
    INDEX FULL SCAN을 한 것.
    이런 방법이 TABLE FULL SCAN보다 빠름
     

    예제

     
    예제1) 직업과 직업별 인원수를 출력하시오
    @demo create index emp_job on emp(job); select job, count(*) from emp group by job; /* 결과 JOB COUNT(*) ------------------ ---------- PRESIDENT 1 MANAGER 3 SALESMAN 4 CLERK 4 ANALYST 2 */ @p ------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 6 | | 1 | HASH GROUP BY | | 1 | 5 | 5 |00:00:00.01 | 6 | | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | ------------------------------------------------------------------------------------- 테이블 full scan하는 실행계획이 출력됨 만약, 인덱스 full scan으로 나오게 하고 싶다면 다음과 같이 하면 됨. job에 not null 제약이 걸려있으면 됨.
    notion image
    만약, 인덱스 full scan으로 나오게 하고 싶다면 다음과 같이 하면 됨. job에 not null 제약이 걸려있으면 됨. alter table emp modify job not null; /* 아래와 같이 걸지 않아도 오라클이 알아서 함 alter table emp modify job constraint emp_job_nn not null; */ select job, count(*) from emp group by job; @p ------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 2 | | 1 | SORT GROUP BY NOSORT| | 1 | 5 | 5 |00:00:00.01 | 2 | | 2 | INDEX FULL SCAN | EMP_JOB | 1 | 14 | 14 |00:00:00.01 | 2 | ------------------------------------------------------------------------------------------ 버퍼의 개수가 6개에서 2개로 줄어들었음
    < DBA 권한이 없을 때 NOT NULL 주는 방법 > -- 특정 테이블에 있는 제약조건 이름 확인하는 SQL select constraint_name from user_constraints where table_name = 'EMP'; CONSTRAINT_NAME ----------------| SYS_C008743 SYS_C008744 -- 제약조건 삭제 방법 alter table [테이블명] drop constraint [제약조건명]; alter table emp drop constraint SYS_C008743; alter table emp drop constraint SYS_C008744; select job, count(*) from emp where job is not null group by job; ------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 2 | | 1 | SORT GROUP BY NOSORT| | 1 | 5 | 5 |00:00:00.01 | 2 | |* 2 | INDEX FULL SCAN | EMP_JOB | 1 | 14 | 14 |00:00:00.01 | 2 | ------------------------------------------------------------------------------------------
    notion image

    문제풀며 익히기

     
    Q1.
    부서번호에 인덱스를 걸고 부서번호, 부서번호별 인원수를 출력하는데, INDEX FULL SCAN이 될 수 있도록 튜닝해서 출력하시오.
    @demo create index emp_deptno on emp(deptno); select deptno, count(*) from emp where deptno is not null group by deptno; @p DEPTNO COUNT(*) ---------- ---------- 10 3 20 5 30 6 --인덱스 full scan은 정렬된 결과가 보장 --왜? 인덱스에서 그대로 full scan을 했기 때문 --------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 2 | | 1 | SORT GROUP BY NOSORT| | 1 | 3 | 3 |00:00:00.01 | 2 | |* 2 | INDEX FULL SCAN | EMP_DEPTNO | 1 | 14 | 14 |00:00:00.01 | 2 | ---------------------------------------------------------------------------------------------
    notion image
     
    Q2. (결합 컬럼 인덱스_ full scan이 되게 하기 위해서)
    직업, 직업별 토탈월급을 출력하는데, table full scan이 아닌 index full scan이 될 수 있게 인덱스도 걸고 sql도 작성하시오.
    (결합 컬럼 인덱스를 사용해야 테이블 엑세스 없이 인덱스만 full로 읽어도 sum(sal)을 구할 수 있게 됨 즉, 인덱스에 있는 데이터만 가지고 sum(sal)을 구할 수 있음
    단일 컬럼 인덱스를 사용하면 테이블 full scan을 하거나 index scan을 해도 테이블 엑세스를 하게 됨)
    https://cafe.daum.net/oracleoracle/Snri/111
    @demo create index emp_job_sal on emp(job, sal); select job, sum(sal) from emp where job is not null group by job; JOB SUM(SAL) ------------------ ---------- ANALYST 6000 CLERK 4150 MANAGER 8275 PRESIDENT 5000 SALESMAN 5600 --job 기준으로 정렬됨 @p ---------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 2 | | 1 | SORT GROUP BY NOSORT| | 1 | 5 | 5 |00:00:00.01 | 2 | |* 2 | INDEX FULL SCAN | EMP_JOB_SAL | 1 | 14 | 14 |00:00:00.01 | 2 | ---------------------------------------------------------------------------------------------- ==================================================== **SORT GROUP BY NOSORT** : 원래 gorup by는 hash group by가 나오는데, 결합컬럼 인덱스에서 이미 full scan하면서 정렬했기 때문에 nosort가 나온것 테이블 스캔보다 빠르게 결과를 볼 수 있음
     
     
    Q3.
    부서번호별 평균 월급을 출력하는데, index full scan이 되도록 인덱스를 생성하고 sql을 작성하시오.
    @demo create index emp_deptno_sal on emp(deptno, sal); select deptno, avg(sal) from emp where deptno is not null group by deptno; DEPTNO AVG(SAL) ---------- ---------- 10 2916.66667 20 2175 30 1566.66667 @p ------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 2 | | 1 | SORT GROUP BY NOSORT| | 1 | 3 | 3 |00:00:00.01 | 2 | |* 2 | INDEX FULL SCAN | EMP_DEPTNO_SAL | 1 | 14 | 14 |00:00:00.01 | 2 | ------------------------------------------------------------------------------------------------- ================= 테이블 스캔을 하지 않고 인덱스 full scan을 하며, deptno를 기준으로 정렬됨
     

    튜닝30. INDEX FAST FULL SCAN을 사용하세요.

    📖
    💡
    인덱스 스캔 방법(힌트를 잘알아두면 80%의 sql은 튜닝 가능)
    1. INDEX UNIQUE SCAN
      1. : /*+ index( [테이블명] [인덱스명] ) */
         
    1. INDEX RANGE SCAN
      1. : /*+ index( [테이블명] [인덱스명] ) */
         
    1. INDEX FULL SCAN - 튜닝예제 29
      1. : /*+ index_fs( [테이블명] [인덱스명] ) */ 힌트 없어짐
         
    1. INDEX FAST FULL SCAN -튜닝예제 30
      1. : /*+ index_ffs( [테이블명] [인덱스명] ) */
         
    1. INDEX SKIP SCAN - 튜닝예제 26
      1. : /*+ index_ss( [테이블명] [인덱스명] ) */
         
    1. INDEX MERGE SCAN - 튜닝예제 22
      1. : /*+ and_equal( [테이블명] [인덱스명1] [인덱스명2] ) */
         
    1. INDEX BITMAP MERGE SCAN - 튜닝예제31
      1. : /*+ index_combine( [테이블명]) */
         
    1. INDEX JOIN SCAN - 튜닝예제32
      1. : /*+ index_join( [테이블명] [인덱스명1] [인덱스명2] ) */
     
    SQL튜너들은 이 내용을 A4용지에 넣어 머리에 잘 암기해 두어야 함
     
    ■ 주의사항
    힌트 사용시 /*+ */ 의 내부에 작성하는 내용과 앞뒤로 공백 하나씩 있어야함
     

    INDEX FAST FULL SCAN

    그림설명

    notion image
    notion image
    notion image
    notion image
    notion image
    notion image
    notion image
    notion image
    notion image
    notion image
     
    • 사용하는 방법
        1. 힌트 사용 : /*+ index_ffs( [테이블명] [인덱스명] ) */
        1. NOT NULL
          1. NOT NULL 제약조건 사용
          2. WHERE 절에 특정 컬럼명이 NOT NULL 임을 보장해주기
            1. DBA권한 없다면 이 방법만 사용 가능
        1. 병렬 힌트 —-오라클 ex버전은 병렬 기능 안됨
          1. 병렬도 : 병렬 프로세스 개수를 몇 개 둘 것인지 정하는 것 CPU_COUNT * 2 개가 최대 가능 병렬 수! DBA가 제한을 줘야함. 개발자들이 남발하지 못하게 전체공지 병렬도 사용시 DBA에게 알려주라고 공지하기
          2. 회사에서 병렬기능 사용시 구매 했는지 확인해야함. 안그럼 벌금..1억..
          3. -- 최대 병렬도 확인하기 SQL> SHOW PARAMETER CPU_COUNT NAME TYPE VALUE ------------------------------------ ---------------------- ------------------------------ cpu_count integer 20 ** 병렬도를 최대 40까지 줄 수 있음 ** 크게 줄 수록 성능이 좋아지지만 남발하면 안됨
            /*+ index_ffs( [테이블명] [인덱스명] ) parallel_index( [테이블명], [인덱스이름], [병렬도] ) */ ex. (예제 2 참조) select /*+ index_ffs(emp emp_job_sal) parallel_index(emp, emp_job_sal,4) */
     

    INDEX FULL SCAN vs INDEX FAST FULL SCAN

    • INDEX FULL SCAN
      • 인덱스의 구조에 따라 스캔하며 정렬이 보장됨
      • SINGLE BLOCK i/o
        • 각 행들이 각각의 블록 안에 있다고 보고, 오라클 데이터베이스에서 메모리에 복사본 데이터를 올려놓는데, DB에서 불러올 때 각 행이 담긴 블록들을 하나씩 올리게 됨
      • 병렬 스캔이 불가능
        • 하나의 프로세서가 전체를 읽음
        •  
    • INDEX FAST FULL SCAN -full scan보다 빠름
      • 세그먼트 전체를 스캔하며, 정렬이 보장되지 않음
        • 세그먼트 전체를 스캔?
          • : 하나하나 순서대로 full scan하는 것에 반해 fast full scan은 블록 단위로 읽는데, 어느 블록이 메모리에 먼저 올라가는지 알 수 없음
            하지만 블록 단위로 읽기 때문에 full scan보다 속도가 빠름
        • 정렬이 보장되지 않는 이유?
          • : 세그먼트 전체를 스캔하기 때문에
      • MULTI BLOCK i/o
        • 각 행들이 그룹으로 나뉘어 블록에 묶여 있을 때, 오라클 데이터베이스에서 메모리에 복사본 데이터를 올려놓는데, DB에서 불러올 때 블록 단위로 올리기 때문에 더 빠르게 I/O됨
      • 병렬 스캔이 가능
        • 여러개의 프로세서가 나눠서 읽음
      • 대용량에 적합
      •  
    [표로 정리]
    ㅤ
    INDEX FULL SCAN
    INDEX FAST FULL SCAN
    정렬
    인덱스의 구조에 따라 스캔하며 정렬이 보장
    세그먼트 전체를 스캔하며, 정렬이 보장되지 않음
    I/O
    SINGLE BLOCK i/o
    MULTI BLOCK i/o
    병렬 스캔
    불가능
    가능
    병렬 스캔 힌트
    ㅤ
    /*+ index_ffs( [테이블명] [인덱스명] ) parallel_index( [테이블명], [인덱스이름], [병렬도] ) */

    예제

     
    예제1)
    직업, 직업별 인원수를 출력하는 SQL 을 작성하는데, INDEX FULL SCAN을 하게 하시오.
    @demo create index emp_job on emp(job); select /*+ index_ffs(emp emp_job) */ job, count(*) from emp where job is not null group by job; @p ------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 4 | | 1 | HASH GROUP BY | | 1 | 5 | 5 |00:00:00.01 | 4 | |* 2 | INDEX FAST FULL SCAN| EMP_JOB | 1 | 14 | 14 |00:00:00.01 | 4 | -------------------------------------------------------------------------------------------
    notion image
     
    예제2) —현장에서 INDEX FAST FULL SCAN 중요 ★
    병렬로 INDEX FAST FULL SCAN을 하시오
    select /*+ index_ffs(emp emp_job_sal) parallel_index(emp, emp_job_sal,4) */ job, sum(sal) from emp where job is not null group by job; @p --xe버전을 병렬기능 사용 불가능
    notion image
     
    PX SEND HASH, PX RECEIVE
    프로세스들이 서로 어디까지 읽었는지 메시지 주고받는 부분
     
    PX SEND QC(RANDOM)
    PX(프로세스)가 총감독관 QC(코디네이터)에게 일 다했다고 알려주는 부분

    문제풀며 익히기

     
    Q1.
    부서번호, 부서번호별 인원수를 출력하는데, 관련된 인덱스도 직접 생성하고, INDEX FAST FULL SCAN이 되게 하시오
    @demo create index emp_deptno on emp(deptno); select /*+ index_ffs(emp emp_deptno) */ deptno, count(*) from emp where deptno is not null group by deptno; DEPTNO COUNT(*) ---------- ---------- 10 3 20 5 30 6 @p ---------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 4 | | 1 | HASH GROUP BY | | 1 | 3 | 3 |00:00:00.01 | 4 | |* 2 | INDEX FAST FULL SCAN| EMP_DEPTNO | 1 | 14 | 14 |00:00:00.01 | 4 | ----------------------------------------------------------------------------------------------
    notion image
     
    Q2.
    직업과 직업별 토탈월급을 출력하는데, INDEX FAST FULL SCAN이 되도록 관련된 INDEX를 걸고 SQL도 작성하시오
    @demo create index emp_job_sal on emp(job, sal); select /*+ index_ffs(emp emp_job_sal) */ job, sum(sal) from emp where job is not null group by job; JOB SUM(SAL) ------------------ ---------- ANALYST 6000 CLERK 4150 MANAGER 8275 PRESIDENT 5000 SALESMAN 5600 select job, sum(sal) from emp where job is not null group by job; @p ----------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ----------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 4 | | 1 | HASH GROUP BY | | 1 | 5 | 5 |00:00:00.01 | 4 | |* 2 | INDEX FAST FULL SCAN| EMP_JOB_SAL | 1 | 14 | 14 |00:00:00.01 | 4 | -----------------------------------------------------------------------------------------------
    notion image
     
    /*+ index_ffs(emp emp_job_sal) */ 힌트를 사용하지 않으면 INDEX FULL SCAN 하게됨
    select job, sum(sal) from emp where job is not null group by job; @p ---------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 2 | | 1 | SORT GROUP BY NOSORT| | 1 | 5 | 5 |00:00:00.01 | 2 | |* 2 | INDEX FULL SCAN | EMP_JOB_SAL | 1 | 14 | 14 |00:00:00.01 | 2 | ----------------------------------------------------------------------------------------------
    notion image
     
     
    Q3.
    부서번호, 부서번호별 인원수를 출력하는데, INDEX FAST FULL SCAN을 하고, 병렬로 작업할 수 있게 힌트를 주시오.
    @demo create index emp_deptno on emp(deptno); select /*+ index_ffs(emp emp_deptno) parallel_index(emp, emp_deptno,16) */ deptno, count(*) from emp where deptno is not null group by deptno; @p --xe 버전은 병렬 안됨. 실행계획 볼 수 없음. ---------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 4 | | 1 | HASH GROUP BY | | 1 | 3 | 3 |00:00:00.01 | 4 | |* 2 | INDEX FAST FULL SCAN| EMP_DEPTNO | 1 | 14 | 14 |00:00:00.01 | 4 | ---------------------------------------------------------------------------------------------- -- 병렬된 버전은 아래 사진 참조.
    notion image
     
     

    튜닝31. INDEX BITMAP MERGE SCAN을 사용하세요.

    📖
    💡
    인덱스 스캔 방법(힌트를 잘알아두면 80%의 sql은 튜닝 가능)
    1. INDEX UNIQUE SCAN
      1. : /*+ index( [테이블명] [인덱스명] ) */
         
    1. INDEX RANGE SCAN
      1. : /*+ index( [테이블명] [인덱스명] ) */
         
    1. INDEX FULL SCAN - 튜닝예제 29
      1. : /*+ index_fs( [테이블명] [인덱스명] ) */ 힌트 없어짐
         
    1. INDEX FAST FULL SCAN -튜닝예제 30
      1. : /*+ index_ffs( [테이블명] [인덱스명] ) */
         
    1. INDEX SKIP SCAN - 튜닝예제 26
      1. : /*+ index_ss( [테이블명] [인덱스명] ) */
         
    1. INDEX MERGE SCAN - 튜닝예제 22
      1. : /*+ and_equal( [테이블명] [인덱스명1] [인덱스명2] ) */
         
    1. INDEX BITMAP MERGE SCAN - 튜닝예제31
      1. : /*+ index_combine( [테이블명]) */
         
    1. INDEX JOIN SCAN - 튜닝예제32
      1. : /*+ index_join( [테이블명] [인덱스명1] [인덱스명2] ) */
     
    SQL튜너들은 이 내용을 A4용지에 넣어 머리에 잘 암기해 두어야 함
     
    ■ 주의사항
    힌트 사용시 /*+ */ 의 내부에 작성하는 내용과 앞뒤로 공백 하나씩 있어야함
     

    INDEX BITMAP MERGE SCAN

    두 개의 인덱스를 같이 스캔해서 테이블 엑세스를 줄이는 방법.
     
    단, INDEX MERGE와 방식은 같으나, 인덱스를 BITMAP으로 변환해서 인덱스의 사이즈를 아주 많이 줄인 다음에 인덱스를 MERGE하는게 INDEX MERGE와의 차이임.
     
    INDEX SIZE가 작아지므로 인덱스를 스캔하는 시간이 짧아져 속도가 빨라지게 됨
     

    INDEX 의 구조로 보는 INDEX BITMAP

    notion image
    notion image

    BITMAP?

    • BIT : ex. 0110101 - 가장 작은 단위
    • BITMAP : BIT로 이루어진 지도

    비트맵 인덱스 설명 그림

    notion image
    일반 인덱스는 B-TREE 구조의 인덱스
    notion image
    두 개의 B-TREE인덱스를 하나로 MERGE 합 함
    notion image
    CONVERSION 후 EMP2테이블에 테이블 ACCESS
     

    실습- 환경 구성

    • 실습 환경 구성
      • @demo create index emp_job on emp(job); create index emp_deptno on emp(deptno);
         
    예제1)
    < 튜닝전 : INDEX MERGE SAN > select /*+ and_equal(emp emp_job emp_deptno) */ empno, ename, job, deptno from emp where deptno = 30 and job = 'SALESMAN'; EMPNO ENAME JOB DEPTNO ---------- -------------------- ------------------ ---------- 7654 MARTIN SALESMAN 30 7499 ALLEN SALESMAN 30 7844 TURNER SALESMAN 30 7521 WARD SALESMAN 30 @p ---------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 8 | |* 1 | TABLE ACCESS BY INDEX ROWID| EMP | 1 | 1 | 4 |00:00:00.01 | 8 | | 2 | AND-EQUAL | | 1 | | 4 |00:00:00.01 | 6 | |* 3 | INDEX RANGE SCAN | EMP_JOB | 1 | 3 | 4 |00:00:00.01 | 4 | |* 4 | INDEX RANGE SCAN | EMP_DEPTNO | 1 | 5 | 4 |00:00:00.01 | 2 | ---------------------------------------------------------------------------------------------------- 버퍼의 개수 : 8개
    notion image
    < 튜닝 후 : INDEX BITMAP MERGE SCAN > select /*+ index_combine(emp) */ empno, ename, job, deptno from emp where deptno = 30 and job = 'SALESMAN'; @p ------------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 4 | | 1 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 1 | 4 |00:00:00.01 | 4 | | 2 | BITMAP CONVERSION TO ROWIDS | | 1 | | 4 |00:00:00.01 | 2 | | 3 | BITMAP AND | | 1 | | 1 |00:00:00.01 | 2 | | 4 | BITMAP CONVERSION FROM ROWIDS | | 1 | | 1 |00:00:00.01 | 1 | |* 5 | INDEX RANGE SCAN | EMP_JOB | 1 | | 4 |00:00:00.01 | 1 | | 6 | BITMAP CONVERSION FROM ROWIDS | | 1 | | 1 |00:00:00.01 | 1 | |* 7 | INDEX RANGE SCAN | EMP_DEPTNO | 1 | | 6 |00:00:00.01 | 1 | ------------------------------------------------------------------------------------------------------------ ================ ** 1. EMP_JOB : INDEX RANGE SCAN -> BITMAP CONVERSION FROM ROWIDS ** emp_job의 인덱스를 range scan해서 BITMAP CONVERSION 했다 즉, bitmap으로 바꿨다 = 사이즈를 줄인 것 ** 2. EMP_DEPTNO : INDEX RANGE SCAN -> BITMAP CONVERSION FROM ROWIDS ** emp_deptno의 인덱스를 range scan해서 BITMAP CONVERSION 했다 즉, bitmap으로 바꿨다 = 사이즈를 줄인 것 ** 3. BITMAP AND ** 크기가 작아진 두 개의 비트맵 인덱스를 MERGE 즉, 하나로 합친 것 ** 4. BITMAP CONVERSION TO ROWIDS ** ROWID를 사용해 하나의 BITMAP으로 바꿈 ** 5. TABLE ACCESS BY INDEX ROWID BATCHED ** ROWID를 사용해 테이블 엑세스 하러 간 것
    notion image
    notion image
     

    문제풀며 익히기

     
    Q1. 아래의 환경을 만들고 아래의 SQL을 튜닝하시오
    drop table mcustsum purge; create table mcustsum as select rownum custno , '2008' || lpad(ceil(rownum/100000), 2, '0') salemm , decode(mod(rownum, 12), 1, 'A', 'B') salegb , round(dbms_random.value(1000,100000), -2) saleamt from dual connect by level <= 1200000 ; create index m_indx1 on mcustsum(custno); create index m_indx2 on mcustsum(salemm); create index m_indx3 on mcustsum(salegb);
    < 튜닝 전 > select /*+ and_equal(t m_indx2 m_indx3) */ count(*) from mcustsum t where salegb = 'A' and salemm = '200801'; COUNT(*) ---------- 8334 @p ------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | ------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.02 | 17229 | 285 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.02 | 17229 | 285 | |* 2 | AND-EQUAL | | 1 | | 8334 |00:00:00.02 | 17229 | 285 | |* 3 | INDEX RANGE SCAN| M_INDX2 | 1 | 100K| 16668 |00:00:00.01 | 541 | 258 | |* 4 | INDEX RANGE SCAN| M_INDX3 | 1 | 600K| 8335 |00:00:00.01 | 16688 | 27 | -------------------------------------------------------------------------------------------------
    notion image
    < 튜닝 후 > select /*+ index_combine(t) */ count(*) from mcustsum t where salegb = 'A' and salemm = '200801'; --------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | --------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.02 | 437 | 161 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.02 | 437 | 161 | | 2 | BITMAP CONVERSION COUNT | | 1 | 50000 | 1 |00:00:00.02 | 437 | 161 | | 3 | BITMAP AND | | 1 | | 1 |00:00:00.02 | 437 | 161 | | 4 | BITMAP CONVERSION FROM ROWIDS| | 1 | | 1 |00:00:00.01 | 253 | 0 | |* 5 | INDEX RANGE SCAN | M_INDX2 | 1 | | 100K|00:00:00.01 | 253 | 0 | | 6 | BITMAP CONVERSION FROM ROWIDS| | 1 | | 2 |00:00:00.01 | 184 | 161 | |* 7 | INDEX RANGE SCAN | M_INDX3 | 1 | | 100K|00:00:00.01 | 184 | 161 | ---------------------------------------------------------------------------------------------------------------
    notion image
     

    튜닝32. INDEX JOIN SCAN을 하세요!

    📖

    INDEX JOIN SCAN

    인덱스끼리 조인을 해서 테이블을 따로 엑세스 하지 않는 스캔방식
    notion image
     
    • index merge scan과의 차이 :
    index merge는 두 인덱스 스캔 후 table access를 하는데,
    index join scan은 각각의 인덱스를 스캔하여 hash join 후 view를 만들어 그 view에서 데이터를 가져오기 때문에 table access를 하지 않음
     

    실습

    • 실습 환경 구성
      • @demo create index emp_deptno on emp(deptno); create index emp_job on emp(job);
     
    예제1) 아래의 SQL을 튜닝하시오
    <튜닝전> select deptno, job from emp where deptno = 30 and job = 'SALESMAN'; @p --------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 4 | |* 1 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 1 | 4 |00:00:00.01 | 4 | |* 2 | INDEX RANGE SCAN | EMP_JOB | 1 | 3 | 4 |00:00:00.01 | 2 | --------------------------------------------------------------------------------------------------------- 테이블 엑세스 함 버퍼의 개수 : 4개
    notion image
    <튜닝후> select /*+ index_join(emp emp_deptno emp_job) */deptno, job from emp where deptno = 30 and job = 'SALESMAN'; @p ---------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 3 | | | | |* 1 | VIEW | index$_join$_001 | 1 | 1 | 4 |00:00:00.01 | 3 | | | | |* 2 | HASH JOIN | | 1 | | 4 |00:00:00.01 | 3 | 1423K| 1423K| 976K (0)| |* 3 | INDEX RANGE SCAN| EMP_JOB | 1 | 1 | 4 |00:00:00.01 | 1 | | | | |* 4 | INDEX RANGE SCAN| EMP_DEPTNO | 1 | 1 | 6 |00:00:00.01 | 2 | | | | ---------------------------------------------------------------------------------------------------------------------------- =========================== 테이블 엑세스 하지 않고 인덱스만 읽어서 인덱스를 hash join하여 view를 만듦 (오라클이 hash join을 자랑스러워함. 이것 덕분에 sw에서 독보적인..)
    notion image
     

    문제풀며 익히기

     
    Q1.
    아래의 환경을 만들고, 아래의 sql을 튜닝하시오
    drop table mcustsum purge; create table mcustsum as select rownum custno , '2008' || lpad(ceil(rownum/100000), 2, '0') salemm , decode(mod(rownum, 12), 1, 'A', 'B') salegb , round(dbms_random.value(1000,100000), -2) saleamt from dual connect by level <= 1200000 ; create index m_indx1 on mcustsum(custno); create index m_indx2 on mcustsum(salemm); create index m_indx3 on mcustsum(salegb);
    < 튜닝 전 > select salegb, salemm from mcustsum t where salegb = 'A' and salemm = '200801'; @p ------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | ------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 8334 |00:00:00.04 | 1677 | 258 | |* 1 | TABLE ACCESS BY INDEX ROWID BATCHED| MCUSTSUM | 1 | 50000 | 8334 |00:00:00.04 | 1677 | 258 | |* 2 | INDEX RANGE SCAN | M_INDX2 | 1 | 100K| 100K|00:00:00.03 | 809 | 258 | -------------------------------------------------------------------------------------------------------------------
    버퍼의 개수 : 1677개
    notion image
    < 튜닝 후 > select /*+ index_join(t m_indx2 m_indx3) */ salegb, salemm from mcustsum t where salegb = 'A' and salemm = '200801'; @p ------------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem | ------------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 8334 |00:00:00.06 | 993 | 188 | | | | |* 1 | VIEW | index$_join$_001 | 1 | 50000 | 8334 |00:00:00.06 | 993 | 188 | | | | |* 2 | HASH JOIN | | 1 | | 8334 |00:00:00.06 | 993 | 188 | 7126K| 1968K| 7673K (0)| |* 3 | INDEX RANGE SCAN| M_INDX2 | 1 | 50000 | 100K|00:00:00.01 | 253 | 0 | | | | |* 4 | INDEX RANGE SCAN| M_INDX3 | 1 | 50000 | 100K|00:00:00.03 | 740 | 188 | | | | -------------------------------------------------------------------------------------------------------------------------------------
    버퍼의 개수 : 993개
    notion image
     

    튜닝33. 옵티마이저에게 INDEX가 안보이게 할 수 있어요.(OCP문제 관련)-SQL튜닝 한글(142p. 4-38)-DBA에게 필요 ★

    📖

    보이지 않는 인덱스

    notion image
    특정 인덱스를 못 타게 하거나(index를 access하지 못하게 하는), full table scan을 하게 하고 싶을 때 사용
    notion image
    • 왜 못보게 함?
        1. 새롭게 생성한 인덱스로 인해 다른 SQL들이 느려질 수 있기 때문에, 일부러 안보이게 만들고. 보이게 했다가 문제가 없으면 계속 사용하고, 문제가 발생하면 다시 안보이게 하기 위해서이다.
          1. ex. 개발자가 인덱스를 만들어 달라고 요청해서. index는 함부로 만들면 안되기 때문에 보이지 않도록 생성한 후 visible로 바꾸고 문제가 없으면 두는데, 문제가 생기면 바로 invisible로 바뀜
             
        1. 개발할 때 생성한 INDEX들 중에서 불필요한 인덱스들이 있음. 사용되지 않는 인덱스들이 있음. 이 인덱스를 DROP하기 전에 미리 보이지 않게 하고, 문제가 없으면 한번에 DROP하기 위해서는 인덱스를 보이지 않게 해야함

    예제

     
    예제1) — 옵티마이저가 인덱스를 볼 수 있는 경우
    아래의 환경을 만들고 sql을 작성하시오
    @demo create index emp_sal on emp(sal); select /*+ index(emp emp_sal) */ ename, sal from emp where sal = 3000; @p --------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 2 |00:00:00.01 | 4 | | 1 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 1 | 2 |00:00:00.01 | 4 | |* 2 | INDEX RANGE SCAN | EMP_SAL | 1 | 1 | 2 |00:00:00.01 | 2 | ---------------------------------------------------------------------------------------------------------
    notion image
     
    예제2) — 옵티마이저가 인덱스를 볼 수 없는 경우
    emp_sal 인덱스를 못보게 하시오
    -- index를 못보게 변경 alter index emp_sal invisible; -- 인덱스의 visible 상태를 확인할 수 있는 sql문 select index_name, visibility from user_indexes where table_name = 'EMP'; INDEX_NAME|VISIBILITY| ----------+----------+ EMP_SAL |VISIBLE | -- 확인하기 select /*+ index(emp emp_sal) */ ename, sal from emp where sal = 3000; @p ------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 2 |00:00:00.01 | 7 | |* 1 | TABLE ACCESS FULL| EMP | 1 | 1 | 2 |00:00:00.01 | 7 | ------------------------------------------------------------------------------------ FULL TABLE SCAN을 하고 있음. 옵티마이저가 INDEX를 못 보는 것
    notion image
    notion image
     
    예제3)
    EMP_SAL 인덱스를 다시 보이게 하시오
    alter index emp_sal visible; select /*+ index(emp emp_sal) */ ename, sal from emp where sal = 3000; @p --------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 2 |00:00:00.01 | 4 | | 1 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 1 | 2 |00:00:00.01 | 4 | |* 2 | INDEX RANGE SCAN | EMP_SAL | 1 | 1 | 2 |00:00:00.01 | 2 | ---------------------------------------------------------------------------------------------------------
    notion image
     
     
     
    Q1.
     

    문제풀며 익히기

     
    Q1.
    emp테이블의 job에 인덱스를 생성하는데, 인덱스를 생성하면서 바로 인덱스가 옵티마이저에 의해서 안보이게 하시오
    -- 처음부터 보이지 않게 인덱스 생성 create index emp_job on emp(job) invisible; -- 인덱스 이름과 VISIBLE여부 확인하기 select index_name, visibility from user_indexes where table_name = 'EMP'; INDEX_NAME|VISIBILITY| ----------+----------+ EMP_SAL |VISIBLE | EMP_JOB |INVISIBLE |
    notion image
     
     
     

    튜닝34. 인덱스 생성시 인덱스를 빠르게 생성하는 옵션 2가지를 사용하세요. - DBA에게 유용 ★

    📖

    INDEX 빠르게 생성하기

    대용량 데이터 베이스 환경에서는 인덱스를 생성할 때 시간이 많이 소모됨 따라서 인덱스를 생성할 때 병렬로 생성하게 하고, 로그정보를 생성하지 않게 하면, 빠르게 인덱스를 생성할 수 있음
     
    옛날 장비 ————-→ 새로운 장비
    옛 오라클 버전 ——→ 새로운 버전
     
    즉,
    AS-IS DB———> TO-BE DB
    수 백 개의 인덱스와 수 백 개의 테이블을 똑같이 옮겨야함
    그럴 때 DBA에게 정말 유용한 기능임
     

    INDEX 빠르게 생성하는 옵션

    1. NOLOGGING 옵션
      1. 인덱스 생성 시 생성되는 기록인 로그 정보를 생성하지 않게 함
        1. create index [인덱스명] on [테이블명]( [인덱스 생성할 컬럼] ) nologging parallel 16;
      2. 기록하는 이유는 추후 장애 발생 시 복구를 위함인데, 처음에 생성할 때만 nologging하고 나중에 logging으로 바꿔 줄 수 있음
      3. 따라서 인덱스 생성 이후에는 반드시 NOLOGGING → LOGGING으로 변경하기
        1. alter index [인덱스명] logging;
    1. PARALLEL 16 옵션
      1. create index [인덱스명] on [테이블명]( [인덱스 생성할 컬럼] ) nologging parallel 16;
      2. 병렬도를 16 으로 주겠다는 의미
      3. 하나의 프로세서가 인덱스를 생성하는게 아니라, 16개의 프로세서가 인덱스를 생성하니까 빠르게 인덱스를 생성할 수 있음
      4. 인덱스 생성할때 빠르게 생성하는 용도이므로 , 생성 후에는 반드시 병렬도를 1로 변경해 놓아야 함. 만약 놓치고 변경하지 않으면, 옵티마이저는 무조건 병렬도가 높은 인덱스를 엑세스하려고 함.(좋은 인덱스가 있어도 그거 안타고 병렬도 높은 인덱스만 타려고 함..)
        1. alter index [인덱스명] parallel 1;
    예제1) 다음과 같이 인덱스를 생성
    @demo create index emp_sal on emp(sal) nologging --이거 하나만 붙여도 빨라짐 parallel 16; create index emp_sal on emp(sal) nologging parallel 16;
     
    예제2) 생성 이후에 반드시 NOLOGGING → LOGGING으로 변경
    alter index emp_sal logging; --로깅인지 확인하는 방법 select index_name, logging from user_indexes where index_name = 'EMP_SAL'; INDEX_NAME|LOGGING| ----------+-------+ EMP_SAL |YES |
    notion image
     
    예제3) 생성 이후에 반드시 병렬도를 1로 변경해야한다
    -- 병렬도 변경하기 alter index emp_sal parallel 1; -- 인덱스의 병렬도 확인하는 작업 < 확인 방법 1 > select index_name, degree from user_indexes where index_name = 'EMP_SAL'; INDEX_NAME|DEGREE| ----------+------+ EMP_SAL |1 | < 확인 방법 2 > select index_name, logging, degree from user_indexes where degree > 1; ---현업에서는 이렇게 많이 확인함 선택된 레코드가 없습니다. ================================ 선택된 레코드가 없다고 나오면, 잘 나온것. 없는게 맞음
    ※ 반드시 병렬도는 1로 변경해 놓아야 합니다. 만약 놓치고 변경하지 않으면 옵티마이져는 무조건 병렬도가 높은 인덱스를 엑세스하려 합니다
    notion image
     
     
     

    문제풀며 익히기

     
    Q1. (DBA에게 매우 중요한 작업 ★)
    emp테이블의 아래의 인덱스를 생성하는데, 빠르게 인덱스를 생성할 수 있게 하고 다시 로깅과 병렬도를 조정하시오
    1. emp테이블의 job에 인덱스 생성
    1. emp테이블의 deptno에 인덱스 생성
    1. emp테이블의 empno와 ename으로 결합 컬럼 인덱스를 생성하기
    @demo create index emp_job on emp(job) nologging parallel 16; create index emp_deptno on emp(deptno) nologging parallel 16; create index emp_empno_ename on emp(empno, ename) nologging parallel 16; alter index emp_job logging parallel 1; alter index emp_deptno logging parallel 1; alter index emp_empno_ename logging parallel 1; --확인하기 select index_name, logging, degree from user_indexes where degree > 1; ---현업에서는 이렇게 많이 확인함 선택된 레코드가 없습니다. ================================ 선택된 레코드가 없다고 나오면, 잘 나온것. 없는게 맞음
     
     

    튜닝35. 검색 속도가 중요하면 IOT(Index Organization Table)을 만드세요. - DBA에게 중요 ★

    📖

    IOT란?

    TABLE RANDOM ACCESS가 발생하지 않도록 테이블을 아예 인덱스 구조로 생성한 OBJECT
    그림출처: https://jinkyu.tistory.com/47
    그림출처: https://jinkyu.tistory.com/47

    IOT의 장점?

    notion image
    IOT는 인덱스 리프 블록에 모든 데이터를 다 포함시키므로, 테이블 랜덤 액세스를 하지 않아도 된다는 장점이 있습니다(테이블의 각각의 행이 각각 하나의 leaf 블록으로 구성되어짐)
    즉, 잘 만든 INDEX하나가 수십 개의 SQL튜닝을 하지 않아도 되고, 잘 만든 IOT하나가 수십 개의 SQL튜닝을 하지 않아도 되게 함
     

    IOT의 장점 및 단점

    ㅤ
    장점
    단점
    성능
    검색 성능 상승
    IOT테이블에서 INSERT 작업 시 성능 저하(아래 그림 설명 참조)
    저장 공간
    저장 공간 절약 (인덱스 따로 테이블 따로가 아니라하나로 합쳤기 때문) ⇒ DBA는 공간관리를 잘 해야 하기 때문에 DBA에게 HAPPY한 경우임!
    ㅤ
     

    IOT 테이블 INSERT작업 시 성능 저하 이유

    notion image
    테이블에 인덱스가 많을수록 INSERT가 느려지는데,
    IOT 근본 자체가 인덱스 형태로 생겼기 때문에
    IOT그림
    IOT그림
    만약 사원번호 컬럼을 가지고 IOT를 구성했다면, LEAF BLOCK에 각각 채워져있을 텐데, 만약 8141이라는 새로운 컬럼이 들어오면,,
    테이블의 경우는 그냥 아래에 데이터 추가하면 되는데(행 간의 순서 없으므로)
    IOT는 사원번호를 기준으로 순서대로 구성된 LEAF BLOCK들 사이를 잘 찾아서 맞춰 넣어야 하기 때문에, 일반 테이블에 데이터를 INSERT하는 것보다 성능이 저하될 수 밖에 없음.(많이 저하됨..)
     

    사용하는 방법

    테이블 생성하는 것과 동일한 방식으로 생성
    << 단, 아래의 두 가지는 반드시 넣어줘야한다 >>
    1. PRIMARY KEY 제약조건
      1. IOT는 인덱스 리프 블록에 모든 데이터를 포함 시킨다고 했다. 따라서 LEAF INDEX 를 만들 때 기준이 되는 COLUMN이 있어야 한다. 각 기준 컬럼의 값들은 기준 컬럼을 기준으로 LEAF INDEX 처럼 만들어진다고 보면 된다. (만약, 100명의 사원이 존재하여 EMPNO가 100개라면, 100개의 LEAF INDEX가 만들어진다고 보면 된다)
    1. ORGANIZATION INDEX
      1. 이 명령을 넣어줘야 IOT가 되는 것
      2. 이렇게 되면, 테이블이지만 인덱스 구조를 가진 테이블이 생성됨
    < IOT 테이블 생성하기 > CREATE TABLE EMP_IOT ( EMPNO NUMBER(4) primary key , ENAME VARCHAR2(10) NULL, JOB VARCHAR2(9) NULL, MGR NUMBER(4) NULL, HIREDATE DATE NULL, SAL NUMBER(7, 2) NULL, COMM NUMBER(7, 2) NULL, DEPTNO NUMBER(2) NULL ) organization index; --이것을 넣어줘야 IOT가 되는 것 --이렇게 되면, 인덱스 구조의 테이블이 생성됨 --즉, 테이블이지만 인덱스임(테이블이 맞음) insert into emp_iot select * from emp; commit; EMP테이블의 데이터를 IOT테이블에 INSERT 따라서 EMP테이블에 ACCESS하는 것과 차원이 다르게 속도가 빨라짐
    IOT그림
    IOT그림
    INDEX의 LEAF블록에 테이블의 데이터를 다 넣어버림 따라서 TABLE ACCESS를 할 필요 없이 INDEX에서 그냥 끝나버림
     

    예제

    예제1) EMP테이블과 EMP_IOT 테이블의 성능 비교를 하시오
    <튜닝전> select * from emp where empno = 7788; @p ------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 7 | |* 1 | TABLE ACCESS FULL| EMP | 1 | 1 | 1 |00:00:00.01 | 7 | ------------------------------------------------------------------------------------
    버퍼의 개수 : 7개
    notion image
    <튜닝후> select * from emp_iot where empno = 7788; @p ------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 1 | |* 1 | INDEX UNIQUE SCAN| SYS_IOT_TOP_79903 | 1 | 1 | 1 |00:00:00.01 | 1 | -------------------------------------------------------------------------------------------------
    버퍼의 개수 : 1개
    notion image
     
     

    문제풀며 익히기

     
    Q1.
    EMP와 EMP_IOT테이블의 저장 공간을 비교하시오
    #1. emp 테이블의 저장공간 확인하기(테이블의 크기 확인 가능한 SQL) -- 테이블 저장 공간 확인하는 SQL 방법 select segment_name, segment_type, bytes, blocks from user_segments where segment_name = 'EMP'; --대문자 -- 결과 확인 SEGMENT_NAME|SEGMENT_TYPE|BYTES|BLOCKS| ------------+------------+-----+------+ EMP |TABLE |65536| 8| ======================================================================== **BLOCK 1개 = 8KB** 따라서 테이블의 크기는 8 BLOCKS x 8KB = 64KB => 테이블의 크기
    notion image
    #2. EMP_IOT의 저장공간 확인하기 -- IOT 저장공간 확인하기 전에 우선 ANALYZE 조회 해야함 analyze table emp_iot compute statistics; -- IOT 크기 조회 방법 select index_name, leaf_blocks from user_indexes where table_name = 'EMP_IOT'; --대문자 -- 결과 확인 INDEX_NAME |LEAF_BLOCKS| -----------------+-----------+ SYS_IOT_TOP_79903| 1| ======================================================================== ** BLOCK 1개 = 8KB ** 따라서 테이블의 크기 : 8KB 즉, USB 8GB짜리가 1GB로 줄어든것과 비슷함
    notion image
     
     
     
     
     
     
     

    튜닝36. INDEX UNIQUE SCAN을 사용하세요! ( UNIQUE 인덱스를 사용하세요!)

    📖

    INDEX UNIQUE SCAN

    UNIQUE INDEX

    PRIMARY KEY 제약이나, UNIQUE 제약을 컬럼에 걸게되면, UNIQUE인덱스 자동 생성
    UNIQUE 인덱스는 데이터 값이 중복 되어 있지 않아야 생성될 수 있음 그래서 옵티마이저가 좋아하는 인덱스 중 하나
     
    • 힌트
      • /*+ index( *[테이블명] [인덱스명]* ) */
         

    UNIQUE INDEX 생성방법

    1. 수동 - UNIQUE 제약
      1. create unique index emp_ename on emp(ename);
    1. 자동 - PRIMARY KEY 제약
      1. alter table emp add constraint emp_empno_pk primary key(empno);
        notion image
     

    UNIQUE INDEX 그림으로 보기

    notion image
    EMPNO = PK
    컬럼값이 ASC로 정렬된 EMP_EMPNO_PK 인덱스
    notion image
    ename은 unique한 값이 아니기 때문에 unique한 값인 empno 인덱스를 사용하는 것이 좋음
     
    • 유니크 스캔 방법
      • INDEX RANGE SCAN의 경우 : 7788 찾으면 RNAGE는 7788이 아닌 것 까지 즉, 7839까지 읽는데,
      • UNIQUE INDEX SCAN의 경우 : 중복되지 않음이 보장되어있기 때문에 아닌 것은 읽을 필요 없이 7788만 읽고 바로 EMP테이블로 TABLE ACCESS함
     

    예제

     
    예제1) EMP테이블에 EMPNO에 PRIMARY KEY 제약을 거시오
    @demo --PK 제약조건 추가하기 alter table emp add constraint emp_empno_pk primary key(empno); --확인 select index_name, uniqueness from user_indexes where table_name = 'EMP'; --결과 INDEX_NAME |UNIQUENESS| ------------+----------+ EMP_EMPNO_PK|UNIQUE | - INDEX_NAME : 제약조건 이름 - UNIQUENESS : UNIQUE 조건 여부
    notion image
     
    예제2)
    사원번호가 7788번인 사원의 사원번호와 사원이름, 월급을 출력하라
    select empno, ename, sal from emp where empno = 7788; @p ------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 2 | | 1 | TABLE ACCESS BY INDEX ROWID| EMP | 1 | 1 | 1 |00:00:00.01 | 2 | |* 2 | INDEX UNIQUE SCAN | EMP_EMPNO_PK | 1 | 1 | 1 |00:00:00.01 | 1 | ------------------------------------------------------------------------------------------------------
    notion image
     
    예제3) emp테이블의 ename에 인덱스를 생성하시오
    create index emp_ename on emp(ename);
     
    예제4)
    사원번호가 7788번이고, 이름이 SCOTT인 사원의 사원번호, 이름, 월급을 출력하는 SQL은 UNIQUE INDEX와 NON UNIQUE INDEX중에 어떤 인덱스를 옵티마이저가 선택할까?
    select empno, ename, sal from emp where empno = 7788 and ename = 'SCOTT'; @p ------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 2 | |* 1 | TABLE ACCESS BY INDEX ROWID| EMP | 1 | 1 | 1 |00:00:00.01 | 2 | |* 2 | INDEX UNIQUE SCAN | EMP_EMPNO_PK | 1 | 1 | 1 |00:00:00.01 | 1 | ------------------------------------------------------------------------------------------------------ ======================================== empno : unique index ename : non unique index unique index를 읽는게 비용이 더 적게 들기 때문에 옵티마이저가 INDEX UNIQUE SCAN을 함
    notion image

    문제풀며 익히기

     
    Q1.
    아래의 SQL의 인덱스를 EMP_ENAME 인덱스를 액세스 하도록 힌트를 주시오
    select /*+ index(emp emp_ename) */ empno, ename, sal from emp where empno = 7788 and ename = 'SCOTT'; @p ----------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ----------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 3 | |* 1 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 1 | 1 |00:00:00.01 | 3 | |* 2 | INDEX RANGE SCAN | EMP_ENAME | 1 | 1 | 1 |00:00:00.01 | 2 | -----------------------------------------------------------------------------------------------------------
    notion image
     
    Q2. 차이 확인하기
    select empno, ename, sal from emp where empno = 7788 and ename = 'SCOTT'; << INDEX UNIQUE SCAN >> ------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 2 | |* 1 | TABLE ACCESS BY INDEX ROWID| EMP | 1 | 1 | 1 |00:00:00.01 | 2 | |* 2 | INDEX UNIQUE SCAN | EMP_EMPNO_PK | 1 | 1 | 1 |00:00:00.01 | 1 | ------------------------------------------------------------------------------------------------------ 버퍼의 개수 : 2개 << INDEX RANGE SCAN >>> select /*+ index(emp emp_ename) */ empno, ename, sal from emp where empno = 7788 and ename = 'SCOTT'; ----------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ----------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 3 | |* 1 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 1 | 1 |00:00:00.01 | 3 | |* 2 | INDEX RANGE SCAN | EMP_ENAME | 1 | 1 | 1 |00:00:00.01 | 2 | ----------------------------------------------------------------------------------------------------------- 버퍼의 개수 : 3개
     
     

    ■ 조인 튜닝

    튜닝37. 조인(JOIN)할때는 조인 순서가 아주 중요합니다.

    📖
     

    조인 순서의 중요성

    notion image
    • DRIVING TABLE : 먼저 읽는 테이블
    • DRIVEN TABLE : 나중에 읽는 테이블
     
    1. DEPT테이블이 DRIVING TBALE인 경우 ⇒ 조인시도 4번
      1. 부서번호 10번이고 부서위치가 NEW YORK 인 사람 부터 EMP테이블에 찾으러 감. → 찾은 후 부서번호 20번, DALLAS 찾으러감 ~~
        ⇒ 조인시도 4번 했음!! (부서번호의 개수만큼 읽었다고 보면 됨)
     
    1. EMP 테이블이 DRIVING TABLE인 경우 ⇒ 조인시도 14번
      1. 이름을 가지고 조인시도를 하라고 했으니 KING을 찾고 부서번호를 찾아 10번을 가지고 DEPT테이블에 조인시도 , 그 후 LOC 찾고 돌아감 → BLACK도 이걸 반복
        ⇒ 조인시도 14번 (사원이름 개수만큼 읽었다고 보면됨)
         

    ORDERED 힌트 사용 예제

    /*+ ordered */
    예제1) EMP와 DEPT를 조인해서 이름과 부서위치를 출력하시오.
    @demo select ename, loc from emp e, dept d where e.deptno = d.deptno; ENAME LOC -------------------- --------------- KING NEW YORK BLAKE CHICAGO CLARK NEW YORK JONES DALLAS MARTIN CHICAGO ALLEN CHICAGO TURNER CHICAGO JAMES CHICAGO WARD CHICAGO FORD DALLAS SMITH DALLAS SCOTT DALLAS ADAMS DALLAS MILLER NEW YORK @p ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 14 | | | | |* 1 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 14 | 1797K| 1797K| 1017K (0)| | 2 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | | | | 3 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 7 | | | | ---------------------------------------------------------------------------------------------------------------- ===================================================== join순서가 작은 테이블 먼저 읽고 (dept) 큰 테이블을 다음에 읽음(emp) 옵티마이저가 똑똑하게 읽은 것 즉, 힌트가 없으면 오히려 옵티마이저가 자유롭게 실행계획을 설정하여 더 똑똑한 결과가 나올 수 있으니, 튜닝할 때 힌트를 없애보기
    notion image
     
    예제2) 이번에는 조인 순서를 EMP → DEPT 순이 되게 하시오
    @demo select /*+ ordered */ename, loc from emp e, dept d where e.deptno = d.deptno; @p ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 14 | | | | |* 1 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 14 | 1856K| 1856K| 998K (0)| | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | | | 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | | | | ----------------------------------------------------------------------------------------------------------------
    notion image

    LEADING 힌트 사용 예제

    /*+ leading(e d) */
     
    예제 3) 예제2번을 leaing 힌트를 사용해서 실행하시오
    select /*+ leading(e d) */ e.ename, d.loc from emp e, dept d where e.deptno = d.deptno;
     

    문제풀며 익히기

     
    Q1.
    EMP와 SALGARDE 테이블을 조인해서 이름과 월급과 급여등급을 출력하는데, 조인 순서가 SALGRADE —→ EMP순이 되게 하시오
    select /*+ leading(s e) */ e.ename, e.sal, s.grade from emp e, salgrade s where e.sal between s.losal and s.hisal; ENAME SAL GRADE -------------------- ---------- ---------- SMITH 800 1 JAMES 950 1 ADAMS 1100 1 WARD 1250 2 MARTIN 1250 2 MILLER 1300 2 TURNER 1500 3 ALLEN 1600 3 CLARK 2450 4 BLAKE 2850 4 JONES 2975 4 FORD 3000 4 SCOTT 3000 4 KING 5000 5 @p ---------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 12 | | | | | 1 | MERGE JOIN | | 1 | 42 | 14 |00:00:00.01 | 12 | | | | | 2 | SORT JOIN | | 1 | 5 | 5 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)| | 3 | TABLE ACCESS FULL | SALGRADE | 1 | 5 | 5 |00:00:00.01 | 6 | | | | |* 4 | FILTER | | 5 | | 14 |00:00:00.01 | 6 | | | | |* 5 | SORT JOIN | | 5 | 14 | 40 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)| | 6 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | | ----------------------------------------------------------------------------------------------------------------------
    notion image
     
    Q2. 위의 조인순서를 EMP → SALGRADE 순이 되게 하시오
    select /*+ leading(e s) */ e.ename, e.sal, s.grade from emp e, salgrade s where e.sal between s.losal and s.hisal; @p ---------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 12 | | | | | 1 | MERGE JOIN | | 1 | 42 | 14 |00:00:00.01 | 12 | | | | | 2 | SORT JOIN | | 1 | 14 | 14 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)| | 3 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | | |* 4 | FILTER | | 14 | | 14 |00:00:00.01 | 6 | | | | |* 5 | SORT JOIN | | 14 | 5 | 40 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)| | 6 | TABLE ACCESS FULL| SALGRADE | 1 | 5 | 5 |00:00:00.01 | 6 | | | | ----------------------------------------------------------------------------------------------------------------------
    notion image
     
     

    튜닝38. 검색조건에 따라 조인 순서를 잘 결정해줘야해요! - DBA를 위한 TIP ★

    📖

    ★조인 순서

    - 작은 테이블은 차이를 안보이지만 큰 테이블에선 보임
    1. ORDERED 힌트 : FROM 절에서 기술한 테이블 순서대로 조인해라!
      1. /*+ ordered */
         
    1. LEADING 힌트 : LEADING 힌트 안에 쓴 테이블 순서대로 조인해라!
      1. /*+ leading([테이블별칭1] [테이블별칭2]) */ 테이블별칭1 : DRIVING TABLE로 설정하고 싶은 테이블 테이블별칭2 : DIRVEN TABLE로 설정하고 싶은 테이블
         
    notion image

    예제

    예제1) 이름이 SCOTT인 사원의 이름과 부서위치를 출력하시오
    select e.ename, d.loc from emp e, dept d where e.deptno = d.deptno and e.ename = 'SCOTT'; ENAME LOC -------------------- -------------------------- SCOTT DALLAS
    notion image
     
    예제2) 위의 sql의 조인 순서는 아래의 2가지 중 어떤게 좋을까요?
    1. DEPT → EMP
    1. EMP → DEPT
    검색 조건 때문에 2번이 더 좋음!! SCOTT을 찾아 딱 한번만 JOIN을 시도하면 되기 때문임 DEPT테이블에서 시작한다면, 4번 조인시도 해야함
     

    문제풀며 익히기— 팁 있음

     
    Q1. ( DBA를 위한 TIP ★ )
    아래의 sql의 가장 좋은 조인순서를 힌트로 정하시오
    select e.ename, d.loc from emp e, dept d where e.deptno = d.deptno and e.job = 'SALESMAN' and d.loc = 'CHICAGO'; ENAME LOC -------------------- -------------------------- MARTIN CHICAGO ALLEN CHICAGO TURNER CHICAGO WARD CHICAGO
    A. 위와 같은 조건이 두 개 있을 때는 각각의 건수가 몇 건 있는지 조사해봐야함 건수가 적은 것을 driving table로 두는게 좋음 따라서 정답은 아래와 같음(현업에서도 이렇게 함. 확실하게 하는게 좋은것 ★) select count(*) from emp where job = 'SALESMAN'; select count(*) from dept where loc = 'CHICAGO'; select /*+ leading(d e) */ e.ename, d.loc from emp e, dept d where e.deptno = d.deptno and e.job = 'SALESMAN' --4건 and d.loc = 'CHICAGO'; --1건 ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 14 | | | | |* 1 | HASH JOIN | | 1 | 1 | 4 |00:00:00.01 | 14 | 1744K| 1744K| 618K (0)| |* 2 | TABLE ACCESS FULL| DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | | | | |* 3 | TABLE ACCESS FULL| EMP | 1 | 3 | 4 |00:00:00.01 | 7 | | | | ----------------------------------------------------------------------------------------------------------------
    notion image
     
    Q2.
    아래의 환경을 구성하고 아래의 SQL 을 튜닝하시오 ! drop table sales200 purge; create table sales200 as select * from sh.sales; create table times200 as select * from sh.times; 튜닝전 : select /*+ gather_plan_statistics leading(s t) use_nl(t) */ t.calendar_year, sum(amount_sold) from sales200 s, times200 t where s.time_id = t.time_id and t.week_ending_day_id =1582 group by t.calendar_year; @p 혹은 SELECT * FROM TABLE(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST')); CALENDAR_YEAR SUM(AMOUNT_SOLD) ------------- ---------------- 1999 425394 --------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | --------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:02:16.87 | 49M| 4485 | | 1 | HASH GROUP BY | | 1 | 4 | 1 |00:02:16.87 | 49M| 4485 | | 2 | NESTED LOOPS | | 1 | 4386 | 6203 |00:02:16.87 | 49M| 4485 | | 3 | TABLE ACCESS FULL| SALES200 | 1 | 918K| 918K|00:00:01.54 | 4436 | 4433 | |* 4 | TABLE ACCESS FULL| TIMES200 | 918K| 1 | 6203 |00:02:15.15 | 49M| 52 | --------------------------------------------------------------------------------------------------- 튜닝전: 500000 개 ============================== 힌트 중 USE_NL : 나중에 배울 것
    notion image
    < 튜닝 후 > select /*+ leading(t s) user_nl(t) */ t.calendar_year, sum(amount_sold) from sales200 s, times200 t where s.time_id = t.time_id and t.week_ending_day_id =1582 group by t.calendar_year; ------------------------------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem | ------------------------------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.09 | 4495 | 4433 | | | | | 1 | HASH GROUP BY | | 1 | 4 | 1 |00:00:00.09 | 4495 | 4433 | 1088K| 1088K| | |* 2 | HASH JOIN | | 1 | 4386 | 6203 |00:00:00.09 | 4495 | 4433 | 1695K| 1695K| 1161K (0)| |* 3 | TABLE ACCESS FULL| TIMES200 | 1 | 7 | 7 |00:00:00.01 | 54 | 0 | | | | | 4 | TABLE ACCESS FULL| SALES200 | 1 | 918K| 918K|00:00:00.09 | 4440 | 4433 | | | | ------------------------------------------------------------------------------------------------------------------------------ 버퍼의 개수 : 4495개
    notion image

    튜닝39. 데이터 양이 작을 때는 NESTED LOOP JOIN으로 조인하세요!

    📖

    NESTED LOOP JOIN

    https://cafe.daum.net/oracleoracle/SntY/43
    가장 일반적으로 많이 쓰는 방법인데, 조인을 할 때 조인을 시도하는 데이터의 양이 작을 때는 nested loop 조인으로 조인함. (LOOP를 많이 돌면 안 좋기 때문에 데이터 양이 작을 때!)
    이 조인은 조인 연결고리가 되는 컬럼에 인덱스를 엑세스 할 수 있냐 없냐에 따라서 수행 속도 차이가 크다.
     
    강조 : nested loop 조인은 연결고리가 되는 컬럼에 인덱스가 있어야 합니다. 그래야 좋은 성능을 보입니다.
     
    • 의미
      • NESTED : 중첩 , LOOP : 반복문
      • 즉, LOOP 문 내에 LOOP가 있는데, 각 LOOP문 안에 실행문이 있음
      •  

    NESTED LOOP JOIN

    먼저 조인 순서를 결정하는 힌트를 작성하고, 조인할 방법을 결정하는 힌트를 넣어줘야함
    방법1. /*+ leading([테이블별칭1],[테이블별칭2]) use_nl([테이블별칭2] ) */ -- 콤마를 사용하는게 정석임, 오라클이 알아서 붙여줌(붙여줘야 성능상 더 좋을듯) -- 하지만 안붙여도 됨 -- 테이블별칭2의 연결고리 조건(where절에서 조인하는 컬럼)에 인덱스 만들어줘야함 driven table을 use_nl()의 괄호 안에 씀 방법2. /*+ leading([테이블별칭1]) use_nl([테이블별칭2] ) */ 가독성을 위해 방법1을 선호
     

    예제-그림설명 포함

    예제1) 이름과 부서위치를 출력하는데, NESTED LOOP 조인으로 수행하시오
    select /*+ leading(d,e) use_nl(e) */ e.ename, d.loc from emp e, dept d where e.deptno = d.deptno; @p ------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 32 | | 1 | NESTED LOOPS | | 1 | 14 | 14 |00:00:00.01 | 32 | | 2 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | |* 3 | TABLE ACCESS FULL| EMP | 4 | 4 | 14 |00:00:00.01 | 25 | ------------------------------------------------------------------------------------- ================================================= use_nl : nested loop조인 하라는 힌트 DEPTNO = 10번 가지고 내부 LOOP의 EMP 테이블 조인하고 DEPTNO = 20번 가지고 내부 LOOP의 EMP 테이블 조인
    notion image
     
    예제2) 아래의 조인문장을 NESTED LOOP조인으로 유도하시오 —인덱스 없음
    select /*+ leading(e,d) use_nl(d) */ e.ename, d.loc, e.sal from emp e, dept d where d.deptno = d.deptno and e.ename = 'SCOTT'; --1건이므로 한번만 조인시도 하면 됨 @p ------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 14 | | 1 | NESTED LOOPS | | 1 | 4 | 4 |00:00:00.01 | 14 | |* 2 | TABLE ACCESS FULL| EMP | 1 | 1 | 1 |00:00:00.01 | 7 | |* 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | ------------------------------------------------------------------------------------- full scan 시 버퍼를 14개나 잡아먹어서 index를 걸어주는게 좋음
    notion image
     
    notion image
    notion image
    • NESTED JOIN 수행 방법
    full scan해서 scott을 찾은 다음
    스캇의 부서번호가 20번이 됨
    그래서 e.deptno라는 컬럼이 20이라는 상수(숫자)가 됨
    그러면 deptno에서 20번을 뽑음
    EX. WHERE 20 = DEPTNO 이것도 성립하는 SQL임
     
    그래서 이름이 SCOTT인 사원의 20번을 가지고 DEPT테이블로 가서 FULL TABLE SCAN함(INDEX가 없을 경우)
     
    인덱스가 있으면, 20번을 가지고 d.deptno의 인덱스에서 rowid를 뽑아 dept테이블에서 loc를 바로 찾을 수 있음
     
    notion image
    표시한 두 곳에 index가 있으면 너무 좋은데, d.deptno에는 무조건 있어야함
     
    강조 : nested loop 조인은 연결고리가 되는 컬럼에 인덱스가 있어야 합니다. 그래야 좋은 성능을 보입니다.
     
    예제3) dept테이블에 deptno에 인덱스를 거는데 unique 인덱스로 생성하시오
    create unique index dept_deptno on dept(deptno); --혹은 pk제약조건도 가능
     
    예제4) 아래의 sql의 실행계획을 확인하시오 - index건 상태
    select /*+ leading(e,d) use_nl(d) index(d dept_deptno) */ e.ename, d.loc, e.sal from emp e, dept d where d.deptno = d.deptno and e.ename = 'SCOTT'; @p ------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 11 | | 1 | NESTED LOOPS | | 1 | 4 | 4 |00:00:00.01 | 11 | | 2 | NESTED LOOPS | | 1 | 4 | 4 |00:00:00.01 | 9 | |* 3 | TABLE ACCESS FULL | EMP | 1 | 1 | 1 |00:00:00.01 | 7 | |* 4 | INDEX FULL SCAN | DEPT_DEPTNO | 1 | 4 | 4 |00:00:00.01 | 2 | | 5 | TABLE ACCESS BY INDEX ROWID| DEPT | 4 | 4 | 4 |00:00:00.01 | 2 | ------------------------------------------------------------------------------------------------------ =================================== NESTED LOOP JOIN이 두 번 실행되는게 최신 실행계획 ** 1. NESTED LOOP JOIN이 두번 보이는 이유는 인덱스와 잘 조인했다고 강조하고자 실행계획에 나온것. NESTED LOOP JOIN을 빠르게 수행하고자 오라클에서 개발한것. 그래서 두 번 수행
    notion image
    notion image
    notion image
    FULL TABLE SCAN한 이유는 ENAME 에 인덱스가 없기 때문
    그래서 FULL TABLE SCAN 후 SCOTT 찾음.
    찾은 후 부서번호를 가지고 DEPT테이블 조인하는데,
    DEPTNO에는 INDEX가 걸려있기 때문에 바로 20번을 찾을 수 있음
    그래서 연결고리가 있는 컬럼에 INDEX가 걸려있어줘야함
     
    예제5) emp 테이블의 ename에 인덱스를 생성하시오
    create index emp_ename on emp(ename);
     
    예제6) 다시 예제 4번의 sql을 작성하시오
    select /*+ leading(e,d) use_nl(d) index(d dept_deptno) index(e emp_ename) */ e.ename, d.loc, e.sal from emp e, dept d where d.deptno = d.deptno and e.ename = 'SCOTT'; @p --------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 7 | | 1 | NESTED LOOPS | | 1 | 4 | 4 |00:00:00.01 | 7 | | 2 | NESTED LOOPS | | 1 | 4 | 4 |00:00:00.01 | 5 | | 3 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 1 | 1 |00:00:00.01 | 3 | |* 4 | INDEX RANGE SCAN | EMP_ENAME | 1 | 1 | 1 |00:00:00.01 | 2 | |* 5 | INDEX FULL SCAN | DEPT_DEPTNO | 1 | 4 | 4 |00:00:00.01 | 2 | | 6 | TABLE ACCESS BY INDEX ROWID | DEPT | 4 | 4 | 4 |00:00:00.01 | 2 | --------------------------------------------------------------------------------------------------------------- ================================================== index를 두 개 모두 써줘야함. 알아서 타는 데 왜 써야하냐면, 안써주면 안탈수도 있기 때문! 그래서 좋은 실행계획이 나올 수 있도록 옵티마이저에게 힌트를 줘야함
    notion image
     
     

    문제풀며 익히기

     
    Q1.
    아래의 sql 조인문을 nested loop조인으로 수행되게 하는데, 조인순서를 올바르게 고치시고, 인덱스가 필요한 컬럼에 인덱스를 거시오
    <튜닝전> select /*+ leading(s t) use_nl(t) */ t.calendar_year, sum(amount_sold) from sales200 s, times200 t where s.time_id = t.time_id and t.week_ending_day_id =1582 group by t.calendar_year;
    <튜닝후> create index sales200_time_id on sales200(time_id); select /*+ leading(t s) use_nl(s) */ t.calendar_year, sum(amount_sold) from sales200 s, times200 t where s.time_id = t.time_id and t.week_ending_day_id =1582 group by t.calendar_year; --------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | --------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.02 | 312 | 23 | | 1 | HASH GROUP BY | | 1 | 4 | 1 |00:00:00.02 | 312 | 23 | | 2 | NESTED LOOPS | | 1 | 4386 | 6203 |00:00:00.02 | 312 | 23 | | 3 | NESTED LOOPS | | 1 | 4403 | 6203 |00:00:00.01 | 86 | 23 | |* 4 | TABLE ACCESS FULL | TIMES200 | 1 | 7 | 7 |00:00:00.01 | 54 | 0 | |* 5 | INDEX RANGE SCAN | SALES200_TIME_ID | 7 | 629 | 6203 |00:00:00.01 | 32 | 23 | | 6 | TABLE ACCESS BY INDEX ROWID| SALES200 | 6203 | 629 | 6203 |00:00:00.01 | 226 | 0 | --------------------------------------------------------------------------------------------------------------------- ========================================================= 순서가 t s 즉, time200 테이블을 읽고 sales200을 읽기 때문에 연결고리인 sales200 테이블의 연결고리로 사용되고 있는 time_id에 인덱스를 만들어줘야됨
    notion image

    튜닝40. 대량의 데이터를 조인할 때는 해쉬조인을 사용하세요!(단, 남발하면 안됨)

    📖

    HASH JOIN

    해쉬 알고리즘을 사용하고 있는 해쉬함수를 이용해서, 메모리에 올라온 데이터를 찾아 조인하는 조인 방법. data를 메모리에 올려놓고 메모리에서 조인하는 조인방법. 메모리를 사용하므로 속도가 드라마틱하게 빠름
    해쉬 조인 시 가급적 index를 타지 않게 하는게 좋음(?- 아직 설명 못들음)
     
    ■PROB테이블의 데이터를 탐색하면서 연결고리가 되는 키 컬럼의 데이터를, 메모리로 올린 해쉬 테이블과 조인하면서 조인하는 방법이 해쉬조인이다
    ■해쉬 조인도 조인순서가 중요한데, 작은 테이블 또는 WHERE조건에 의해서 엑세스 되는 데이터가 작은 테이블이 메모리로 올라가게 해야함.
     

    해쉬 조인 남발하면 안됨, 왜? 해쉬조인은 메모리를 사용하는 조인이기 때문
     
    OLTP서버에 실시간 주문이 들어옴(지금 현재 중요한 데이터)
    DW서버에는 과거 데이터들이 전부 들어있어서 그것을 가지고 데이터를 분석해서 현재의 시스템을 개선해 나가는 것.
    그래서 DW서버에서는 대량의 데이터를 조인하기 때문에 여기서는 HASH JOIN을 사용해야하고, OLTP서버는 실시간으로 주문이 발생하고 바로바로 결과를 보여줘야하기 때문에, 입력되는 1건의 데이터가 자주 발생하는 현상이라고 볼 수 있음. 따라서 NL조인을 사용해야함. 왜? 메모리는 한정된 공간이기 때문임! 남발하면 좋지 않음. OLTP서버에서 HASH JOIN을 자제해야하고, 남발하면 안됨, DBA가 잘 확인해야함.

     

    HASH JOIN의 원리

    notion image
    select /*+ leading(d,e) use_hash(e) */ e.ename, d.loc from emp e, dept d where d.detpno = d.deptno; 힌트에서 앞에있는 d가 hash table이 되며, 뒤에 넣은 e(emp)가 prob테이블이 됨
    WHERE절에 검색조건 따로 없이 EMP와 DEPT테이블이 조인하는 상황
     
    만약 두 테이블이 1억건이 넘는다면, 이럴 때 해쉬 조인 사용
     
    오라클의 PGA라는 메모리에 두 테이블 중에 하나를 올리는데, 메모리도 한정된 공간이기 때문에 작은 테이블(여기선 DEPT)을 올려야함.
    notion image
    만약 큰 테이블을 메모리에 올린다면, 한 공간의 메모리에 올리지 못해서 TEMP라는 공간에도 나눠 올려야해서 I/O발생으로 좋지 않음.
     
    어쨌든, 메모리로 올라가게 되면 ROWID가 → DB 논리적 주소인 메모리 주소 값으로 저장되어 해쉬 함수에 저장됨
    • dept테이블이 pga 메모리로 올라감 → pga: rowid가 논리적인 메모리 주소 값으로 저장됨 → 해시 함수에 저장됨
    notion image
     
    KING의 부서번호를 해쉬함수에 넣고 메모리에 올라간 DEPT테이블의 ROWID(해쉬 값으로 변환됨) 를 해쉬함수에서 찾음(메모리 주소 값이 해쉬에 저장되어있음)
     
    • HASH TABLE : 메모리에 올라간 테이블
    • PROB TABLE : 메모리로 올라가지 않은 즉, DISK쪽에 있는 테이블
     
    정리
    PROB테이블의 데이터를 탐색하면서 연결고리가 되는 키 컬럼의 데이터를, 메모리로 올린 해쉬 테이블과 조인하면서 조인하는 방법이 해쉬조인이다
     
    해쉬 조인도 조인순서가 중요한데, 작은 테이블 또는 WHERE조건에 의해서 엑세스 되는 데이터가 작은 테이블이 메모리로 올라가게 해야함.
     

    검색 조건이 있었을 때의 해쉬조인 순서

    notion image
    해시 테이블인 dept에서 loc가 CHICAGO인 행(부서번호 30번 행)의 데이터가 PGA메모리에 올라가게 됨
    select /*+ leading(d e) use_hash(e) */ e.ename, d.loc from emp e, dept d where e.deptno = d.deptno ;
    prob 테이블의 데이터를 탐색하면서 연결고리가 되는 키 컬럼의 데이터를
    메모리로 올린 해쉬 테이블과 조인하면서 조인하는 방법이 해쉬조인입니다.
    해쉬조인도 조인순서가 중요한데 작은 테이블 또는 where 조건에 의해서 엑세스 되는
    데이터가 작은 테이블이 메모리로 올라가게 해야 합니다.

    예제

     
    예제1) 아래의 SQL을 해쉬조인하세요
    • 조인순서 : DEPT → EMP
    select e.ename, d.loc from emp e, dept d where e.deptno = d.deptno;
    A. select /*+ leading(d,e) use_hash(e) */ e.ename, d.loc from emp e, dept d where e.deptno = d.deptno; ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 14 | | | | |* 1 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 14 | 1797K| 1797K| 1034K (0)| | 2 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | | | | 3 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 7 | | | | ----------------------------------------------------------------------------------------------------------------
    notion image
     
    Q2. 아래 조인 문장의 해쉬 조인 순서를 결정하시오
    select e.ename, d.loc from emp e, dept d where e.deptno = d.deptno --4건 and e.ename = 'SCOTT'; --1건
    A. @demo select /*+ leading(e,d) use_hash(d) */ e.ename, d.loc from emp e, dept d where e.deptno = d.deptno and e.ename = 'SCOTT'; @p ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 14 | | | | |* 1 | HASH JOIN | | 1 | 1 | 1 |00:00:00.01 | 14 | 1856K| 1856K| 671K (0)| |* 2 | TABLE ACCESS FULL| EMP | 1 | 1 | 1 |00:00:00.01 | 6 | | | | | 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | | | | ----------------------------------------------------------------------------------------------------------------
    notion image
     
     

    문제풀며 익히기

     
    Q1. 아래의 sql의 조인을 해쉬조인으로 수행하시오.
    <튜닝 전: 최악의 sql왜? 대용량 테이블을 nested loop join을 타니까 > select /*+ leading(s,t) user_nl(t) */ t.calendar_year, sum(amount_sold) from sales200 s, times200 t where s.time_id = t.time_id and t.week_ending_day_id =1582 group by t.calendar_year;
    <튜닝후> select /*+ leading(t,s) use_hash(s) */ t.calendar_year, sum(amount_sold) from sales200 s, times200 t where s.time_id = t.time_id and t.week_ending_day_id =1582 group by t.calendar_year; @p --------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | --------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.02 | 4495 | | | | | 1 | HASH GROUP BY | | 1 | 4 | 1 |00:00:00.02 | 4495 | 1088K| 1088K| | |* 2 | HASH JOIN | | 1 | 4386 | 6203 |00:00:00.02 | 4495 | 1695K| 1695K| 1183K (0)| |* 3 | TABLE ACCESS FULL| TIMES200 | 1 | 7 | 7 |00:00:00.01 | 54 | | | | | 4 | TABLE ACCESS FULL| SALES200 | 1 | 918K| 918K|00:00:00.01 | 4440 | | | | ---------------------------------------------------------------------------------------------------------------------
    점 조건이라 데이터가 작을 t(time_id)테이블을 해시테이블로 구성하는 것이 튜닝 된 것.
    notion image
     
    Q2. 아래의 환경을 만들고 아래의 sql을 튜닝하시오
    -- 환경구성 create table customers200 as select * from sh.customers; select count(*) from customers200; COUNT(*) ---------- 55500
    <튜닝전> select /*+ leading(s c) use_nl(c) */ count(*) from sales200 s, customers200 c where s.cust_id = c.cust_id and c.country_id = 52790 and s.time_id between to_date('1999/01/01','YYYY/MM/DD') and to_date('1999/12/31','YYYY/MM/DD'); 거의 멈춤.. 그냥 나감
    <튜닝후> -- 어떤 데이터가 더 작은지 확인 select count(*) from customers200 where country_id = 52790; COUNT(*) ---------- 18520 select count(*) from sales200 where time_id between to_date('1999/01/01','YYYY/MM/DD') and to_date('1999/12/31','YYYY/MM/DD'); COUNT(*) ---------- 247945 --sql 튜닝 select /*+ leading(c s) use_hash(s) */ count(*) from sales200 s, customers200 c where s.cust_id = c.cust_id and c.country_id = 52790 and s.time_id between to_date('1999/01/01','YYYY/MM/DD') and to_date('1999/12/31','YYYY/MM/DD'); ------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.02 | 5961 | | | | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.02 | 5961 | | | | |* 2 | HASH JOIN | | 1 | 95316 | 141K|00:00:00.02 | 5961 | 2801K| 2801K| 2279K (0)| |* 3 | TABLE ACCESS FULL| CUSTOMERS200 | 1 | 2921 | 18520 |00:00:00.01 | 1520 | | | | |* 4 | TABLE ACCESS FULL| SALES200 | 1 | 230K| 247K|00:00:00.01 | 4440 | | | | ------------------------------------------------------------------------------------------------------------------------- =========================================== count하기 귀찮으면 그냥 경우의 수 넣어서 힌트만 살짝 고쳐서 돌려봐도 됨 어쨌든,, 점조건이 더 좋지 않을까? (내생각)
    notion image

    튜닝41. 3개의 테이블을 해쉬조인 할 때는 이 힌트를 반드시 알아야합니다. (★ 튜너들 무조건 알아야함 ★ )

    📖
    notion image
    notion image
    notion image

    ★★3개 이상의 테이블 해쉬조인 할 때 힌트★★

    <<방법1>> /*+ leading(e,d,b) use_hash(d) use_hash(b) */ --순서주기 ** leading(e,d,b) ** emp-dept : hash join emp 테이블과 dept테이블이 hash join 한 결과가 hash table이 됨 위의 결과 데이터가 hash table이 되어 이것과 bonus가 hash join 하게 됨 ** use_hash(d) use_hash(b) ** 조인순서 작성 후 뒤에는 prob table작성해주면 됨 ------------------------------------------------------------------ <<방법2>> /*+ leading(b,e,d) use_hash(e) use_hash(d) swap_join_inputs(d) */ ** swap_join_inputs(d) ** : 해쉬 테이블로 구성할 테이블을 결정해줌 즉, prob table을 hash table로 사용하고 싶을 때 주로 사용하는 힌트 ** no_swap_join_inputs(d) ** : prob테이블을 결정하는 힌트
    notion image
     

    검색 조건이 있었을 때의 해쉬조인 순서는?

    notion image
    notion image
     
     
    실습환경 구성
    drop table bonus; create table bonus as select empno, sal*1.2 as bonus from emp;
     

    예제

     
    예제1) emp와 dept와 bonus를 조인해서 아래의 sql을 작성하시오
    select e.ename, d.loc, b.bonus from emp e, dept d, bonus b where e.deptno = d.deptno and e.empno = b.empno; --------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem | --------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 17 | 1 | | | | |* 1 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 17 | 1 | 1476K| 1476K| 1560K (0)| |* 2 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 13 | 0 | 1797K| 1797K| 1139K (0)| | 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | 0 | | | | | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | 0 | | | | | 5 | TABLE ACCESS FULL | BONUS | 1 | 14 | 14 |00:00:00.01 | 3 | 1 | | | | ---------------------------------------------------------------------------------------------------------------------------
    notion image
     
    예제2) 위의 조인순서를 아래와 같이 되게 하시오
    • 조인순서 : emp → dept → bonus
    select /*+ leading(e,d,b) use_hash(d) use_hash(b) */ e.ename, d.loc, b.bonus from emp e, dept d, bonus b where e.deptno = d.deptno and e.empno = b.empno; ------------------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ------------------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 17 | | | | |* 1 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 17 | 1476K| 1476K| 1590K (0)| |* 2 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 13 | 1651K| 1651K| 894K (0)| | 3 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | | | 4 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | | | | 5 | TABLE ACCESS FULL | BONUS | 1 | 14 | 14 |00:00:00.01 | 3 | | | | ------------------------------------------------------------------------------------------------------------------ ==================================================== /*+ leading(e,d,b) use_hash(d) use_hash(b) */ ** leading(e,d,b) ** emp-dept : hash join emp 테이블과 dept테이블이 hash join 한 결과가 hash table이 됨 위의 결과 데이터가 hash table이 되어 이것과 bonus가 hash join 하게 됨 ** use_hash(d) use_hash(b) ** 조인순서 작성 후 뒤에는 prob table작성해주면 됨
    notion image
     
    예제3) 위의 조인순서를 아래와 같이 되게하시오(조인 방법은 해쉬조인)
    • 조인순서 : bonus → emp → dept
    select /*+ leading(b,e,d) use_hash(e) use_hash(d) */ e.ename, d.loc, b.bonus from emp e, dept d, bonus b where e.deptno = d.deptno and e.empno = b.empno; @p ------------------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ------------------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 17 | | | | |* 1 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 17 | 1651K| 1651K| 978K (0)| |* 2 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 9 | 1995K| 1995K| 1493K (0)| | 3 | TABLE ACCESS FULL| BONUS | 1 | 14 | 14 |00:00:00.01 | 2 | | | | | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | | | 5 | TABLE ACCESS FULL | DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | | | | ------------------------------------------------------------------------------------------------------------------
    notion image
     

    문제풀며 익히기- swap_join_inputs

     
    Q1. 아래와 같이 실행계획이 나오게 하시오
    notion image
    select /*+ leading(b,e,d) use_hash(e) use_hash(d) */ e.ename, d.loc, b.bonus from emp e, dept d, bonus b where e.deptno = d.deptno and e.empno = b.empno; ---------------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem || ---------------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 49 | 8 | | | || |* 1 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 49 | 8 | 1651K| 1651K| 1028K (0)|| |* 2 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 9 | 2 | 1995K| 1995K| 1622K (0)|| | 3 | TABLE ACCESS FULL| BONUS | 1 | 14 | 14 |00:00:00.01 | 2 | 2 | | | || | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | 0 | | | || | 5 | TABLE ACCESS FULL | DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | 6 | | | || ---------------------------------------------------------------------------------------------------------------------------| hash테이블인 보너스와 emp 조인 그 결과와 dept와 해쉬조인 해야하는데, 제공된 실행계획에서 보면 dept 테이블 엑세스를 먼저 실행되기 때문에 dept가 hash table select /*+ leading(b,e,d) use_hash(e) use_hash(d) swap_join_inputs(d) */ e.ename, d.loc, b.bonus from emp e, dept d, bonus b where e.deptno = d.deptno and e.empno = b.empno; ------------------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ------------------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 17 | | | | |* 1 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 17 | 1797K| 1797K| 1084K (0)| | 2 | TABLE ACCESS FULL | DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | | | |* 3 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 10 | 1995K| 1995K| 1458K (0)| | 4 | TABLE ACCESS FULL| BONUS | 1 | 14 | 14 |00:00:00.01 | 2 | | | | | 5 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 7 | | | | ------------------------------------------------------------------------------------------------------------------
    notion image
     
    Q2. sql의 실행계획이 아래와 같이 실행계획이 나오게 하시오
    notion image
    select /*+ leading(d,e,b) use_hash(e) use_hash(b) */ e.ename, d.loc, b.bonus from emp e, dept d, bonus b where e.deptno = d.deptno and e.empno = b.empno; ------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 17 | | | || |* 1 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 17 | 1476K| 1476K| 1532K (0)|| |* 2 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 13 | 1797K| 1797K| 1024K (0)|| | 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | | || | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || | 5 | TABLE ACCESS FULL | BONUS | 1 | 14 | 14 |00:00:00.01 | 3 | | | || ------------------------------------------------------------------------------------------------------------------| select /*+ leading(d,e,b) use_hash(e) use_hash(b) swap_join_inputs(b) */ e.ename, d.loc, b.bonus from emp e, dept d, bonus b where e.deptno = d.deptno and e.empno = b.empno; @p ------------------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ------------------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 17 | | | | |* 1 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 17 | 1995K| 1995K| 1570K (0)| | 2 | TABLE ACCESS FULL | BONUS | 1 | 14 | 14 |00:00:00.01 | 2 | | | | |* 3 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 14 | 1797K| 1797K| 1011K (0)| | 4 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | | | | 5 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 7 | | | | ------------------------------------------------------------------------------------------------------------------
    notion image
     
    Q3. 아래의 환경을 구성하고, 아래의 sql을 수행하시오
    -환경 구성 create table sale200 as select * from sh.sales; create table times200 as select * from sh.times; create table products200 as select * from sh.products; select p.prod_name, t.CALENDAR_YEAR, sum(s.amount_sold) from sales200 s, times200 t, products200 p where s.time_id = t.time_id and s.prod_id = p.prod_id and t.CALENDAR_YEAR in (2000,2001) and p.prod_name like 'Deluxe%' group by p.prod_name, t.calendar_year; PROD_NAME |CALENDAR_YEAR|SUM(S.AMOUNT_SOLD)| ------------+-------------+------------------+ Deluxe Mouse| 2000| 53224.73| Deluxe Mouse| 2001| 142334.42|
    notion image
     
    Q4. 위의 sql의 실행계획이 아래와 같이 출력되게하시오
    notion image
    select /*+ leading(s,t,p) use_hash(t) use_hash(p) swap_join_inputs(p) */ p.prod_name, t.CALENDAR_YEAR, sum(s.amount_sold) from sales200 s, times200 t, products200 p where s.time_id = t.time_id and s.prod_id = p.prod_id and t.CALENDAR_YEAR in (2000,2001) and p.prod_name like 'Deluxe%' group by p.prod_name, t.calendar_year; ------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 2 |00:00:00.11 | 4499 | | | | | 1 | HASH GROUP BY | | 1 | 2 | 2 |00:00:00.11 | 4499 | 921K| 921K| | |* 2 | HASH JOIN | | 1 | 6474 | 6669 |00:00:00.11 | 4499 | 1538K| 1538K| 652K (0)| |* 3 | TABLE ACCESS FULL | PRODUCTS200 | 1 | 1 | 1 |00:00:00.01 | 3 | | | | |* 4 | HASH JOIN | | 1 | 459K| 492K|00:00:00.09 | 4495 | 56M| 8196K| 77M (0)| | 5 | TABLE ACCESS FULL| SALES200 | 1 | 918K| 918K|00:00:00.02 | 4440 | | | | |* 6 | TABLE ACCESS FULL| TIMES200 | 1 | 730 | 731 |00:00:00.01 | 54 | | | | -------------------------------------------------------------------------------------------------------------------------

    튜닝42. HASH JOIN이 안되는 연산자

    📖

    HASH JOIN이 안되는 연산자

    해쉬조인이 가능하려면, 반드시 조인의 연결고리가 =(EQUAL)조건이어야 한다.
    무조건! 반드시! 이퀄로 조인 연결고리 사용해야 hash join 가능!
     
    예제1)
    EMP와 SALGRADE를 조인해서 이름,월급, 급여등급(GRADE)을 출력하는데, 해쉬조인이 되게하시오
    select /*+ leading(s,e) use_hash(e) */ e.ename, e.sal, s.grade from emp e, salgrade s where e.sal between s.losal and s.hisal; ----------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ----------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 38 | | 1 | NESTED LOOPS | | 1 | 42 | 14 |00:00:00.01 | 38 | | 2 | TABLE ACCESS FULL| SALGRADE | 1 | 5 | 5 |00:00:00.01 | 7 | |* 3 | TABLE ACCESS FULL| EMP | 5 | 8 | 14 |00:00:00.01 | 31 | ----------------------------------------------------------------------------------------- ============================================================ between .. and는 선분조건이므로 hash join이 안된것을 확인할 수 있다.
    notion image
     
    만약 emp와 salgrade 테이블이 대용량 테이블이어서 nested loop join을 하면 너무 성능이 느리다면, 어떻게 튜닝해야하나?
    A. SORT MERGE JOIN을 사용하면 됨 (튜닝예제 43)
     

    튜닝43. 대량의 테이블을 조인하는데, 해쉬조인을 할 수 없다면, SORT MERGE JOIN을 하세요.

    📖
    notion image
    ⇒ emp 테이블과 salgrade 테이블이 모두 대용량 데이터를 포함하고 있고, 이들을 조인할 때 nested loop 조인을 사용하면 성능이 매우 느립니다. between 조건을 사용하는 현재의 쿼리 상황에서 해시 조인(hash join)이 불가능할 때, 성능을 향상시킬 수 있는 조인 방법

    SORT MERGE JOIN

    notion image
    컬럼의 데이터를 미리 정렬해놓고, 조인을 하는 조인 방법이다.
    미리 연결고리 컬럼(즉, key가 될 컬럼)을 정렬해 놓았으므로 빠르게 조인을 수행할 수 있음.
     
    이것도 조인 순서가 중요함(leading사용)

    SORT MERGE JOIN이 유용할 때

    1. 해쉬조인을 해야하는데, 해쉬조인을 할 수 없을 때
      1. t1.daumcdn.net
        https://t1.daumcdn.net/cafeattach/zchT/3b43c333b4c7ae00540c09ac2e87bce8c46f19d7
        ex. 튜닝예제42와 같이 조인 조건에 이퀄 연산자를 사용할 수 없는 경우
     
    1. 조인된 결과를 정렬해서 봐야할 때
     

    SORT MERGE JOIN 의 원리

    https://cafe.daum.net/oracleoracle/Snri/158
    emp 테이블에서 deptno에 대해서 내부적으로 asc하게 정렬이 됨(dept 테이블도 키 컬럼(연결조건)인 deptno를 기준으로 asc하게 정렬됨)
    정렬이 되어져서 같은 deptno끼리 모여져 있으니까 full scan을 할 필요가 없음
    따라서 정렬해놨기 때문에 빠르게 가서 스캔할 수 있음
     
    이것도 순서 중요! emp에서 시작하면 14번 조인해야하며, dept에서 시작하면 4번만 조인
    notion image
     
    즉, 데이터를 미리 정렬해놓고 데이터를 조회하는 조인이 sort merge join
    (sort merge join 은 조인하려는  컬럼의 데이터를 미리 정렬해놓고   조인을 하는 조인 방법입니다.   미리 연결고리 컬럼을 정렬해 놓았으므로 빠르게 조인을 수행할 수 가 있습니다.)
     
    예제1)
    select /*+ leading(d,e) use_merge(e) */ e.ename, d.loc, e.deptno from emp e, dept d where e.deptno = d.deptno; ENAME LOC DEPTNO -------------------- -------------------------- ---------- KING NEW YORK 10 CLARK NEW YORK 10 MILLER NEW YORK 10 ADAMS DALLAS 20 SCOTT DALLAS 20 SMITH DALLAS 20 FORD DALLAS 20 JONES DALLAS 20 WARD CHICAGO 30 JAMES CHICAGO 30 ALLEN CHICAGO 30 MARTIN CHICAGO 30 BLAKE CHICAGO 30 TURNER CHICAGO 30 ----------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ----------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 12 | | | | | 1 | MERGE JOIN | | 1 | 14 | 14 |00:00:00.01 | 12 | | | | | 2 | SORT JOIN | | 1 | 4 | 4 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)| | 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | | | |* 4 | SORT JOIN | | 4 | 14 | 14 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)| | 5 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | | ----------------------------------------------------------------------------------------------------------------- =================== order by절을 사용하지 않았는데도 출력결과를 보면 부서번호끼리 모여있고, 정렬되어있는 것을 확인할 수 있다.
    notion image
     

    문제풀며 익히기

     
    Q1.
    emp와 salgrade 테이블에서 이름, 월급, 급여등급(grade)을 출력하는데, sort merge join이 되게 하시오.
    select /*+ leading(s e) use_merge(e) */ e.ename, e.sal, s.grade from emp e, salgrade s where e.sal between s.losal and s.hisal; ENAME SAL GRADE -------------------- ---------- ---------- SMITH 800 1 JAMES 950 1 ADAMS 1100 1 WARD 1250 2 MARTIN 1250 2 MILLER 1300 2 TURNER 1500 3 ALLEN 1600 3 CLARK 2450 4 BLAKE 2850 4 JONES 2975 4 FORD 3000 4 SCOTT 3000 4 KING 5000 5 @p ---------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 12 | | | | | 1 | MERGE JOIN | | 1 | 42 | 14 |00:00:00.01 | 12 | | | | | 2 | SORT JOIN | | 1 | 5 | 5 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)| | 3 | TABLE ACCESS FULL | SALGRADE | 1 | 5 | 5 |00:00:00.01 | 6 | | | | |* 4 | FILTER | | 5 | | 14 |00:00:00.01 | 6 | | | | |* 5 | SORT JOIN | | 5 | 14 | 40 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)| | 6 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | | ---------------------------------------------------------------------------------------------------------------------- =============================== 연결 조건이 월급이기 때문에 월급이 ascending하게 정렬된 것을 확인할 수 있다. 키 컬럼이 grade가 아니기 때문에 아래의 내 생각은 틀린듯 grade가 5등급 이기 때문에 salgrade를 선두 테이블로 두는 것이 좋다
    notion image

    튜닝44. OUTER JOIN은 이렇게 튜닝해야합니다.

    📖

    OUTER JOIN

    notion image
    아우터 조인의 실행계획은 조인되는 순서가 기본적으로 아우터 조인 SIGN이 없는 쪽에서 → 있는 쪽으로 조인을 유지합니다.
     
    ★아우터 조인 SIGN이 없는 쪽 → 있는 쪽으로 조인하고 있는것
     
     

    아우터 조인의 해쉬 조인시에 해쉬 테이블은?

    notion image
    notion image
    notion image
     
     
     
    예제1) 이름, 부서위치 를 출력하는데, 아우터 조인을 줄력하시오.
    @demo insert into emp(empno, ename, sal, deptno) VALUES(1123, 'JONES', 3000, 70); select e.ename, d.loc from emp e, dept d where e.deptno(+) = d.deptno; @p ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 15 |00:00:00.01 | 13 | | | | |* 1 | HASH JOIN OUTER | | 1 | 15 | 15 |00:00:00.01 | 13 | 1797K| 1797K| 1072K (0)| | 2 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | | | | 3 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 7 | | | | ---------------------------------------------------------------------------------------------------------------- select e.ename, d.loc from emp e, dept d where e.deptno = d.deptno(+); ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 15 |00:00:00.01 | 13 | | | | |* 1 | HASH JOIN OUTER | | 1 | 14 | 15 |00:00:00.01 | 13 | 1856K| 1856K| 1029K (0)| | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 15 |00:00:00.01 | 6 | | | | | 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | | | | ----------------------------------------------------------------------------------------------------------------
    notion image
     
    예제2) 아래의 SQL의 실행결과가 DEPT테이블이 해쉬 테이블이 되게 하시오
    (DEPT테이블이 더 작기 때문에 해쉬 테이블이 되어야함)
    select /*+ leaing(d,e) use_hash(e) */ e.ename, d.loc from emp e, dept d where e.deptno = d.deptno(+); ================================== 안바뀌고 emp테이브을 먼저 읽고 있음 따라서 swap_join_inputs 힌트를 사용해야함
    select /*+ leaing(d,e) use_hash(e) swap_join_inputs(d) */ e.ename, d.loc from emp e, dept d where e.deptno = d.deptno(+); ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 15 |00:00:00.01 | 13 | | | | |* 1 | HASH JOIN OUTER | | 1 | 14 | 15 |00:00:00.01 | 13 | 1856K| 1856K| 978K (0)| | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 15 |00:00:00.01 | 6 | | | | | 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | | | | ----------------------------------------------------------------------------------------------------------------
    notion image

    문제풀며 익히기

     
    Q1.
    아래의 SQL을 튜닝하시오 ! (해쉬조인으로 수행하세요. 조인 순서도 변경하세요)
    • 튜닝전
    select t.calendar_year, sum(s.amount_sold) from sales200 s, times200 t where s.time_id = t.time_id (+) and t.week_ending_day_id (+) = 1581 group by t.calendar_year;
    • 튜닝후(정답 학원에서 실행해 확인해보기)
    select /*+ leading(t, s) use_hash(s) swap_join_inputs(t) */ t.calendar_year, sum(s.amount_sold) from sales200 s, times200 t where s.time_id = t.time_id (+) and t.week_ending_day_id (+) = 1581 group by t.calendar_year;

    튜닝44. OUTER JOIN은 이렇게 튜닝해야합니다.

    📖

    OUTER JOIN

    아우터 조인의 실행계획은 조인되는 순서가 기본적으로 아우터 조인 SIGN이 없는 쪽에서 있는 쪽으로 조인을 유지합니다.
     
    ★아우터 조인 SIGN이 없는 쪽 → 있는 쪽으로 조인하고 있는것
     
    예제1) 이름, 부서위치 를 출력하는데, 아우터 조인을 줄력하시오.
    @demo insert into emp(empno, ename, sal, deptno) VALUES(1123, 'JONES', 3000, 70); select e.ename, d.loc from emp e, dept d where e.deptno(+) = d.deptno; @p ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 15 |00:00:00.01 | 13 | | | | |* 1 | HASH JOIN OUTER | | 1 | 15 | 15 |00:00:00.01 | 13 | 1797K| 1797K| 1072K (0)| | 2 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | | | | 3 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 7 | | | | ---------------------------------------------------------------------------------------------------------------- select e.ename, d.loc from emp e, dept d where e.deptno = d.deptno(+); ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 15 |00:00:00.01 | 13 | | | | |* 1 | HASH JOIN OUTER | | 1 | 14 | 15 |00:00:00.01 | 13 | 1856K| 1856K| 1029K (0)| | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 15 |00:00:00.01 | 6 | | | | | 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | | | | ----------------------------------------------------------------------------------------------------------------
    notion image
     
    예제2) 아래의 SQL의 실행결과가 DEPT테이블에 해쉬 테이블이 되게 하시오
    (DEPT테이블이 더 작기 때문에 해쉬 테이블이 되어야함)
    select /*+ leaing(d,e) use_hash(e) */ e.ename, d.loc from emp e, dept d where e.deptno = d.deptno(+); ================================== 안바뀌고 emp테이브을 먼저 읽고 있음 따라서 swap_join_inputs 힌트를 사용해야함
    select /*+ leaing(d,e) use_hash(e) swap_join_inputs(d) */ e.ename, d.loc from emp e, dept d where e.deptno = d.deptno(+); ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 15 |00:00:00.01 | 13 | | | | |* 1 | HASH JOIN OUTER | | 1 | 14 | 15 |00:00:00.01 | 13 | 1856K| 1856K| 978K (0)| | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 15 |00:00:00.01 | 6 | | | | | 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | | | | ----------------------------------------------------------------------------------------------------------------
    notion image
     

    튜닝45. DBeaver에서 실제 실행계획을 확인할 수 있어요.

    📖
    --직전 수행 SQL문 SELECT ename, job FROM emp WHERE job='SALESMAN'; --SQL_ID와, CHILD NUMBER 검색하는 SQL 문(EMP를 포함하는) select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; --실행계획 확인 문장 SELECT * FROM TABLE(dbms_xplan.display_cursor('SQL_ID를 넣으세요', CHILD NUMBER를 넣으세요 ,'ALLSTATS LAST'));

    문제풀며 익히기

     
    Q1.
    아래의 sql의 실제 실행계획을 DBeaver에서 확인하시오
    select ename, job, deptno from emp where deptno = 20; ENAME|JOB |DEPTNO| -----+-------+------+ JONES|MANAGER| 20| FORD |ANALYST| 20| SMITH|CLERK | 20| SCOTT|ANALYST| 20| ADAMS|CLERK | 20|
    select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('g0xqxvyrp2sw9',0,'ALLSTATS LAST')); |------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || |------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 6 || |* 1 | TABLE ACCESS FULL| EMP | 1 | 5 | 5 |00:00:00.01 | 6 || |------------------------------------------------------------------------------------|
    notion image
    notion image

    튜닝46. 3개 이상의 테이블의 nested loop join하는 방법을 알아야해요.

    📖

    ★3개이상의 테이블을 nested loop조인할 때는 이걸 알아야해요-그림설명

    --sql문 SELECT /*+ leading(s,e,d) use_nl(e) use_nl(d) */ e.ename, d.loc, s.grade FROM emp e, dept d, salgrade s WHERE d.deptno = d.deptno AND e.sal BETWEEN s.losal AND s.hisal; --sql_id, child number 확인하는 sql문 select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; --실행계획 확인하는 sql문 SELECT * FROM TABLE(dbms_xplan.display_cursor('7rdzznxwj9zfq',0,'ALLSTATS LAST')); --결과 확인 ------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 56 |00:00:00.01 | 132 || | 1 | NESTED LOOPS | | 1 | 170 | 56 |00:00:00.01 | 132 || | 2 | NESTED LOOPS | | 1 | 42 | 14 |00:00:00.01 | 45 || | 3 | TABLE ACCESS FULL| SALGRADE | 1 | 5 | 5 |00:00:00.01 | 10 || |* 4 | TABLE ACCESS FULL| EMP | 5 | 8 | 14 |00:00:00.01 | 35 || |* 5 | TABLE ACCESS FULL | DEPT | 14 | 4 | 56 |00:00:00.01 | 87 || ------------------------------------------------------------------------------------------|
    notion image
    notion image
    notion image
    조인 순서를 보면, 연결고리가 있는 테이블끼리 조인 순서를 묶어둔 것! 눈여겨보기.
    SALES랑 EMP조인할 때 NESTED LOOP조인하고, 그 결과를 가지고 DEPT와 NL조인 한 것. (INDEX가 없다는 가정이므로 모두 FULL TABLE SCAN)
     
    notion image
    SELECT /*+ leading(d,e,s) use_nl(e) use_nl(s) */ e.ename, d.loc, s.grade FROM emp e, dept d, salgrade s WHERE d.deptno = d.deptno AND e.sal BETWEEN s.losal AND s.hisal;
    notion image
     
    notion image
    soctt이 10건 있다고 가정하였을 때, emp테이블의 데이터는 10건이고 salgrade테이블의 데이터는 5건이니까 5건을 먹저 읽는게 맞는게 아닌가? NO emp쪽을 읽는게 맞음.
    왜? salgrade쪽에서 먼저 조인하면, scott이 아닌 모든 관련 없는 사원들도 join한 후 scott을 찾기 때문에, 처음부터 scott만 찾아서 하는게 맞음.
     
    이름이 SCOTT 인 데이터를 먼저 찾아서 SCOTT 관련한 데이터로 조인해서 dept 테이블, salgrade 테이블 순으로 조인해야합니다.
    그런데 만약 반대로 salgrade 를 driving 테이블로 둔다면  salgrade--> dept --> emp 순으로 조인하게 되는데 이렇게 되면 모든 사원을 다 찾아서 조인한다음에 나중에 scott 만 남겨두고 나머지 사원들은 다 버립니다.
    기껏 조인해놓았더니 다 버리게 되는 꼴이 되어버립니다.
     
    notion image
     
     
     
    notion image
    DALLAS는 1건이고, GRADE는 여러 건이라서. 1건인 것을 가지고 JOIN 하는게 나아보임. 3개의 테이블을 조인하면 조인 순서가 몇 개 나올 수 있을까요? 3! = 3 * 2 * 1 = 6개 6개에 대한 조인순서와 3개의 조인방법을 다 조합하면, 총 몇개의 실행계획이 나올 수 있을까요? 6 * 3 = 18개 옵티마이저가 실행계획 만들 때 아주 짧은 시간에 18개의 실행계획 중 가장 비용이 적게 드는 실행 계획을 생성하게 됨 단, 현장에서는 테이블 통계정보를 잘 수집하지 않기 때문에 위의 18개의 실행계획 중 가장 비용이 적게드는 실행계획을 옵티마이저가 잘 찾지 못함. ⇒ 따라서 1. 힌트를 사용하거나 2. 통계정보를 수집해서 옵티마이저를 똑똑하게 만들어줌 오라클에서는 계속해서 테이블 통계정보를 수집하라고 권고하고 있음.그런데, 공격적인 sql튜닝을 하는 회사는 수집을 하고 그렇지 않은 회사는 잘 수집하지 않음. 왜? sql느려질까봐 수집을 잘 하지 않음 그래도 통계정보를 수집해야함
     

    예제

     
    예제1)
     
     
    예제3) 검색 조건이 있었을 때의 조인 순서 정하기
    SELECT /*+ leading(d,e,s) use_nl(e) use_nl(s) */ e.ename, d.loc, s.grade FROM emp e, dept d, salgrade s WHERE d.deptno = d.deptno AND e.sal BETWEEN s.losal AND s.hisal AND e.ename = 'SCOTT'; ------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 54 || | 1 | NESTED LOOPS | | 1 | 12 | 4 |00:00:00.01 | 54 || | 2 | NESTED LOOPS | | 1 | 4 | 4 |00:00:00.01 | 30 || |* 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 || |* 4 | TABLE ACCESS FULL| EMP | 4 | 1 | 4 |00:00:00.01 | 24 || |* 5 | TABLE ACCESS FULL | SALGRADE | 4 | 3 | 4 |00:00:00.01 | 24 || ------------------------------------------------------------------------------------------| ======================================================================= 설명: 이름이 SCOTT인 데이터를 먼저 찾아서 SCOTT 관련한 데이터로 조인해서 DEPT테이블과 SALGRADE 테이블에가서 조인해야한다. 그런데 만약 반대로 SALGRADE를 DRIVING테이블로 둔다면, SALGRADE -> DEPT -> EMP 순으로 조인하게 되는데 이렇게 되면, 모든 사원을 다 찾아서 조인한 다음에 나중에 SCOTT만 남겨두고, 나머지 사언들은 다버리게 됨. 기껏 조인해놓았더니 다 버리게 되면 더 비효율적임. 따라서 우선순위를 확실하게 구해야함. 지금 당장 필요한것만! 욕심부려서 다 조인할 필요 없는 것.
    notion image
     
    예제4) 검색조건이 여러개 있을 때의 조인 순서
     
     

    문제풀며 익히기

     
    Q1.
    아래 2개의 조인 순서 중 버퍼의 개수가 더 작은 것은?
    1. EMP → DEPT → SALGRADE
    1. SALGRADE → DEPT → EMP
    # 1번. SELECT /*+ leading(e,d,s) use_nl(d) use_nl(s) */ e.ename, d.loc, s.grade FROM emp e, dept d, salgrade s WHERE d.deptno = d.deptno AND e.sal BETWEEN s.losal AND s.hisal AND e.ename = 'SCOTT'; ------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 39 | | 1 | NESTED LOOPS | | 1 | 12 | 4 |00:00:00.01 | 39 | | 2 | NESTED LOOPS | | 1 | 4 | 4 |00:00:00.01 | 14 | |* 3 | TABLE ACCESS FULL| EMP | 1 | 1 | 1 |00:00:00.01 | 7 | |* 4 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | |* 5 | TABLE ACCESS FULL | SALGRADE | 4 | 3 | 4 |00:00:00.01 | 25 | ------------------------------------------------------------------------------------------
    버퍼의 개수 : 39개
    notion image
    # 2번. SELECT /*+ leading(s,d,e) use_nl(d) use_nl(e) */ e.ename, d.loc, s.grade FROM emp e, dept d, salgrade s WHERE d.deptno = d.deptno AND e.sal BETWEEN s.losal AND s.hisal AND e.ename = 'SCOTT'; ------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 159 | | 1 | NESTED LOOPS | | 1 | 12 | 4 |00:00:00.01 | 159 | | 2 | NESTED LOOPS | | 1 | 20 | 20 |00:00:00.01 | 38 | | 3 | TABLE ACCESS FULL| SALGRADE | 1 | 5 | 5 |00:00:00.01 | 7 | |* 4 | TABLE ACCESS FULL| DEPT | 5 | 4 | 20 |00:00:00.01 | 31 | |* 5 | TABLE ACCESS FULL | EMP | 20 | 1 | 4 |00:00:00.01 | 121 | ------------------------------------------------------------------------------------------
    버퍼의 개수 : 159개
    salgrade와 dept는 서로 연결고리가 없는데 서로 조인했기 때문에 salgrade table 5개와 dept table 4개를 곱해서 20개의 row가 나온것(연결고리 없이 조인 한 것)
    notion image
     
    Q2. 아래의 sql을 튜닝하시오
    (단, 반드시 nl조인으로 조인하면서 조인관련 힌트를 사용하세요!)
    select e.ename, d.loc, s.grade from emp e, dept d, salgrade s where e.deptno = d.deptno and e.sal between s.losal and s.hisal and s.grade in (3,4) and d.loc = 'DALLAS';
    A. select /*+ leading(d, e, s) use_nl(e) use_nl(s) */e.ename, d.loc, s.grade from emp e, dept d, salgrade s where e.deptno = d.deptno and e.sal between s.losal and s.hisal and s.grade in (3,4) and d.loc = 'DALLAS'; ------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 45 | | 1 | NESTED LOOPS | | 1 | 6 | 3 |00:00:00.01 | 45 | | 2 | NESTED LOOPS | | 1 | 5 | 5 |00:00:00.01 | 14 | |* 3 | TABLE ACCESS FULL| DEPT | 1 | 1 | 1 |00:00:00.01 | 7 | |* 4 | TABLE ACCESS FULL| EMP | 1 | 5 | 5 |00:00:00.01 | 7 | |* 5 | TABLE ACCESS FULL | SALGRADE | 5 | 1 | 3 |00:00:00.01 | 31 | ------------------------------------------------------------------------------------------
    notion image
    A2. select /*+ leading(d, s, e) use_nl(s) use_nl(e) */e.ename, d.loc, s.grade from emp e, dept d, salgrade s where e.deptno = d.deptno and e.sal between s.losal and s.hisal and s.grade in (3,4) and d.loc = 'DALLAS'; ------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 27 | | 1 | NESTED LOOPS | | 1 | 6 | 3 |00:00:00.01 | 27 | | 2 | NESTED LOOPS | | 1 | 2 | 2 |00:00:00.01 | 14 | |* 3 | TABLE ACCESS FULL| DEPT | 1 | 1 | 1 |00:00:00.01 | 7 | |* 4 | TABLE ACCESS FULL| SALGRADE | 1 | 2 | 2 |00:00:00.01 | 7 | |* 5 | TABLE ACCESS FULL | EMP | 2 | 3 | 3 |00:00:00.01 | 13 | ------------------------------------------------------------------------------------------ =================================== dept와 salgrade가 서로 연결고리가 없지만 더 나은 경우임
    notion image
     
    Q3.
    아래의 sql의 조인 속도를 높이기 위해서 필요한 인덱스를 생성하시오
    ▣ 3개 이상의 테이블 nested loop조인 할때 인덱스 사용이 중요해요
    notion image
    (반드시 전부 nested loop 조인으로 조인하면서 조인관련 힌트를 사용하시오!)
    • 조인순서1: dept → emp → salgrade
    • 조인순서2: dept → salgrade → emp
    (index가 있어야 nested loop조인이 빛을 발할 수 있음) select e.ename, d.loc, s.grade from emp e, dept d, salgrade s where e.deptno = d.deptno and e.sal between s.losal and s.hisal and s.grade in (3,4) and d.loc = 'DALLAS';
    < 조인 1: e.deptno에 index걸기 > create index emp_deptno on emp(deptno); select /*+ leading(d,e,s) use_nl(e) use_nl(s) */e.ename, d.loc, s.grade from emp e, dept d, salgrade s where e.deptno = d.deptno and e.sal between s.losal and s.hisal and s.grade in (3,4) and d.loc = 'DALLAS'; -------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 42 | | 1 | NESTED LOOPS | | 1 | 6 | 3 |00:00:00.01 | 42 | | 2 | NESTED LOOPS | | 1 | 5 | 5 |00:00:00.01 | 11 | |* 3 | TABLE ACCESS FULL | DEPT | 1 | 1 | 1 |00:00:00.01 | 7 | | 4 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 5 | 5 |00:00:00.01 | 4 | |* 5 | INDEX RANGE SCAN | EMP_DEPTNO | 1 | 5 | 5 |00:00:00.01 | 2 | |* 6 | TABLE ACCESS FULL | SALGRADE | 5 | 1 | 3 |00:00:00.01 | 31 | -------------------------------------------------------------------------------------------------------------- 버퍼의 개수 42개 // d.loc에도 index걸기 create index dept_loc on dept(loc); select /*+ leading(d,e,s) use_nl(e) use_nl(s) */e.ename, d.loc, s.grade from emp e, dept d, salgrade s where e.deptno = d.deptno and e.sal between s.losal and s.hisal and s.grade in (3,4) and d.loc = 'DALLAS'; -------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 38 | | 1 | NESTED LOOPS | | 1 | 6 | 3 |00:00:00.01 | 38 | | 2 | NESTED LOOPS | | 1 | 5 | 5 |00:00:00.01 | 7 | | 3 | TABLE ACCESS BY INDEX ROWID BATCHED| DEPT | 1 | 1 | 1 |00:00:00.01 | 3 | |* 4 | INDEX RANGE SCAN | DEPT_LOC | 1 | 1 | 1 |00:00:00.01 | 2 | | 5 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 5 | 5 |00:00:00.01 | 4 | |* 6 | INDEX RANGE SCAN | EMP_DEPTNO | 1 | 5 | 5 |00:00:00.01 | 2 | |* 7 | TABLE ACCESS FULL | SALGRADE | 5 | 1 | 3 |00:00:00.01 | 31 | -------------------------------------------------------------------------------------------------------------- 버퍼의 개수 : 38 즉, nested loop로 한다면 index를 e.deptno와 d.loc에 걸어줘야함.
    notion image
    notion image
    < 조인2 : dept -> salgrade -> emp = dept와 salgrade는 연결고리가 없으므로 카티션 조인함 > select /*+ leading(d,s,e) use_nl(s) use_nl(e) */e.ename, d.loc, s.grade from emp e, dept d, salgrade s where e.deptno = d.deptno and e.sal between s.losal and s.hisal and s.grade in (3,4) and d.loc = 'DALLAS'; --------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 15 | | 1 | NESTED LOOPS | | 1 | 6 | 3 |00:00:00.01 | 15 | | 2 | NESTED LOOPS | | 1 | 10 | 10 |00:00:00.01 | 13 | | 3 | NESTED LOOPS | | 1 | 2 | 2 |00:00:00.01 | 10 | | 4 | TABLE ACCESS BY INDEX ROWID BATCHED| DEPT | 1 | 1 | 1 |00:00:00.01 | 3 | |* 5 | INDEX RANGE SCAN | DEPT_LOC | 1 | 1 | 1 |00:00:00.01 | 2 | |* 6 | TABLE ACCESS FULL | SALGRADE | 1 | 2 | 2 |00:00:00.01 | 7 | |* 7 | INDEX RANGE SCAN | EMP_DEPTNO | 2 | 5 | 10 |00:00:00.01 | 3 | |* 8 | TABLE ACCESS BY INDEX ROWID | EMP | 10 | 3 | 3 |00:00:00.01 | 2 | ---------------------------------------------------------------------------------------------------------------
    notion image
    2건만 가지고 nl조인을 하러 갔으니까 버퍼의 개수가 작을 수 밖에 없음
    2건만 가지고 nl조인을 하러 갔으니까 버퍼의 개수가 작을 수 밖에 없음

    튜닝47. 3개의 테이블을 조인할 때 조인 방법을 다양하게 조절할 줄 알아야해요.

    📖

    JOIN 방법 섞어서 사용하기

    조인 별 테이블 명칭

    실행계획 상
    NL JOIN
    HASH JOIN
    SORT MERGE JOIN
    위쪽 테이블
    driving table
    hash table
    선행 테이블
    아래쪽 테이블
    driven table
    probe table
    후행 테이블
     
    • 조인 순서 : emp <hash join> dept <NL join> salgrade
    @demo select /*+ leading(e,d,s) use_hash(d) use_nl(s) */ e.ename, d.loc, s.grade from emp e, dept d, salgrade s where e.deptno = d.deptno and e.sal between s.losal and s.hisal; -- sql_id확인, child_number확인 select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; -- 실행계획 확인 SELECT * FROM TABLE(dbms_xplan.display_cursor('0asmbwg2m6v4f',0,'ALLSTATS LAST')); ---------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ---------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 98 | | | || | 1 | NESTED LOOPS | | 1 | 42 | 14 |00:00:00.01 | 98 | | | || |* 2 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 13 | 1651K| 1651K| 996K (0)|| | 3 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || | 4 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 7 | | | || |* 5 | TABLE ACCESS FULL | SALGRADE | 14 | 3 | 14 |00:00:00.01 | 85 | | | || ---------------------------------------------------------------------------------------------------------------------|
    notion image

    문제풀며 익히기

     
    Q1.
    아래와 같이 조인순서와 조인방법이 결정되도록 힌트를 주시오
    • 조인 순서: salgrade <sort merge join> emp <NL join> dept
    select /*+ leading(s,e,d) use_merge(e) use_nl(d) */ e.ename, d.loc, s.grade from emp e, dept d, salgrade s where e.deptno = d.deptno and e.sal between s.losal and s.hisal; -------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 97 | | 1 | NESTED LOOPS | | 1 | 42 | 14 |00:00:00.01 | 97 | | 2 | MERGE JOIN | | 1 | 42 | 14 |00:00:00.01 | 12 | | 3 | SORT JOIN | | 1 | 5 | 5 |00:00:00.01 | 6 | | 4 | TABLE ACCESS FULL | SALGRADE | 1 | 5 | 5 |00:00:00.01 | 6 | |* 5 | FILTER | | 5 | | 14 |00:00:00.01 | 6 | |* 6 | SORT JOIN | | 5 | 14 | 40 |00:00:00.01 | 6 | | 7 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | |* 8 | TABLE ACCESS FULL | DEPT | 14 | 1 | 14 |00:00:00.01 | 85 | --------------------------------------------------------------------------------------------
    notion image
     

    튜닝48. 선택적으로 조인을 하면서 조인의 성능을 높일 수 있어요. -★현장에서도 많이 사용됨(sqlp에서도 자주 언급)★

    📖

    선택적 조인 (상호 배타적 조인)

    decode 를 사용한 선택적 조인
     
    선택적 조인 환경구성
    -- 환경구성 /* emp테이블에 emp_kind라는 문자형 컬럼을 추가할것임. 추가 후 emp_kind의 값을 update해줌 */ @demo alter table emp add emp_kind varchar2(1) default 1 not null; update emp set emp_kind = case when mod(empno,2) = 1 then 1 else 2 end ; select ename, emp_kind from emp; # emp_kind 가 1이면 정규직 , emp_kind 가 2이면 비정규직 /* ENAME EM -------------------- -- KING 1 BLAKE 2 CLARK 2 JONES 2 MARTIN 2 ALLEN 1 TURNER 2 JAMES 2 WARD 1 FORD 2 SMITH 1 SCOTT 2 ADAMS 2 MILLER 2 */ -- 정규직 사원 테이블 생성 create table emp_kind1 as select empno, ename, sal + 200 as office_sal from emp where emp_kind ='1'; -- 비정규직 사원 테이블 생성 create table emp_kind2 as select empno, ename, sal + 200 as sal from emp where emp_kind ='2'; -- primary key 제약을 각각 건다 alter table emp_kind1 add constraint pk_emp_kind1 primary key(empno); alter table emp_kind2 add constraint pk_emp_kind2 primary key(empno); alter table emp drop column sal ; /* SQL> select * from emp_kind1; EMPNO ENAME OFFICE_SAL ---------- -------------------- ---------- 7839 KING 5200 7499 ALLEN 1800 7521 WARD 1450 7369 SMITH 1000 SQL> select * from emp_kind2; EMPNO ENAME SAL ---------- -------------------- ---------- 7698 BLAKE 3050 7782 CLARK 2650 7566 JONES 3175 7654 MARTIN 1450 7844 TURNER 1700 7900 JAMES 1150 7902 FORD 3200 7788 SCOTT 3200 7876 ADAMS 1300 7934 MILLER 1500 */
    notion image
     
    예제1)
    emp와 emp_kind1과 emp_kind2 테이블을 조인해서 사원번호가 7839인 사원번호, 사원이름, 정규직 사원 월급, 비정규직 사원 월급을 출력하시오.
    select e.empno, e.ename, k1.office_sal, k2.sal from emp e, emp_kind1 k1, emp_kind2 k2 where e.empno = k1.empno(+) and e.empno = k2.empno(+) and e.empno = 7839; EMPNO ENAME OFFICE_SAL SAL ---------- -------------------- ---------- ---------- 7839 KING 5200 ========================================================= 사원번호가 무엇이 들어올지 모르기 때문에 k1.empno와 k2.empno 모두에 아우터 조인 사인을 넣어주어야함.
    notion image
    select e.empno, e.ename, k1.office_sal, k2.sal from emp e, emp_kind1 k1, emp_kind2 k2 where e.empno = k1.empno and e.empno = k2.empno and e.empno = 7839; 선택된 레코드가 없습니다. ============================================================= 7839는 정규직 사원 테이블에만 있는 사원이기 때문에, emp_kind2와 equi조인을 할 수 없기 때문에 이런 결과가 도출된다.
    notion image
     
    예제2)
    정규직 사원인 7839사원의 데이터를 볼 때, emp테이블이 emp_kind1테이블과 조인을 할때는 버퍼를 몇 개를 읽을 수 있는 것인데, emp_kind2와도 조인할 때 버퍼를 읽었을 까? ⇒ 실제 실행계획을 보면 알 수 있음_
    select e.empno, e.ename, k1.office_sal, k2.sal from emp e, emp_kind1 k1, emp_kind2 k2 where e.empno = k1.empno(+) and e.empno = k2.empno(+) and e.empno = 7839; EMPNO|ENAME|OFFICE_SAL|SAL| -----+-----+----------+---+ 7839|KING | 5200| | | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 9 | | 1 | MERGE JOIN OUTER | | 1 | 1 | 1 |00:00:00.01 | 9 | | 2 | MERGE JOIN OUTER | | 1 | 1 | 1 |00:00:00.01 | 8 | |* 3 | TABLE ACCESS FULL | EMP | 1 | 1 | 1 |00:00:00.01 | 6 | | 4 | BUFFER SORT | | 1 | 1 | 1 |00:00:00.01 | 2 | | 5 | TABLE ACCESS BY INDEX ROWID| EMP_KIND1 | 1 | 1 | 1 |00:00:00.01 | 2 | |* 6 | INDEX UNIQUE SCAN | PK_EMP_KIND1 | 1 | 1 | 1 |00:00:00.01 | 1 | | 7 | BUFFER SORT | | 1 | 1 | 0 |00:00:00.01 | 1 | | 8 | TABLE ACCESS BY INDEX ROWID | EMP_KIND2 | 1 | 1 | 0 |00:00:00.01 | 1 | |* 9 | INDEX UNIQUE SCAN | PK_EMP_KIND2 | 1 | 1 | 0 |00:00:00.01 | 1 | ---------------------------------------------------------------------------------------------------------
    notion image
    비정규직 사원 테이블과 조인할 때 버퍼를 1개 읽었음을 알 수 있음 PK 제약조건을 걸었기 때문에 UNIQUE SCAN을 함 예제3) 아래의 비정규직 사원인 7902사원의 데이터를 볼 때도 불필요하게 정규직 사원 테이블의 데이터를 조인해서 버퍼 1개를 읽었는지 확인하시오
    select e.empno, e.ename, k1.office_sal, k2.sal from emp e, emp_kind1 k1, emp_kind2 k2 where e.empno = k1.empno(+) and e.empno = k2.empno(+) and e.empno = 7902; -------------------------------------------------------------------------------------------------------- Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------------------------- 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 9 | 1 | MERGE JOIN OUTER | | 1 | 1 | 1 |00:00:00.01 | 9 | 2 | MERGE JOIN OUTER | | 1 | 1 | 1 |00:00:00.01 | 7 | * 3 | TABLE ACCESS FULL | EMP | 1 | 1 | 1 |00:00:00.01 | 6 | 4 | BUFFER SORT | | 1 | 1 | 0 |00:00:00.01 | 1 | 5 | TABLE ACCESS BY INDEX ROWID| EMP_KIND1 | 1 | 1 | 0 |00:00:00.01 | 1 | * 6 | INDEX UNIQUE SCAN | PK_EMP_KIND1 | 1 | 1 | 0 |00:00:00.01 | 1 | 7 | BUFFER SORT | | 1 | 1 | 1 |00:00:00.01 | 2 | 8 | TABLE ACCESS BY INDEX ROWID | EMP_KIND2 | 1 | 1 | 1 |00:00:00.01 | 2 | * 9 | INDEX UNIQUE SCAN | PK_EMP_KIND2 | 1 | 1 | 1 |00:00:00.01 | 1 | --------------------------------------------------------------------------------------------------------
    notion image
     
    notion image
    notion image
    예제 4) 위의 sql을 튜닝하시오
    사원번호가 정규직 사원번호가 입력될때는 정규직 사원테이블과 조인되게 하고, 비정규직 테이블과는 조인되지 않게 하고, 사원번호가 비정규직 사원번호가 입력될 때에는 비정규직 사원 테이블과 조인되게하고, 정규직 사원 테이블과는 조인되지 않게 sql을 고치시오(선택적 조인)
    select e.empno, e.ename, k1.office_sal, k2.sal from emp e, emp_kind1 k1, emp_kind2 k2 where decode(e.emp_kind, 1, e.empno) = k1.empno(+) and decode(e.emp_kind, 2, e.empno) = k2.empno(+) and e.empno = 7902;
    notion image
    비정규직이니까, 정규직 사원테이블과 조인하는 부분에는 buffer = 0 나옴!!!
     
    위의 조인 튜닝 방법을 선택적 조인 or 상호 배타적 관계의 조인이라고 부른다.
     

    문제풀며 익히기

     
    Q1.
    아래의 환경을 생성하세요!
    @demo drop table dept_10; drop table dept_20; drop table dept_30; create table dept_10 as select empno, ename, sal*0.1 bonus_10 from emp where deptno = 10; create table dept_20 as select empno, ename, sal*0.2 bonus_20 from emp where deptno = 20; create table dept_30 as select empno, ename, sal*0.3 bonus_30 from emp where deptno = 30; ------------------------------- SQL> select * from dept_30; EMPNO ENAME BONUS_30 ---------- -------------------- ---------- 7698 BLAKE 855 7654 MARTIN 375 7499 ALLEN 480 7844 TURNER 450 7900 JAMES 285 7521 WARD 375
    notion image
     
    Q2.
    emp와 dept_10, dept_20, dept_30을 조인해서 , 사원번호가 7788번인 사원의 사원번호, 사원이름, bonus_10, bonus_20, bonus_30을 출력하시오.
    select e.empno, e.ename, d10.bonus_10, d20.bonus_20, d30.bonus_30 from emp e, dept_10 d10, dept_20 d20, dept_30 d30 where e.empno = d10.empno(+) and e.empno = d20.empno(+) and e.empno = d30.empno(+) and e.empno = 7788; EMPNO ENAME BONUS_10 BONUS_20 BONUS_30 ---------- -------------------- ---------- ---------- ---------- 7788 SCOTT 600
    notion image
     
    Q3.
    위의 sql을 선택적 조인을 할 수 있도록 구성하시오
    select e.empno, e.ename, d10.bonus_10, d20.bonus_20, d30.bonus_30 from emp e, dept_10 d10, dept_20 d20, dept_30 d30 where decode(e.deptno, 10, e.empno ) = d10.empno(+) and decode(e.deptno, 20, e.empno) = d20.empno(+) and decode(e.deptno, 30, e.empno ) = d30.empno(+) and e.empno = 7788; --------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | --------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 12 | | | | |* 1 | HASH JOIN OUTER | | 1 | 1 | 1 |00:00:00.01 | 12 | 1476K| 1476K| 464K (0)| |* 2 | HASH JOIN OUTER | | 1 | 1 | 1 |00:00:00.01 | 10 | 1399K| 1399K| 662K (0)| |* 3 | HASH JOIN OUTER | | 1 | 1 | 1 |00:00:00.01 | 8 | 1538K| 1538K| 728K (0)| |* 4 | TABLE ACCESS FULL| EMP | 1 | 1 | 1 |00:00:00.01 | 6 | | | | | 5 | TABLE ACCESS FULL| DEPT_10 | 1 | 3 | 3 |00:00:00.01 | 2 | | | | | 6 | TABLE ACCESS FULL | DEPT_20 | 1 | 5 | 5 |00:00:00.01 | 2 | | | | | 7 | TABLE ACCESS FULL | DEPT_30 | 1 | 6 | 6 |00:00:00.01 | 2 | | | | --------------------------------------------------------------------------------------------------------------------- ================= INDEX가 걸려있지 않기 때문에 BUFFER를 읽고 있는 것.
     
    Q4.
    선택적 조인을 할 때 BUFFER의 개수가 필요한 테이블만 읽을 수 있도록 DEPT_10과 DEPT_20과 DEPT_30의 EMPNO에 PK제약을 각각 거시오
    alter table dept_10 add constraint dept_10_pk primary key(empno); alter table dept_20 add constraint dept_20_pk primary key(empno); alter table dept_30 add constraint dept_30_pk primary key(empno); --인덱스가 걸려있어야 선택적 조인이 가능해짐 select e.empno, e.ename, d10.bonus_10, d20.bonus_20, d30.bonus_30 from emp e, dept_10 d10, dept_20 d20, dept_30 d30 where decode(e.deptno, 10, e.empno ) = d10.empno(+) and decode(e.deptno, 20, e.empno) = d20.empno(+) and decode(e.deptno, 30, e.empno ) = d30.empno(+) and e.empno = 7788; @p ------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 8 | | 1 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 8 | | 2 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 8 | | 3 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 6 | |* 4 | TABLE ACCESS FULL | EMP | 1 | 1 | 1 |00:00:00.01 | 6 | | 5 | TABLE ACCESS BY INDEX ROWID| DEPT_10 | 1 | 1 | 0 |00:00:00.01 | 0 | |* 6 | INDEX UNIQUE SCAN | DEPT_10_PK | 1 | 1 | 0 |00:00:00.01 | 0 | | 7 | TABLE ACCESS BY INDEX ROWID | DEPT_20 | 1 | 1 | 1 |00:00:00.01 | 2 | |* 8 | INDEX UNIQUE SCAN | DEPT_20_PK | 1 | 1 | 1 |00:00:00.01 | 1 | | 9 | TABLE ACCESS BY INDEX ROWID | DEPT_30 | 1 | 1 | 0 |00:00:00.01 | 0 | |* 10 | INDEX UNIQUE SCAN | DEPT_30_PK | 1 | 1 | 0 |00:00:00.01 | 0 | -------------------------------------------------------------------------------------------------------
    notion image
     
    notion image

    튜닝49. 인라인 뷰와 조인할 때는 인라인 뷰가 해체되지 않게 하세요.

    📖

    인라인뷰(from절의 서브쿼리)와 조인

    인라인뷰에 의해 결과가 줄어들어 조인 되는 결과가 작아지면, 인라인뷰를 해체하지 않는게 좋음.
     

    관련 힌트

    1. NO_MERGE : 인라인 뷰나 뷰를 해체하지 말아라!
    1. MERGE : 인라인 뷰나 뷰를 해체해라!
    @demo select v.ename, v.loc, s.grade from salgrade s, (select e.ename, e.sal, d.loc from emp e, dept d where e.deptno = d.deptno) v where v.sal between s.losal and s.hisal; ENAME LOC GRADE -------------------- -------------------------- ---------- KING NEW YORK 5 SCOTT DALLAS 4 FORD DALLAS 4 JONES DALLAS 4 BLAKE CHICAGO 4 CLARK NEW YORK 4 ALLEN CHICAGO 3 TURNER CHICAGO 3 MILLER NEW YORK 2 WARD CHICAGO 2 MARTIN CHICAGO 2 ADAMS DALLAS 1 JAMES CHICAGO 1 SMITH DALLAS 1
    sqlgrade결과는 5건 인라인뷰의 결과는 14건이기 때문에 조인순서 : salgrade -> in line view의 결과 순이어야 한다. 옵티마이저가 그렇게 수행했는지 실행계획을 확인해보기 ============================================ @p ------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 18 | | 1 | MERGE JOIN | | 1 | 42 | 14 |00:00:00.01 | 18 | | 2 | SORT JOIN | | 1 | 14 | 14 |00:00:00.01 | 12 | |* 3 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 12 | | 4 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | 5 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | |* 6 | FILTER | | 14 | | 14 |00:00:00.01 | 6 | |* 7 | SORT JOIN | | 14 | 5 | 40 |00:00:00.01 | 6 | | 8 | TABLE ACCESS FULL| SALGRADE | 1 | 5 | 5 |00:00:00.01 | 6 | -------------------------------------------------------------------------------------------
    notion image
    인라인 뷰를 해체하고, dept → emp → salgrade 순으로 갔다. 즉, 실행계획에 view가 없으면 in line view를 해체된것. 해체되고 query transformer가 아래의 sql을 변경해서 수행한 것
    !! 옵티마이저가 아래와 같이 sql을 멋대로 바꿔버림 !! select e.ename, d.loc, s.grade from emp e, dept d, salgrade s where e.deptno = d.deptno and e.sal between s.losal and s.hisal;
     

    예제 - 위와 연결됨(힌트 사용)

     
    예제1) Query transformer가 in line view를 해체하지 못하게 하시오.
    A1. select /*+ no_merge(v) */ v.ename, v.loc, s.grade from salgrade s, (select e.ename, e.sal, d.loc from emp e, dept d where e.deptno = d.deptno) v where v.sal between s.losal and s.hisal; 혹은 A2. select v.ename, v.loc, s.grade from salgrade s, (select /*+ no_merge */e.ename, e.sal, d.loc from emp e, dept d where e.deptno = d.deptno) v where v.sal between s.losal and s.hisal; ---------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 18 | | 1 | MERGE JOIN | | 1 | 42 | 14 |00:00:00.01 | 18 | | 2 | SORT JOIN | | 1 | 5 | 5 |00:00:00.01 | 6 | | 3 | TABLE ACCESS FULL | SALGRADE | 1 | 5 | 5 |00:00:00.01 | 6 | |* 4 | FILTER | | 5 | | 14 |00:00:00.01 | 12 | |* 5 | SORT JOIN | | 5 | 14 | 40 |00:00:00.01 | 12 | | 6 | VIEW | | 1 | 14 | 14 |00:00:00.01 | 12 | |* 7 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 12 | | 8 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | 9 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | ----------------------------------------------------------------------------------------------
    notion image
     
     

    문제풀며 익히기

     
    Q1. 위의 SQL의 실행계획이 아래와 같이 출력되게 하시오.
    A1. select /*+ leading(v,s) use_nl(s) */ v.ename, v.loc, s.grade from salgrade s, (select /*+ no_merge */e.ename, e.sal, d.loc from emp e, dept d where e.deptno = d.deptno) v where v.sal between s.losal and s.hisal; A2. select /*+ no_merge(v) leading(v,s) use_nl(s) */ v.ename, v.loc, s.grade from salgrade s, (select e.ename, e.sal, d.loc from emp e, dept d where e.deptno = d.deptno) v where v.sal between s.losal and s.hisal; ------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 98 | | 1 | NESTED LOOPS | | 1 | 42 | 14 |00:00:00.01 | 98 | | 2 | VIEW | | 1 | 14 | 14 |00:00:00.01 | 13 | |* 3 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 13 | | 4 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | 5 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 7 | |* 6 | TABLE ACCESS FULL | SALGRADE | 14 | 3 | 14 |00:00:00.01 | 85 | ------------------------------------------------------------------------------------------- ========================================= 인라인뷰가 DRIVING TABLE이며, 실행계획에 VIEW가 있으므로 인라인뷰가 해체되지 않음 즉, 인라인뷰를 해체하지 않게 해야 leading(v,s) use_nl(s)가 의미 있는 것. 만약 인라인뷰를 해체했다면, 힌트는 전부 무시되임
    notion image
     

    튜닝50. VIEW도 해체하거나 해체하지 않게 할 수 있어요.

    📖

    VIEW해체 OR 해체하지 않기

    VIEW해체 관련 힌트

    1. NO_MERGE : 인라인 뷰나 뷰를 해체하지 말아라!
    1. MERGE : 인라인 뷰나 뷰를 해체해라!
     
    실습환경 구성하기 -view만들기
    -- 환경 만들기 -- @demo create index emp_sal_idx on emp(sal); -- 통계정보 수집 -- exec dbms_stats.gather_table_stats(user, 'dept'); exec dbms_stats.gather_table_stats(user, 'emp'); -- view 만들기 -- create or replace view emp_salesman as select empno, ename, job, mgr, hiredate, sal, comm, deptno from emp where job = 'SALESMAN' ; --확인하기-- SQL> select * from emp_salesman; EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO ---------- -------------------- ------------------ ---------- -------- ---------- ---------- ---------- 7654 MARTIN SALESMAN 7698 81/09/10 1250 1400 30 7499 ALLEN SALESMAN 7698 81/02/11 1600 300 30 7844 TURNER SALESMAN 7698 81/08/21 1500 0 30 7521 WARD SALESMAN 7698 81/02/23 1250 500 30
     
    예제1) 아래의 sql을 튜닝하시오
    < 튜닝전 : 옵티마이저가 뷰를 해체하지 않은 경우 > select /*+ no_merge(v) leading(v,d) use_nl(d) */ v.empno, v.ename, v.job, v.mgr, v.sal, d.dname from emp_salesman v, dept d where v.deptno = d.deptno and v.sal >= 1500; EMPNO|ENAME |JOB |MGR |SAL |DNAME| -----+------+--------+----+----+-----+ 7844|TURNER|SALESMAN|7698|1500|SALES| 7499|ALLEN |SALESMAN|7698|1600|SALES| @p ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 2 |00:00:00.01 | 14 | | 1 | NESTED LOOPS | | 1 | 3 | 2 |00:00:00.01 | 14 | | 2 | VIEW | EMP_SALESMAN | 1 | 3 | 2 |00:00:00.01 | 2 | |* 3 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 3 | 2 |00:00:00.01 | 2 | |* 4 | INDEX RANGE SCAN | EMP_SAL_IDX | 1 | 13 | 8 |00:00:00.01 | 1 | |* 5 | TABLE ACCESS FULL | DEPT | 2 | 1 | 2 |00:00:00.01 | 12 | ----------------------------------------------------------------------------------------------------------------
    notion image
    < 튜닝 후 : view 해체 후 옵티마이저가 자유롭게 실행계획 세우도록 함 > select /*+ merge(v) */ v.empno, v.ename, v.job, v.mgr, v.sal, d.dname from emp_salesman v, dept d where v.deptno = d.deptno and v.sal >= 1500; -------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 2 |00:00:00.01 | 8 | |* 1 | HASH JOIN | | 1 | 3 | 2 |00:00:00.01 | 8 | |* 2 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 3 | 2 |00:00:00.01 | 2 | |* 3 | INDEX RANGE SCAN | EMP_SAL_IDX | 1 | 13 | 8 |00:00:00.01 | 1 | | 4 | TABLE ACCESS FULL | DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | --------------------------------------------------------------------------------------------------------------
    notion image
    실행계획에 view가 없기 때문에, view가 해체된 것을 알 수 있다. 즉, view를 해체하면서 query transformer가 아래와 같이 sql을 재작성했음
    select e.empno, e.ename, e.job, e.mgr, e.sal, d.dname from emp e, dept d where e.deptno = d.deptno and e.sal >= 1500; view를 해체해버리면서 월급이 1500이상인 사원들의 데이터를 모두 가져온 후 조인하고 있음

    문제풀며 익히기

     
    Q1. 아래의 view를 해체하지 않고 join되게 하시오.
    @demo --환경 구성-- drop table dept_10; create or replace view dept_10 as select deptno, dname, loc from dept where deptno = 10;
    < view 해체 > select e.empno, e.ename, v.loc from emp e, dept_10 v where e.deptno = v.deptno and e.sal > 1000; EMPNO|ENAME |LOC | -----+------+--------+ 7839|KING |NEW YORK| 7782|CLARK |NEW YORK| 7934|MILLER|NEW YORK| -------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 13 | |* 1 | HASH JOIN | | 1 | 4 | 3 |00:00:00.01 | 13 | |* 2 | TABLE ACCESS FULL| DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | |* 3 | TABLE ACCESS FULL| EMP | 1 | 4 | 3 |00:00:00.01 | 6 | --------------------------------------------------------------------------------------
    < view 해체 안됐을 > select /*+ no_merge(v) */ e.empno, e.ename, v.loc from emp e, dept_10 v where e.deptno = v.deptno and e.sal > 1000; ------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 13 | |* 1 | HASH JOIN | | 1 | 4 | 3 |00:00:00.01 | 13 | | 2 | VIEW | DEPT_10 | 1 | 1 | 1 |00:00:00.01 | 6 | |* 3 | TABLE ACCESS FULL| DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | |* 4 | TABLE ACCESS FULL | EMP | 1 | 13 | 12 |00:00:00.01 | 6 | -------------------------------------------------------------------------------------------
     

    튜닝51. VIEW 안의 조인 순서도 변경할 수 있어요. ★

    📖
    환경 구성
    @demo create or replace view emp_dept as select e.ename, e.sal, d.loc from emp e, dept d where e.deptno = d.deptno;
     
     
    예제1) emp_dept뷰와 sqlgrade 를 조인하는데, 뷰를 해체하지 못하게 하시오
    select /*+ no_merge(v) */ v.ename, v.sal, v.loc, s.grade from emp_dept v, salgrade s where v.sal between s.losal and s.hisal; ENAME |SAL |LOC |GRADE| ------+----+--------+-----+ SMITH | 800|DALLAS | 1| JAMES | 950|CHICAGO | 1| ADAMS |1100|DALLAS | 1| WARD |1250|CHICAGO | 2| MARTIN|1250|CHICAGO | 2| MILLER|1300|NEW YORK| 2| TURNER|1500|CHICAGO | 3| ALLEN |1600|CHICAGO | 3| CLARK |2450|NEW YORK| 4| BLAKE |2850|CHICAGO | 4| JONES |2975|DALLAS | 4| FORD |3000|DALLAS | 4| SCOTT |3000|DALLAS | 4| KING |5000|NEW YORK| 5| --------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 18 | | 1 | MERGE JOIN | | 1 | 42 | 14 |00:00:00.01 | 18 | | 2 | SORT JOIN | | 1 | 5 | 5 |00:00:00.01 | 6 | | 3 | TABLE ACCESS FULL | SALGRADE | 1 | 5 | 5 |00:00:00.01 | 6 | |* 4 | FILTER | | 5 | | 14 |00:00:00.01 | 12 | |* 5 | SORT JOIN | | 5 | 14 | 40 |00:00:00.01 | 12 | | 6 | VIEW | EMP_DEPT | 1 | 14 | 14 |00:00:00.01 | 12 | |* 7 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 12 | | 8 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | 9 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | ---------------------------------------------------------------------------------------------
    notion image
     
    예제2)
    위의 상태에서 emp_dept 뷰와 salgrade의 조인 방법을 nested loop조인으로 수행되게 하시오
    • 조인순서 : salgrade를 읽고 → emp_dept와 nl조인
    select /*+ no_merge(v) leading(s,v) use_nl(v) */ v.ename, v.sal, v.loc, s.grade from emp_dept v, salgrade s where v.sal between s.losal and s.hisal; -------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 68 | | 1 | NESTED LOOPS | | 1 | 42 | 14 |00:00:00.01 | 68 | | 2 | TABLE ACCESS FULL | SALGRADE | 1 | 5 | 5 |00:00:00.01 | 7 | |* 3 | VIEW | EMP_DEPT | 5 | 8 | 14 |00:00:00.01 | 61 | |* 4 | HASH JOIN | | 5 | 14 | 70 |00:00:00.01 | 61 | | 5 | TABLE ACCESS FULL| DEPT | 5 | 4 | 20 |00:00:00.01 | 30 | | 6 | TABLE ACCESS FULL| EMP | 5 | 14 | 70 |00:00:00.01 | 31 | --------------------------------------------------------------------------------------------
    notion image
     
    예제3) 뷰 안의 조인 순서 변경
    위의 실행계획을 다시 출력하는데, view안의 조인 순서를 emp → dept 순이 되도록 하시오.
    select /*+ no_merge(v) leading(s,v) use_nl(v) leading(v.e, v.d) use_hash(v.d)*/ v.ename, v.sal, v.loc, s.grade from emp_dept v, salgrade s where v.sal between s.losal and s.hisal; -------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 68 | | 1 | NESTED LOOPS | | 1 | 42 | 14 |00:00:00.01 | 68 | | 2 | TABLE ACCESS FULL | SALGRADE | 1 | 5 | 5 |00:00:00.01 | 7 | |* 3 | VIEW | EMP_DEPT | 5 | 8 | 14 |00:00:00.01 | 61 | |* 4 | HASH JOIN | | 5 | 14 | 70 |00:00:00.01 | 61 | | 5 | TABLE ACCESS FULL| EMP | 5 | 14 | 70 |00:00:00.01 | 30 | | 6 | TABLE ACCESS FULL| DEPT | 5 | 4 | 20 |00:00:00.01 | 31 | --------------------------------------------------------------------------------------------
    notion image

    문제풀며 익히기

     
    Q1. 위의 sql의 실행계획이 아래와 같이 출력되게 하시오.
    select /*+ no_merge(v) leading(v,s) use_nl(s) leading(v.e, v.d) use_nl(v.d) */ v.ename, v.sal, v.loc, s.grade from emp_dept v, salgrade s where v.sal between s.losal and s.hisal; ------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 177 | | 1 | NESTED LOOPS | | 1 | 42 | 14 |00:00:00.01 | 177 | | 2 | VIEW | EMP_DEPT | 1 | 14 | 14 |00:00:00.01 | 92 | | 3 | NESTED LOOPS | | 1 | 14 | 14 |00:00:00.01 | 92 | | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 7 | |* 5 | TABLE ACCESS FULL| DEPT | 14 | 1 | 14 |00:00:00.01 | 85 | |* 6 | TABLE ACCESS FULL | SALGRADE | 14 | 3 | 14 |00:00:00.01 | 85 | ------------------------------------------------------------------------------------------- ========================= no_merge(v) : view를 해체하지 않음 leading(v,s) : view를 먼저 읽고 그 후 salgrade읽음 use_nl(s) : view와 salgarde와 nl join leading(v.e, v.d) : view 안의 테이블 읽는 순서 지정 use_nl(v.d) : view안의 dept테이블과 emp테이블을 nl join
    notion image
     

    튜닝52. 중간 데이터를 검색하는 SQL은 이렇게 튜닝해야해요.-조인 튜닝

    📖

    예제

    -- 환경구성 @demo create index emp_ename on emp(ename);
     
    예제1) 이름에 EN 또는 IN을 포함하는 사원들의 모든 컬럼을 출력하시오.
    select * from emp where ename like '%EN%' or ename like '%IN%'; EMPNO|ENAME |JOB |MGR |HIREDATE |SAL |COMM|DEPTNO| -----+------+---------+----+-----------------------+----+----+------+ 7839|KING |PRESIDENT| |1981-11-17 00:00:00.000|5000| | 10| 7654|MARTIN|SALESMAN |7698|1981-09-10 00:00:00.000|1250|1400| 30| 7499|ALLEN |SALESMAN |7698|1981-02-11 00:00:00.000|1600| 300| 30| ------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 6 | |* 1 | TABLE ACCESS FULL| EMP | 1 | 1 | 3 |00:00:00.01 | 6 | ------------------------------------------------------------------------------------
    notion image
    인덱스를 못 탄 이유 : 와일드 카드(%)가 앞에 있기 때문
     
    예제2) 위의 SQL을 다시 작성하는데, ROWID만 가져오게 하시오.
    select ROWID from emp where ename like '%EN%' or ename like '%IN%'; ROWID | ------------------+ AAATh7AAHAAAoPNAAF| AAATh7AAHAAAoPNAAA| AAATh7AAHAAAoPNAAE| ========================= 테이블에서 데이터를 가져오지 않고, 인덱스에서 가져온 것. 실행계획을 보면 INDEX FULL SCAN을 해서 가져온 값임을 알 수 있다. ---------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 1 | |* 1 | INDEX FULL SCAN | EMP_ENAME | 1 | 1 | 3 |00:00:00.01 | 1 | ----------------------------------------------------------------------------------------
    notion image
     
    예제3) 위 실행계획은 INDEX FAST FULL SCAN을 하게 하시오
    select /*+ index_ffs(emp emp_ename) */ ROWID from emp where ename like '%EN%' or ename like '%IN%'; -------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 4 | |* 1 | INDEX FAST FULL SCAN| EMP_ENAME | 1 | 1 | 3 |00:00:00.01 | 4 | --------------------------------------------------------------------------------------------
    notion image
     
    예제4) - sqlp실기 문제 ☆★
    위의 sql을 인라인뷰로 만들고 emp 와 조인해서 이름에 EN 또는 IN을 포함하는 사원들의 모든 컬럼을 출력하시오.
    select e.* from emp e, (select /*+ index_ffs(emp emp_ename) */ ROWID as rid from emp where ename like '%EN%' or ename like '%IN%') v where e.rowid = v.rid; --옵티마이저가 아래와 같이 바꿔버림-- select * from emp where ename like '%EN%' or ename like '%IN%'; ------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 6 | |* 1 | TABLE ACCESS FULL| EMP | 1 | 1 | 3 |00:00:00.01 | 6 | -----------------------------------------------------------------------------------
    notion image
    <해체하지 않게 하기> select /*+ no_merge(v) */e.* from emp e, (select /*+ index_ffs(emp emp_ename) */ ROWID as rid from emp where ename like '%EN%' or ename like '%IN%') v where e.rowid = v.rid; ---------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ---------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 5 || | 1 | NESTED LOOPS | | 1 | 1 | 3 |00:00:00.01 | 5 || | 2 | VIEW | | 1 | 1 | 3 |00:00:00.01 | 4 || |* 3 | INDEX FAST FULL SCAN | EMP_ENAME | 1 | 1 | 3 |00:00:00.01 | 4 || | 4 | TABLE ACCESS BY USER ROWID| EMP | 3 | 1 | 3 |00:00:00.01 | 1 || ---------------------------------------------------------------------------------------------------| ================================= view를 먼저 읽으면서 emp와 조인할 때 nested loop 조인을 했는데, 이때 rowid가 v.rid -> e.rowid로 넘어간것. ================================= <더 확실하게 세팅하기> select /*+ no_merge(v) leading(v,e) use_nl(e) rowid(e) */e.* from emp e, (select /*+ index_ffs(emp emp_ename) */ ROWID as rid from emp where ename like '%EN%' or ename like '%IN%') v where e.rowid = v.rid; ----------------------------- rowid(e) : 최고 1순위의 옵티마이저 검색 순위. 가장 빠른 데이터 검색 방법 rowid에 의한 데이터 검색을 하라는 힌트
    notion image
     

    문제풀며 익히기

     
    Q1.
    입사일에 09 또는 08을 포함하는 사원들의 모든 컬럼을 출력하시오.
    @demo create index emp_hiredate on emp(hiredate); select * from emp where hiredate like '%09%' or hiredate like '%08%'; EMPNO|ENAME |JOB |MGR |HIREDATE |SAL |COMM|DEPTNO| -----+------+--------+----+-----------------------+----+----+------+ 7782|CLARK |MANAGER |7839|1981-05-09 00:00:00.000|2450| | 10| 7654|MARTIN|SALESMAN|7698|1981-09-10 00:00:00.000|1250|1400| 30| 7844|TURNER|SALESMAN|7698|1981-08-21 00:00:00.000|1500| 0| 30| 7369|SMITH |CLERK |7902|1980-12-09 00:00:00.000| 800| | 20| ------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 6 || |* 1 | TABLE ACCESS FULL| EMP | 1 | 1 | 4 |00:00:00.01 | 6 || ------------------------------------------------------------------------------------|
    notion image
    alter table emp modify hiredate not null; select /*+ no_merge(v) leading(v,e) use_nl(e) rowid(e) */e.* from emp e, (select /*+ index_ffs(emp emp_hiredate) */ rowid as rid from emp where hiredate like '%09%' or hiredate like '%08%') v where e.rowid = v.rid; ------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 7 | | 1 | NESTED LOOPS | | 1 | 1 | 4 |00:00:00.01 | 7 | | 2 | VIEW | | 1 | 1 | 4 |00:00:00.01 | 5 | |* 3 | INDEX FAST FULL SCAN | EMP_HIREDATE | 1 | 1 | 4 |00:00:00.01 | 5 | | 4 | TABLE ACCESS BY USER ROWID| EMP | 4 | 1 | 4 |00:00:00.01 | 2 | ------------------------------------------------------------------------------------------------------
    notion image

    튜닝53. 조인된 결과의 검색속도가 정말 중요하다면, 다중 클러스터 테이블을 고려하세요. - DBA만 가능

    📖

    다중 클러스터 테이블

    여러 테이블들의 레코드를 하나의 물리적 공간에 같이 저장해두는 방식으로 구성된 db object이다. - DBA만 가능 ★ (즉, 아무리 조인튜닝해도 조인이 안되면, 그냥 조인 된 상태에서 저장해버린 것)
     
    Q. 면접문제 : 다중 클러스터 테이블에 대해서 말씀해 주실 수 있나요?
    A. 답: 여러 테이블들을 서로 조인된 상태로 저장해두는 것을 말합니다.
     

    다중 클러스터 테이블의 장단점

    • 장점 : 검색 속도가 현저하게 빨라진다
    • 단점 : 대량의 데이터를 insert 하는 작업이 느려짐(한건 씩 insert하는 경우가 아니라 대량으로 insert할 때)
    ⇒ 따라서 대량의 데이터 insert보다 검색이 중요한 작업에서 다중클러스터 테이블 사용
     

    다중 클러스터 설명 그림

    notion image
    왼쪽 그림의 위에가 테이블이고 , 아래가 데이터베이스에 저장되어져 있는 물리적 구조임(db에 물리적으로 어떻게 저장되어져 있는지 그림).
    예를들어 외장하드에 emp와 dept테이블을 넣는다면, 외장하드가 아래 그림이고, select해서 볼 수 있는 것들이 위의 그림이라고 생각하면 됨.
     
    아래의 그림은 일반적인 테이블의 데이터 저장 방식이다. block에 저장되며 나열되며 저장된다.
    notion image
     
    조인튜닝을 열심히 해도 답이 없는 대용량 테이블의 경우 첫번째 사진의 우측 그림과 같이 multi-table cluster를 사용한다. 즉, dept와 emp를 block에 저장할 때 조인된 상태 그대로 저장한다. 조인된 상태로 저장하다보니, 중복된 데이터가 발생하여 공간이 많이 필요하긴 하지만, 조인이 너무나 중요해서 꼭 필요하다면, 다중 클러스터를 이용해서 조인 상태를 저장해버린다.
    아래의 그림은 다중클러스터 방식의 데이터 저장 그림이다.
    notion image
     

    emp와 dept를 가지고 다중 클러스터 테이블 구현

     
    1. 클러스터를 생성한다.(클러스터 키 컬럼 데이터타입 통일하는 작업) @demo alter table emp modify deptno number(10); alter table dept modify deptno number(10); /* deptno를 클러스터 키 컬럼으로 구성할 것이기 때문에, 데이터 타입을 동일하게 맞춰줘야해서 위와 같은 작업을 할 것 */ create cluster c_deptno ( deptno number(10) ) index; /* create cluster [클러스터 명 설정] ( [클러스터 키 컬럼] [데이터타입] ) index; */ 2. 클러스터(에) 인덱스를 생성한다. --클러스터 인덱스를 이용해 클러스터 테이블의 값 엑세스 --이는 유니크한 구조로 만들어져 있다. create index i_deptno on cluster c_deptno ; 3. 클러스터 테이블을 생성한다. drop table emp_cluster; create table emp_cluster cluster c_deptno(deptno) as select * from emp; /* emp 테이블을 그대로 가져다가 emp_cluster를 만들었음 */ drop table dept_cluster; create table dept_cluster cluster c_deptno(deptno) as select * from dept ; /* 같은 클러스터를(c_deptno)사용한다는 것 명심! 그래야 c_deptno라는 같은 클러스터 사용으로 emp와 dept가 같은 블록 안에 데이터가 저장되게 됨 */ SQL> set lines 3000 SQL> set pages 2000 select * from emp_cluster; /* king이 맨 위에 있지 않고 deptno기준으로 각 부서별로 모여서 출력되고 있는 것을 확인할 수 있다. EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO ---------- -------------------- ------------------ ---------- -------- ---------- ---------- ---------- 7698 BLAKE MANAGER 7839 81/05/01 2850 30 7654 MARTIN SALESMAN 7698 81/09/10 1250 1400 30 7499 ALLEN SALESMAN 7698 81/02/11 1600 300 30 7844 TURNER SALESMAN 7698 81/08/21 1500 0 30 7900 JAMES CLERK 7698 81/12/11 950 30 7521 WARD SALESMAN 7698 81/02/23 1250 500 30 7566 JONES MANAGER 7839 81/04/01 2975 20 7902 FORD ANALYST 7566 81/12/11 3000 20 7369 SMITH CLERK 7902 80/12/09 800 20 7788 SCOTT ANALYST 7566 82/12/22 3000 20 7876 ADAMS CLERK 7788 83/01/15 1100 20 7839 KING PRESIDENT 81/11/17 5000 10 7782 CLARK MANAGER 7839 81/05/09 2450 10 7934 MILLER CLERK 7782 82/01/11 1300 10 */ select * from dept_cluster; DEPTNO DNAME LOC ---------- ---------------------------- -------------------------- 30 SALES CHICAGO 20 RESEARCH DALLAS 40 OPERATIONS BOSTON 10 ACCOUNTING NEW YORK
    이렇게 만들면, 보기에는 테이블이 별도로 있지만, 물리적으로는 두 개의 테이블이 같은 블록에 저장되어 있는 상태이다.
    notion image
     

    예제

     
    예제1) 일반 heap table과 cluster table의 성능을 비교하시오
    <튜닝전> select /*+ leading(e,d) use_nl(d) */ e.ename, d.loc, e.sal, e.hiredate, d.dname, e.deptno, e.mgr from emp e, dept d where e.deptno = d.deptno and d.loc = 'DALLAS'; ENAME|LOC |SAL |HIREDATE |DNAME |DEPTNO|MGR | -----+------+----+-----------------------+--------+------+----+ JONES|DALLAS|2975|1981-04-01 00:00:00.000|RESEARCH| 20|7839| FORD |DALLAS|3000|1981-12-11 00:00:00.000|RESEARCH| 20|7566| SMITH|DALLAS| 800|1980-12-09 00:00:00.000|RESEARCH| 20|7902| SCOTT|DALLAS|3000|1982-12-22 00:00:00.000|RESEARCH| 20|7566| ADAMS|DALLAS|1100|1983-01-15 00:00:00.000|RESEARCH| 20|7788| -------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || -------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 90 || | 1 | NESTED LOOPS | | 1 | 5 | 5 |00:00:00.01 | 90 || | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 || |* 3 | TABLE ACCESS FULL| DEPT | 14 | 1 | 5 |00:00:00.01 | 84 || -------------------------------------------------------------------------------------|
    notion image
    버퍼의 개수 : 90개
    notion image
    <튜닝후> select /*+ leading(e,d) use_nl(d) */ e.ename, d.loc, e.sal, e.hiredate, d.dname, e.deptno, e.mgr from emp_cluster e, dept_cluster d where e.deptno = d.deptno and d.loc = 'DALLAS'; ------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 35 || | 1 | NESTED LOOPS | | 1 | 5 | 5 |00:00:00.01 | 35 || | 2 | TABLE ACCESS FULL | EMP_CLUSTER | 1 | 14 | 14 |00:00:00.01 | 7 || |* 3 | TABLE ACCESS CLUSTER| DEPT_CLUSTER | 14 | 1 | 5 |00:00:00.01 | 28 || ------------------------------------------------------------------------------------------------|
    버퍼의 개수 : 35개
    notion image
     
    예제2) —block_number확인 방법
    emp_cluster와 dept_cluster가 물리적으로 정말 같은 공간에 존재하는지 확인하시오.
    select d.deptno, e.empno, e.ename, d.loc, dbms_rowid.rowid_block_number(d.rowid) dept_block_no, dbms_rowid.rowid_block_number(e.rowid) emp_block_no from dept_cluster d, emp_cluster e where e.deptno = d.deptno order by d.deptno ; DEPTNO|EMPNO|ENAME |LOC |DEPT_BLOCK_NO|EMP_BLOCK_NO| ------+-----+------+--------+-------------+------------+ 10| 7782|CLARK |NEW YORK| 164903| 164903| 10| 7839|KING |NEW YORK| 164903| 164903| 10| 7934|MILLER|NEW YORK| 164903| 164903| 20| 7566|JONES |DALLAS | 164901| 164901| 20| 7876|ADAMS |DALLAS | 164901| 164901| 20| 7788|SCOTT |DALLAS | 164901| 164901| 20| 7369|SMITH |DALLAS | 164901| 164901| 20| 7902|FORD |DALLAS | 164901| 164901| 30| 7698|BLAKE |CHICAGO | 164899| 164899| 30| 7654|MARTIN|CHICAGO | 164899| 164899| 30| 7499|ALLEN |CHICAGO | 164899| 164899| 30| 7521|WARD |CHICAGO | 164899| 164899| 30| 7900|JAMES |CHICAGO | 164899| 164899| 30| 7844|TURNER|CHICAGO | 164899| 164899| ======================================= dept block번호와 emp block번호를 조회한 것 dept_cluster와 emp_cluster를 dbms라는 패키지 안에 있는 dbms_rowid.rowid_block_number라는 함수를 사용해서 괄호 안에 각각 d.rowid와 e.rowid를 보면, 각각이 어디 block에 들어있는지 나옴
    notion image
     
     

    문제풀며 익히기

     
    Q1.
    아래의 조인 sql의 성능을 높이기 위해서 클러스터 테이블을 생성하시오
    <튜닝전> select /*+ leading(p,s) use_nl(s) */ p.prod_name, sum(s.amount_sold) from sales200 s, products200 p where s.prod_id = p.prod_id and p.prod_id = 40 group by p.prod_name; PROD_NAME |SUM(S.AMOUNT_SOLD)| -------------------------------+------------------+ O/S Documentation Set - English| 1314478.74| select sql_id, child_number, sql_text from v$sql where sql_text like '%sales%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('4y0azcnffx1rn',0,'ALLSTATS LAST')); ---------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.04 | 4439 | | 1 | HASH GROUP BY | | 1 | 1 | 1 |00:00:00.04 | 4439 | | 2 | NESTED LOOPS | | 1 | 12762 | 27114 |00:00:00.04 | 4439 | |* 3 | TABLE ACCESS FULL| PRODUCTS200 | 1 | 1 | 1 |00:00:00.01 | 3 | |* 4 | TABLE ACCESS FULL| SALES200 | 1 | 12762 | 27114 |00:00:00.04 | 4436 | ----------------------------------------------------------------------------------------------
    버퍼의 개수 : 4439개
    notion image
    <튜닝 후> -- 클러스터 테이블 만들기 1. 클러스터를 생성한다. alter table sales200 modify prod_id number; alter table products200 modify prod_id number; create cluster c_prod_id ( prod_id number ) index; 2. 클러스터 인덱스를 생성한다. create index i_prod_id on cluster c_prod_id ; 3. 클러스터 테이블을 생성한다. drop table sales200_cluster; drop table products200_cluster; create table sales200_cluster cluster c_prod_id(prod_id) as select * from sales200; create table products200_cluster cluster c_prod_id(prod_id) as select * from products200 ; --실행 후 DBeaver에서 실행계획 보기-- select /*+ leading(p,s) use_nl(s) */ p.prod_name, sum(s.amount_sold) from sales200_cluster s, products200_cluster p where s.prod_id = p.prod_id and p.prod_id = 40 group by p.prod_name; select sql_id, child_number, sql_text from v$sql where sql_text like '%sales%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('77uapss8fb4na',0,'ALLSTATS LAST')); ------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers ------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 244 | 1 | HASH GROUP BY | | 1 | 15402 | 1 |00:00:00.01 | 244 | 2 | NESTED LOOPS | | 1 | 15402 | 27114 |00:00:00.01 | 244 | 3 | TABLE ACCESS CLUSTER| PRODUCTS200_CLUSTER | 1 | 1 | 1 |00:00:00.01 | 122 |* 4 | INDEX UNIQUE SCAN | I_PROD_ID | 1 | 1 | 1 |00:00:00.01 | 1 |* 5 | TABLE ACCESS CLUSTER| SALES200_CLUSTER | 1 | 15204 | 27114 |00:00:00.01 | 122 -------------------------------------------------------------------------------------------------------
    버퍼의 개수 : 244개
    notion image
     

    튜닝54. 조인의 성능을 높이고 싶다면, MVIEW 생성을 고려하세요. - DBA만 가능

    📖

    MVIEW

    일반 VIEW는 데이터를 저장하고 있지 않은데 (일반 뷰는 그냥 쿼리 자체임), Materialize View(MVIEW)는 데이터를 저장하고 있다. materialize - 물체의 : 즉, 데이터를 가지고 있다는 의미
     
    실제 테이블과 mview데이터가 달라지는데, 같기 동기화 (?) 할 주기를 정해야하는데 그 때 사용하는게 on demand와 on commit(바로 반영)이다.
     
    • QUERY REWRITE 이란?
      • MVIEW의 데이터를 엑세스 할 수 있는 권한
      • rewrite vs no_rewrite 힌트
        • REWIRTE 힌트 : MVIEW에서 데이터를 가져와라
        • NO_REWRITE 힌트 : 테이블에서 데이터를 가져와라
         

    MVIEW 생성 실습

    MView를 생성하고 테스트 하기 위해서는, sysdba권한을 가진 유저에서 Query Rewrite권한과 CREATE MATERIALIZED VIEW 권한을 MView를 생성하는 유저에게 부여해야 합니다. 1. sysdba 권한으로 접속 합니다. SQL> conn sys/oracle_4U as sysdba 연결되었습니다. 2. QUERY REWRITE 권한을 부여 합니다. SQL> GRANT QUERY REWRITE TO C##SCOTT; 권한이 부여되었습니다. /* QUERY REWRITE : MVIEW의 데이터를 엑세스 할 수 있는 권한 */ 3. CREATE MATERIALIZED VIEW 권한을 부여 합니다. SQL> GRANT CREATE MATERIALIZED VIEW TO C##SCOTT; 권한이 부여되었습니다. 4. MATERIALIZED VIEW를 생성할 유저로 접속 합니다. SQL> conn c##scott/tiger 연결되었습니다. 5. MATERIALIZED VIEW 생성 SQL> CREATE MATERIALIZED VIEW dept_sal --MATERIALIZED를 붙여주면 데이터를 갖고있게 됨 -- PCTFREE 0 TABLESPACE mviews -- STORAGE (initial 16k next 16k pctincrease 0) BUILD IMMEDIATE -- BUILD IMMEDIATE(데이터 바로 생성), BUILD DEFERRED(데이터 나중에 생성) 선택. REFRESH COMPLETE -- FORCE, COMPLETE, FAST, NEVER 선택. ON DEMAND -- ON DEMAND, ON COMMIT 선택. ENABLE QUERY REWRITE AS SELECT SUM(a.sal), a.deptno FROM emp a, dept b WHERE a.deptno = b.deptno GROUP BY a.deptno; --이 select문이 너무 느려서, 결과데이터를 가지고 mview를 만드는 > 구체화된 뷰가 생성되었습니다. 6. MATERIALIZED VIEW 조회 SQL> SELECT * FROM DEPT_SAL; SUM(A.SAL) DEPTNO ---------- ---------- 8750 10 10875 20 9400 30
    ========================================================== ◈ 위에서 생성한 CREATE MATERIALIZED VIEW 구문을 살펴 보겠습니다. - BUILD IMMEDIATE : MView 생성과 동시에 데이터들도 생성되는 옵션입니다.. - BUILD DEFERRED : MView를 생성은 하지만, 그 안의 데이터는 추후에 생성하도록 하는 기능 입니다. .위에 MView 생성시 BUILD IMMEDIATE 대신 BUILD DEFERRED 옵션을 사용하면 조회된 데이터가 없겠죠.. - REFRESH(동기화) 절은 오라클이 MView의 데이터를 언제, 어떻게 Refresh 하는지를 결정 하는 방법입니다. .Refresh 방법에는 ON COMMIT 방법과, ON DEMAND 방법 2 가지가 있습니다. .ON COMMIT 은 기초 테이블에 Commit 이 일어날 때 Refresh 가 일어나는 방안이며, 이는 1 개의 테이블에 COUNT(*), SUM(*)과 같은 집합 함수를 사용하거나, MView에 조인만이 있는 경우, Group By 절에 사용된 컬럼에 대해 COUNT(col) 함수가 기술된 경우만 사용이 가능 합니다. --커밋될 때 mview를 refresh하는 것 .ON DEMAND는 사용자가 DBMS_MVIEW 패키지 (REFRESH, REFRESH_ALL_MVIEWS, REFRESH_DEPENDENT) 를 실행 한 경우 Refresh 되는 경우 입니다. --커밋될 때가 아니라 dbms_mview패키지를 활용해서 refresh - EMP와 DEPT가 변경되었을 때 MVIEW를 Refresh를 하는 방법에는 FORCE, COMPLETE, FAST, NEVER의 4가지가 존재 합니다.(변경이 어떻게 일어날지에 대한 옵션) .COMPLETE : MView의 정의에 따라 MView의 데이터 전체가 Refresh 되는 것으로 ATOMIC_REFRESH=TRUE와 COMPLETE으로 설정한 경우 입니다.어려우니까 나중에 .FAST : 새로운 데이터가 삽입될 때마다 점진적으로 Refresh 되는 방안으로 Direct Path나 Mview log를 이용 합니다.(즉, 전체를 싹 바꾸는게 아니라 NEW데이터 삽입때 마다 바로바로! 점진적) (EX. EMP가 변경되었을 때 바로 MVIEW를 Refresh) .FORCE : 이 경우 먼저 Fast Refresh가 가능한지 점검 후 가능하면 이를 적용하고, 아니면 Complete Refresh를 적용 합니다.(디폴트) .NEVER : MView의 Refresh를 발생시키지 않습니다 즉, 동기화 하지 않음 - ENABLE QUERY REWRITE : MView 생성시 이 옵션을 주어야만 임의의 SQL문장을을 처리시 Query Rewrite를 고려 합니다. 만일 MView 생성시 이를 지정하지 않은 경우는 ALTER MATERIALIZED VIEW를 이용하여 수정하면 됩니다. - 마지막으로 일반 View나 Snapshot처럼 AS 구문 뒤에 필요한 컬럼과 조건들을 기술 하면 됩니다
     

    예제

     
    예제1) 아래의 쿼리를 실행하고 실행계획을 보시오
    SELECT SUM(a.sal), a.deptno FROM emp a, dept b WHERE a.deptno = b.deptno GROUP BY a.deptno; select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('78wgyff8jtkvc',0,'ALLSTATS LAST')); ---------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ---------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 2 || | 1 | MAT_VIEW REWRITE ACCESS FULL| DEPT_SAL | 1 | 3 | 3 |00:00:00.01 | 2 || ---------------------------------------------------------------------------------------------------|
    emp와 dept를 조인하지 않고, MVIEW에서 데이터를 가져왔음. 실행계획을 보면 JOIN하지 않은 것을 알 수 있음.
    notion image
     
    예제2) 위의 결과에서 10번 부서번호만 출력하고, 실행계획을 확인하시오.
    SELECT SUM(a.sal), a.deptno FROM emp a, dept b WHERE a.deptno = b.deptno AND a.deptno = 10 GROUP BY a.deptno; SUM(A.SAL)|DEPTNO| ----------+------+ 8750| 10| ---------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ---------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 2 || |* 1 | MAT_VIEW REWRITE ACCESS FULL| DEPT_SAL | 1 | 1 | 1 |00:00:00.01 | 2 || ---------------------------------------------------------------------------------------------------|
    mview생성시 deptno =10 조건이 없었음에도 emp와 dept를 조인하지 않고 MVIEW에서 읽어왔음
    notion image
     
    예제3) 이번에는 같은 SQL인데, 테이블에서 데이터를 읽어오게 하시오
    SELECT /*+ no_rewrite */ SUM(a.sal), a.deptno FROM emp a, dept b WHERE a.deptno = b.deptno AND a.deptno = 10 GROUP BY a.deptno; -------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 16 | | 1 | SORT GROUP BY NOSORT | | 1 | 1 | 1 |00:00:00.01 | 16 | |* 2 | HASH JOIN | | 1 | 1 | 1 |00:00:00.01 | 16 | | 3 | VIEW | VW_GBC_5 | 1 | 1 | 1 |00:00:00.01 | 6 | | 4 | HASH GROUP BY | | 1 | 1 | 1 |00:00:00.01 | 6 | |* 5 | TABLE ACCESS FULL| EMP | 1 | 5 | 3 |00:00:00.01 | 6 | | 6 | TABLE ACCESS FULL | DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | --------------------------------------------------------------------------------------------
    notion image
     
    예제4)
    emp와 dept테이블에 아래의 데이터를 입력하고, mview에 바로 반영이 되는지 확인하시오.
    insert into dept(deptno, dname, loc) values(50, 'aaa', 'bbb'); insert into emp(empno, ename, sal, deptno) values(1234, 'aaa', 3000, 50); commit; SELECT /*+ rewrite */ SUM(a.sal), a.deptno FROM emp a, dept b WHERE a.deptno = b.deptno GROUP BY a.deptno; --------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 13 | | 1 | HASH GROUP BY | | 1 | 3 | 4 |00:00:00.01 | 13 | |* 2 | HASH JOIN | | 1 | 14 | 15 |00:00:00.01 | 13 | | 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 5 |00:00:00.01 | 6 | | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 15 |00:00:00.01 | 6 | ---------------------------------------------------------------------------------------
    notion image
    MVIEW에서 읽어와야 하는데, TABLE에서 읽어왔음 EMP와 DEPT가 에 INSERT가 일어난 이후에 DEPT_SAL_MVIEW에 바로 반영되지 않아서 , QUERY REWRITE가 작동되지 않은것.
    그래서 수동으로 동기화를 해줘야함. 왜냐하면, MVIEW를 만들 때 ON COMMIT으로 안만들고 ON DEMAND로 만들었기 대문이다.
    따라서 아래와 같이 해줘야함
    --pl/sql로 만들어봄 begin dbms_mview.refresh('DEPT_SAL'); end; / --다시 확인하기 SELECT /*+ rewrite */ SUM(a.sal), a.deptno FROM emp a, dept b WHERE a.deptno = b.deptno GROUP BY a.deptno; --------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 2 | | 1 | MAT_VIEW REWRITE ACCESS FULL| DEPT_SAL | 1 | 3 | 4 |00:00:00.01 | 2 | ---------------------------------------------------------------------------------------------------
     
    예제5) → 문제 1과 이어짐
    이번에는 commit을 하면 바로 테이블의 데이터가 mview에 반영될 수 있도록 mview를 재생성하시오.
    drop materialized view dept_sal; CREATE MATERIALIZED VIEW dept_sal BUILD IMMEDIATE -- BUILD IMMEDIATE, BUILD DEFERRED 선택. REFRESH COMPLETE -- FORCE, COMPLETE, FAST, NEVER 선택. ON COMMIT -- ON DEMAND, ON COMMIT 선택. ENABLE QUERY REWRITE AS SELECT SUM(a.sal), a.deptno FROM emp a, dept b WHERE a.deptno = b.deptno GROUP BY a.deptno; --------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 2 | | 1 | MAT_VIEW REWRITE ACCESS FULL| DEPT_SAL | 1 | 4 | 4 |00:00:00.01 | 2 | --------------------------------------------------------------------------------------------------- ----------------- ON COMMIT을 사용하였기 때문에 INSERT 되자마자 바로 반영될 것.
     

    문제풀며 익히기

     
    Q1.
    다시 EMP와 DEPT에 아래의 데이터를 입력하고, COMMIT하면 MVIEW에 바로 반영이 되어서 QUERY REWRITE가 잘 작동되는지 확인하시오.
    -- 새로운 값 입력하기 -- insert into dept(deptno, dname, loc) values( 80, 'aaa', 'bbb' ); insert into emp(empno, ename, sal, deptno) values( 3456, 'allen', 4000, 80 ); commit; -- 값이 입력 되었는지 바로 조회 해보기 -- SELECT SUM(a.sal), a.deptno FROM emp a, dept b WHERE a.deptno = b.deptno GROUP BY a.deptno; SUM(A.SAL)|DEPTNO| ----------+------+ 8750| 10| 9400| 30| 10875| 20| 3000| 50| 4000| 80| -- 실행계획으로 mview를 사용했는지 확인해보기 -- --------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 2 | | 1 | MAT_VIEW REWRITE ACCESS FULL| DEPT_SAL | 1 | 4 | 5 |00:00:00.01 | 2 | ---------------------------------------------------------------------------------------------------
    notion image
     
    Q2.
    아래의 쿼리가 query rewrite가 가능하도록 mview를 생성하시오.
    select d.dname, avg(e.sal) from emp e, dept d where e.deptno = d.deptno group by d.dname; DNAME |AVG(E.SAL) | ----------+-----------------------------------------+ ACCOUNTING|2916.666666666666666666666666666666666667| SALES |1566.666666666666666666666666666666666667| RESEARCH | 2175|
    drop materialized view dept_avg; CREATE MATERIALIZED VIEW dept_avg BUILD IMMEDIATE -- BUILD IMMEDIATE, BUILD DEFERRED 선택. REFRESH COMPLETE -- FORCE, COMPLETE, FAST, NEVER 선택. ON DEMAND -- ON DEMAND, ON COMMIT 선택. ENABLE QUERY REWRITE AS select d.dname, avg(e.sal) from emp e, dept d where e.deptno = d.deptno group by d.dname; select /*+ rewrite */ d.dname, avg(e.sal) from emp e, dept d where e.deptno = d.deptno group by d.dname; --------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 2 | | 1 | MAT_VIEW REWRITE ACCESS FULL| DEPT_AVG | 1 | 3 | 3 |00:00:00.01 | 2 | --------------------------------------------------------------------------------------------------- #avg 때문에 on commit은 사용할 수 없음.
     
    Q4.
    아래의 쿼리가 query rewrite가 가능하도록 mview를 생성하시오.
    select d.dname, sum(e.sal) from emp e, dept d where e.deptno = d.deptno group by d.dname; DNAME |SUM(E.SAL)| ----------+----------+ ACCOUNTING| 8750| SALES | 9400| RESEARCH | 10875|
    drop materialized view dept_sum; CREATE MATERIALIZED VIEW dept_sum BUILD IMMEDIATE -- BUILD IMMEDIATE, BUILD DEFERRED 선택. REFRESH COMPLETE -- FORCE, COMPLETE, FAST, NEVER 선택. ON DEMAND -- ON DEMAND, ON COMMIT 선택. ENABLE QUERY REWRITE AS select d.dname, sum(e.sal) from emp e, dept d where e.deptno = d.deptno group by d.dname; --------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 2 | | 1 | MAT_VIEW REWRITE ACCESS FULL| DEPT_SUM | 1 | 3 | 3 |00:00:00.01 | 2 | ---------------------------------------------------------------------------------------------------
    notion image
     
    Q5.
    아래의 쿼리가 테이블에서 데이터를 읽어오도록 힌트를 주시오.
    select d.dname, sum(e.sal) from emp e, dept d where e.deptno = d.deptno group by d.dname;
    select /*+ no_rewrite */ d.dname, sum(e.sal) from emp e, dept d where e.deptno = d.deptno group by d.dname; --------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 13 | | 1 | HASH GROUP BY | | 1 | 4 | 3 |00:00:00.01 | 13 | |* 2 | HASH JOIN | | 1 | 14 | 14 |00:00:00.01 | 13 | | 3 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | ---------------------------------------------------------------------------------------
    notion image
     
    Q6.
    생성한 MVIEW를 모두 DROP하시오
    #1. MVIEW와 관련한 딕셔너리 확인 select * from dictionary --데이터 베이스와 관련한 사전을 모두 담고있는 사전(잘알아두기) where table name like '%MVIEW%'; --대문자로 #2. USER_MVIES를 조회합니다. select * from user_mviews; OWNER |MVIEW_NAME|CONTAINER_NAME|QUERY --------+----------+--------------+---------- C##SCOTT|DEPT_AVG |DEPT_AVG |select d.d... C##SCOTT|DEPT_SAL |DEPT_SAL |SELECT SUM... C##SCOTT|DEPT_SUM |DEPT_SUM |select d.d... #3. MVIEW를 전부 DROP합니다. drop materialized view dept_sal; drop materialized view dept_sum; drop materialized view dept_avg; > 구체화된 뷰가 삭제되었습니다.
     
     

    ■ 서브쿼리(Sub-query) 튜닝

     

    튜닝55. 서브쿼리의 데이터가 작으면, 서브쿼리부터 수정해야해요.

    📖
     
    no_unnest = no, un이라는 부정이 두번이나 나온 것은. 강한 긍정.
    즉, 괄호를 풀어해치지말고 감싸라. 조인으로 변경하지 말고 서브쿼리를 수행해라. 라는 의미
    no_unnest, push_subq와는 짝
     

    예제

     
    예제1) 아래의 sql의 실행계획을 확인하시오.
    select ename, sal from emp where deptno in (select deptno from dept where deptno = 10); --------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | --------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 12 | |* 1 | HASH JOIN SEMI | | 1 | 5 | 3 |00:00:00.01 | 12 | |* 2 | TABLE ACCESS FULL| EMP | 1 | 5 | 3 |00:00:00.01 | 6 | |* 3 | TABLE ACCESS FULL| DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | ---------------------------------------------------------------------------------------
    notion image
    분명히 서브쿼리로 사용했는데, 실행계획을 확인하니 해쉬조인을 사용함 해쉬조인은 남발하여 사용하는게 좋지 않으므로, 필요한 환경에서 필요할 때만 사용하는게 좋음.(해쉬 조인은 메모리를 사용하기 때문) 따라서 순수하게 서브쿼리로 수행되게 해보기.
     
    예제2)
    위의 sql이 순수하게 서브쿼리로 실행계획이 나와서 수행되게 하시오.
    select ename, sal from emp where deptno in (select /*+ no_unnest push_subq */deptno from dept where deptno = 10); -------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 14 | |* 1 | TABLE ACCESS FULL | EMP | 1 | 1 | 3 |00:00:00.01 | 14 | |* 2 | FILTER | | 3 | | 1 |00:00:00.01 | 7 | |* 3 | TABLE ACCESS FULL| DEPT | 1 | 1 | 1 |00:00:00.01 | 7 | -------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter( IS NOT NULL) 2 - filter(10=:B1) 3 - filter("DEPTNO"=:B1)
    notion image
    no_unnest 힌트는 조인으로 변경하지 말고 서브쿼리로 수행하라!라는 뜻이며, push_subq를 써서 서브쿼리부터 수행되게 했다.
     
    예제3) 위의 서브쿼리문에 메인 쿼리 부터 수행되게 하시오
    select ename, sal from emp where deptno in (select /*+ no_unnest no_push_subq */deptno from dept where deptno = 10); -------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 14 | |* 1 | FILTER | | 1 | | 3 |00:00:00.01 | 14 | | 2 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 7 | |* 3 | FILTER | | 3 | | 1 |00:00:00.01 | 7 | |* 4 | TABLE ACCESS FULL| DEPT | 1 | 1 | 1 |00:00:00.01 | 7 | --------------------------------------------------------------------------------------
    notion image
    notion image
    FILTER가 맨 위에 있다는 것이 메인쿼리부터 수행한것(?_)
     

    문제풀며 익히기

     
    Q1. 아래의 SQL을 튜닝하시오
    select count(*) from sales200; COUNT(*) ---------- 918843 select count(*) from customers200; COUNT(*) ---------- 55500 create index sales200_cust_id on sales200(cust_id); <튜닝전> select count(*) from sales200 where cust_id in (select /*+ no_unnest no_push_subq */cust_id from customers200 where cust_first_name = 'Abel'); => 악성인 이유. main query부터 수행하게 했기 때문! --악성이라 값을 보기 어려워 예상 실행계획만 확인해봄-- ------------------------------------------------------------------------------------ | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | 5 | 333M (1)| 03:37:00 | | 1 | SORT AGGREGATE | | 1 | 5 | | | |* 2 | FILTER | | | | | | | 3 | TABLE ACCESS FULL| SALES200 | 918K| 4486K| 1238 (2)| 00:00:01 | |* 4 | TABLE ACCESS FULL| CUSTOMERS200 | 1 | 12 | 424 (1)| 00:00:01 | ------------------------------------------------------------------------------------
    <튜닝후> select count(*) from sales200 where cust_id in (select /*+ no_unnest push_subq */cust_id from customers200 where cust_first_name = 'Abel'); COUNT(*)| --------+ 1336| ----------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ----------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:16.87 | 10M| | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:16.87 | 10M| |* 2 | INDEX FAST FULL SCAN| SALES200_CUST_ID | 1 | 45942 | 1336 |00:00:16.87 | 10M| |* 3 | TABLE ACCESS FULL | CUSTOMERS200 | 7059 | 1 | 7 |00:00:16.65 | 10M| ----------------------------------------------------------------------------------------------------- ============================================ no_unnest만 힌트를 넣어주면 세미 조인을 사용하기 때문에 더 빠름 하지만,서브쿼리 사용하는 것을 보여주기 위해서 push_subq한 것임 40초 넘게 걸렸지만, main query 부터 수행하는 것 보다는 빠르게 결과가 출력됨.
    notion image
     
     
     
     

    튜닝56. 서브쿼리를 세미조인으로 변경해서 수행되게 하시오. ★

    📖
     

    ★HASH SEMI JOIN : hash_sj ★

    순수하게 서브쿼리로만 수행되게 되면, 실행계획이 제한적이다. 서브쿼리부터 수행하느냐, 아니면 메인쿼리부터 수행하느냐 둘 중 하나임..
    그런데 조인으로 변경하게 되면, 다양한 조인 방법을 사용할 수 있고, 조인 방법 중에 아주 강력한 해쉬 조인을 사용할 수 있게 됨. 조인이면 조인이지 왜 semi(절반) 조인인가? 완전한 조인이면, 조인 순서를 자유롭게 변경할 수 있는데, 절반의 조인이어서, 무조건 main query의 table을 먼저 driving함 (단, 마법의 힌트 swap_join_inputs()를 사용하면 변경할 수 있음)

    HASH SEMI JOIN이 유용한 경우

    서브쿼리와 메인쿼리 둘 다 대용량 테이블일 때 , 속도를 높이기 위해서 유용하다.
     

    예제

     
    예제1) 아래의 sql을 해쉬 세미 조인이 되게 하시오
    select ename, sal from emp where deptno in (select /*+ unnest hash_sj */deptno from dept where loc='DALLAS'); select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('dkctpk7xzr9s9',0,'ALLSTATS LAST')); ------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 12 | |* 1 | HASH JOIN SEMI | | 1 | 5 | 5 |00:00:00.01 | 12 | | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | |* 3 | TABLE ACCESS FULL| DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | -------------------------------------------------------------------------------------
    emp가 먼저 수행되었기 때문에, emp가 메모리로 올라간 해쉬테이블이 된 것임을 알 수 있다. 즉 세미 조인은 무조건 main query를 먼저 해시 테이블로 사용함
    notion image

    문제풀며 익히기

     
    Q1. 아래의 SQL을 HASH SEMI JOIN으로 수행되게 하시오
    < 튜닝 전: 돌리진 말기! 오래걸리니까 > select count(*) from customers200 where cust_id in (select /*+ no_unnest push_subq */ cust_id from sales200 where amount_sold between 0 and 10000);
    < 튜닝 후 > select count(*) from customers200 where cust_id in (select /*+ unnest hash_sj */ cust_id from sales200 where amount_sold between 0 and 10000); --sql_id와 child_number확인하기 select sql_id, child_number, sql_text from v$sql where sql_text like '%cust%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; --실행계획 확인하기 SELECT * FROM TABLE(dbms_xplan.display_cursor('7tb0yvhnxzw8g',0,'ALLSTATS LAST')); ---------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.05 | 5960 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.05 | 5960 | |* 2 | HASH JOIN SEMI | | 1 | 7059 | 7059 |00:00:00.05 | 5960 | | 3 | TABLE ACCESS FULL| CUSTOMERS200 | 1 | 55500 | 55500 |00:00:00.01 | 1520 | |* 4 | TABLE ACCESS FULL| SALES200 | 1 | 918K| 918K|00:00:00.02 | 4440 | ----------------------------------------------------------------------------------------------
    notion image

    튜닝57. 세미 조인도 서브쿼리부터 수행되게 할 수 있어요.

    📖
     
    세미조인은 원래는 메인쿼리의 테이블을 driving 테이블로 두고 조인하는 조인방법입니다. 그런데 서브쿼리의 테이블을  driving 테이블로 둘 수 있습니다. 바로  swap_join_inputs 를 쓰면 됩니다.

    ★HASH SEMI JOIN : hash_sj ★

    순수하게 서브쿼리로만 수행되게 되면, 실행계획이 제한적이다. 서브쿼리부터 수행하느냐, 아니면 메인쿼리부터 수행하느냐 둘 중 하나임..
    그런데 조인으로 변경하게 되면, 다양한 조인 방법을 사용할 수 있고, 조인 방법 중에 아주 강력한 해쉬 조인을 사용할 수 있게 됨. 조인이면 조인이지 왜 semi(절반) 조인인가? 완전한 조인이면, 조인 순서를 자유롭게 변경할 수 있는데, 절반의 조인이어서, 무조건 main query의 table을 먼저 driving함 (단, 마법의 힌트 swap_join_inputs()를 사용하면 변경할 수 있음)
     

    HASH SEMI JOIN이 유용한 경우

    서브쿼리와 메인쿼리 둘 다 대용량 테이블일 때 , 속도를 높이기 위해서 유용하다. 세미조인은 원래 메인쿼리의 테이블을 driving table로 두고 조인하는 조인방법 그런데, 서브쿼리의 테이블을 driving table로 둘 수 있음 서브쿼리가 hash table이 되도록 할 수 있는 것.
    바로 swap_join_inputs를 사용하면 됨!

    예시

     
    예시1)
    아래의 sql을 해쉬세미조인 되게 하는데, 서브쿼리의 테이블이 해쉬테이블이 되게 하시오.
    <튜닝전> select ename, sal from emp where deptno in (select deptno from dept where loc = 'DALLAS'); -------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 12 | |* 1 | HASH JOIN SEMI | | 1 | 5 | 5 |00:00:00.01 | 12 | | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | |* 3 | TABLE ACCESS FULL| DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | --------------------------------------------------------------------------------------
    dept가 더 데이터가 적은데도 불구하고 emp를 먼저 읽음 따라서 작은 테이블이 메모리로 올라가는 hash table이 되는게 버퍼를 더 적게 사용해서 좋기 때문에 , 순서 변경해주기
    <튜닝후> select ename, sal from emp where deptno in (select /*+ unnest hash_sj swap_join_inputs(dept) */deptno from dept where loc = 'DALLAS'); ---------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 12 | |* 1 | HASH JOIN RIGHT SEMI| | 1 | 5 | 5 |00:00:00.01 | 12 | |* 2 | TABLE ACCESS FULL | DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | | 3 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 6 | ----------------------------------------------------------------------------------------
    notion image
     

    문제풀며 익히기

     
    Q1.
    아래의 sql의 서브쿼리의 테이블이 hash table이 되도록 HASH RIGHT SEMI JOIN이 되게 하시오.
    <튜닝전 : 오래걸리니까 돌리지 말기 > select count(*) from sales200 where cust_id in (select /*+ no_unnest no_push_subq */cust_id from customers200 where cust_first_name = 'Abel');
    <튜닝후> select count(*) from sales200 where cust_id in (select /*+ unnest hash_sj swap_join_inputs(c) */cust_id from customers200 c where cust_first_name = 'Abel'); ------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.03 | 5960 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.03 | 5960 | |* 2 | HASH JOIN RIGHT SEMI| | 1 | 5557 | 1336 |00:00:00.03 | 5960 | |* 3 | TABLE ACCESS FULL | CUSTOMERS200 | 1 | 43 | 63 |00:00:00.01 | 1520 | | 4 | TABLE ACCESS FULL | SALES200 | 1 | 918K| 918K|00:00:00.01 | 4440 | ------------------------------------------------------------------------------------------------
    notion image

    튜닝58. NOT IN을 사용한 서브쿼리는 이렇게 튜닝해야해요

    📖

    ANTI JOIN

    IN과 NOT IN 둘 다 해쉬 조인을 사용하는데, IN을 사용하면 SEMI 조인이 나오고, NOT IN을 사용하면, ANTI JOIN 실행계획이 출력된다. 그러면서, 해쉬 조인을 사용할 수 있기 때문에 빠르게 서브쿼리를 수행할 수 있다.
    select ename, sal from emp where deptno not in ( select deptno from dept where loc = 'DALLAS'); -------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 9 |00:00:00.01 | 12 | |* 1 | HASH JOIN ANTI NA | | 1 | 9 | 9 |00:00:00.01 | 12 | | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | |* 3 | TABLE ACCESS FULL| DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | --------------------------------------------------------------------------------------
    notion image
    그런데, 위의 경우에 해쉬 테이블 EMP테이블이 되었다. 만약 DEPT테이블을 해쉬테이블로 구성하려면 어떻게 해야할까요?
    select ename, sal from emp where deptno not in ( select /*+ unnest hash_aj swap_join_inputs(dept) */ deptno from dept where loc = 'DALLAS'); !!계속 emp가 hash테이블로 구성됨!! -------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 9 |00:00:00.01 | 12 | |* 1 | HASH JOIN ANTI NA | | 1 | 9 | 9 |00:00:00.01 | 12 | | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | |* 3 | TABLE ACCESS FULL| DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | --------------------------------------------------------------------------------------
    notion image
    되게 하려면, 다음과 같이 수행해야한다. —sqlp 실기 기출문제 ★
    select ename, sal from emp where deptno not in ( select /*+ unnest hash_aj swap_join_inputs(dept) */ deptno from dept where loc = 'DALLAS' and deptno is not null) and deptno is not null; ---------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 9 |00:00:00.01 | 12 | |* 1 | HASH JOIN RIGHT ANTI| | 1 | 9 | 9 |00:00:00.01 | 12 | |* 2 | TABLE ACCESS FULL | DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | |* 3 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 6 | ---------------------------------------------------------------------------------------- ========================== HASH JOIN RIGHT ANTI가 나오게 하려면 서브쿼리와 main쿼리 모두 not null조건을 넣어줘야 swap이 가능함!
     

    문제풀며 익히기

     
    Q1. 아래의 sql을 해쉬 right anti조인이 되게 튜닝하시오
    <HASH JOIN ANTI> select count(*) from customers200 where cust_id not in (select cust_id from sales200 where amount_sold between 0 and 10000); -------------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || -------------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.05 | 5960 | | | || | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.05 | 5960 | | | || |* 2 | HASH JOIN ANTI | | 1 | 48441 | 48441 |00:00:00.05 | 5960 | 4399K| 3201K| 4776K (0)|| | 3 | TABLE ACCESS FULL| CUSTOMERS200 | 1 | 55500 | 55500 |00:00:00.01 | 1520 | | | || |* 4 | TABLE ACCESS FULL| SALES200 | 1 | 918K| 918K|00:00:00.02 | 4440 | | | || -------------------------------------------------------------------------------------------------------------------------|
    notion image
    <HASH JOIN RIGHT ANTI> select count(*) from customers200 where cust_id not in (select /*+ unnest hash_aj swap_join_inputs(s) */cust_id from sales200 s where amount_sold between 0 and 10000 and cust_id is not null) and cust_id is not null; --------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | --------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.10 | 5960 | | | | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.10 | 5960 | | | | |* 2 | HASH JOIN RIGHT ANTI| | 1 | 48441 | 48441 |00:00:00.10 | 5960 | 42M| 6400K| 47M (0)| |* 3 | TABLE ACCESS FULL | SALES200 | 1 | 918K| 918K|00:00:00.03 | 4440 | | | | | 4 | TABLE ACCESS FULL | CUSTOMERS200 | 1 | 55500 | 55500 |00:00:00.01 | 1520 | | | | ---------------------------------------------------------------------------------------------------------------------------
    notion image
     
    Q2.
    아래의 sql을 hash right anti join이 되게 하시오
    select count(*) from sales200 where cust_id not in (select cust_id from customers200 where cust_first_name = 'Abel'); --------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | --------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.03 | 5960 | | | | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.03 | 5960 | | | | |* 2 | HASH JOIN RIGHT ANTI| | 1 | 913K| 917K|00:00:00.03 | 5960 | 2402K| 2402K| 1692K (0)| |* 3 | TABLE ACCESS FULL | CUSTOMERS200 | 1 | 43 | 63 |00:00:00.01 | 1520 | | | | | 4 | TABLE ACCESS FULL | SALES200 | 1 | 918K| 918K|00:00:00.01 | 4440 | | | | --------------------------------------------------------------------------------------------------------------------------- 옵티마이저가 기억해버려서 알아서 hash right anti join을 하지만 아래와 같이 swap과 not null을 해줘야 맞음! select count(*) from sales200 where cust_id not in (select /*+ unnest hash_aj swap_join_inputs(c) */ cust_id from customers200 c where cust_first_name = 'Abel');
    notion image
     
     
     

    튜닝59. 서브쿼리와 메인쿼리의 테이블명이 동일하면, 이 힌트를 쓰세요! - QB_NAME 힌트

    📖

    QB_NAME 힌트

    메인쿼리와 서브쿼리의 테이블명이 동일하여,
    실행계획을 보았을 때 무엇이 먼저 실행되었는지 알 수 없는 경우 사용하는 힌트
    즉, 쿼리의 이름을 지정해주는 힌트이다.
     
    예제1) 관리자인 사원들의 이름을 출력하시오.
    select ename from emp where empno in ( select mgr from emp ); ------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 6 |00:00:00.01 | 12 | |* 1 | HASH JOIN SEMI | | 1 | 6 | 6 |00:00:00.01 | 12 | | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | |* 3 | TABLE ACCESS FULL| EMP | 1 | 13 | 13 |00:00:00.01 | 6 | -------------------------------------------------------------------------------------
    notion image
    실행계획을 보면, 메인쿼리와 서브쿼리 모두 테이블명이 emp라서 둘 중 무엇을 먼저 수행했는지 알 수 없다.
    그래서 사용하는 힌트가 QB_NAME
     
    예제2) QB_NAME 힌트를 이용해서 위의 SQL의 실행계획을 보시오
    select /*+ QB_NAME(main) */ ename from emp where empno in ( select /*+ QB_NAME(sub) */ mgr from emp ); SELECT * FROM TABLE(dbms_xplan.display_cursor(null,null,'ADVANCED ALLSTATS LAST')); --NULL부분에 SQL_ID와 CHILD_NUMBER를 각각 넣어줘야함 --ALLSTATS LAST 대신 ADVANCED ALLSTATS LAST를 넣어줘야함 select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('5cq56ppsmxwv5',0,'ADVANCED ALLSTATS LAST')); ------------------------------------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ------------------------------------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | | 6 (100)| | 6 |00:00:00.01 | 12 | | | || |* 1 | HASH JOIN SEMI | | 1 | 6 | 66 | 6 (0)| 00:00:01 | 6 |00:00:00.01 | 12 | 1856K| 1856K| 1553K (0)|| | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 112 | 3 (0)| 00:00:01 | 14 |00:00:00.01 | 6 | | | || |* 3 | TABLE ACCESS FULL| EMP | 1 | 13 | 39 | 3 (0)| 00:00:01 | 13 |00:00:00.01 | 6 | | | || ------------------------------------------------------------------------------------------------------------------------------------------------| | Query Block Name / Object Alias (identified by operation id): | ------------------------------------------------------------- | | 1 - SEL$526A7031 | 2 - SEL$526A7031 / "EMP"@"MAIN" | 3 - SEL$526A7031 / "EMP"@"SUB" dbeaver에서만 위와 같이 보이고 dos창에서는 아래와 같이 보임 Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("EMPNO"="MGR") 3 - filter("MGR" IS NOT NULL) | |
    notion image
     
    예제3)
    아래의 SQL이 순수하게 서브쿼리로 수행되게 하고 서브쿼리부터 수행되게 하시오.!
    select /*+ QB_NAME(main) */ ename from emp where empno in ( select /*+ QB_NAME(sub) */ mgr from emp );
    답: select /*+ QB_NAME(main) */ ename from emp where empno in ( select /*+ QB_NAME(sub) no_unnest push_subq(emp@sub) */ mgr from emp ); !!원하는 실행계획 나오지 않음!! push_subq의 괄호에 이름을 넣어주지 말고 아래와 같이 아무것도 넣지 않아야함 select /*+ QB_NAME(main) */ ename from emp where empno in ( select /*+ QB_NAME(sub) no_unnest push_subq */ mgr from emp ); ---------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time | A-Rows | A-Time | Buffers || ---------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | | 6 (100)| | 6 |00:00:00.01 | 91 || |* 1 | TABLE ACCESS FULL | EMP | 1 | 1 | 8 | 3 (0)| 00:00:01 | 6 |00:00:00.01 | 91 || |* 2 | TABLE ACCESS FULL| EMP | 14 | 2 | 6 | 3 (0)| 00:00:01 | 6 |00:00:00.01 | 84 || ---------------------------------------------------------------------------------------------------------------------| | Query Block Name / Object Alias (identified by operation id): | ------------------------------------------------------------- | | 1 - MAIN / "EMP"@"MAIN" | 2 - SUB / "EMP"@"SUB" | |
    notion image

    튜닝60. IN연산자를 EXISTS로 변경해서 튜닝하시오.

    📖

    IN과 EXISTS문 비교

    IN은 다 찾아서 조인하지만, EXISTS문은 메인쿼리의 데이터가 서브쿼리에 존재하면, 거기서 찾는 것을 멈춥니다. 메인 쿼리의 데이터의 양이 얼마 되지 않을 때 유리한 SQL문이다.

    EXISTS문 그림 설명

    notion image
    main query : sales a sub query : customers b
     
    파란색 데이터가 customers에 존재하는지 scan하다가 찾으면 멈춤. 존재하기 때문에 멈추는 것. 뒤에 파란색 데이터가 아무리 많아도 읽지 않음.
    반면에 IN은 다아아ㅏ아 찾아서 조인함
     
     
    예제1) 아래의 서브쿼리문을 수행하시오.
    select deptno from dept where deptno in (select deptno from emp); DEPTNO| ------+ 10| 30| 20| ------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 12 | |* 1 | HASH JOIN SEMI | | 1 | 3 | 3 |00:00:00.01 | 12 | | 2 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | 3 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | -------------------------------------------------------------------------------------
    notion image
    위의 sql의 문제점은 dept테이블의 deptno를 모두 emp 테이블에 가서 조인을 해서 결과를 출력한게 문제점이다. 만약 emp테이블이 엄청 큰 대용량 테이블이면 바람직한 sql은 아니다. 따라서 이 sql을 exists문으로 다음과 같이 튜닝해야함.
    select deptno from dept d where exists ( select deptno from emp e where e.deptno = d.deptno); DEPTNO| ------+ 10| 30| 20|

    문제풀며 익히기

     
    Q1. 아래의 SQL을 EXISTS문으로 변경하시오
    <튜닝전> select count(*) from customers200 where cust_first_name = 'Abel' and cust_id in (select cust_id from sales200 where amount_sold between 0 and 10000); COUNT(*)| --------+ 7| -------------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || -------------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.04 | 5960 | | | || | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.04 | 5960 | | | || |* 2 | HASH JOIN SEMI | | 1 | 43 | 7 |00:00:00.04 | 5960 | 2402K| 2402K| 1703K (0)|| |* 3 | TABLE ACCESS FULL| CUSTOMERS200 | 1 | 43 | 63 |00:00:00.01 | 1520 | | | || |* 4 | TABLE ACCESS FULL| SALES200 | 1 | 918K| 918K|00:00:00.02 | 4440 | | | || -------------------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 2 - access("CUST_ID"="CUST_ID") | 3 - filter("CUST_FIRST_NAME"='Abel') | 4 - filter(("AMOUNT_SOLD">=0 AND "AMOUNT_SOLD"<=10000)) |
    <튜닝 후> select count(*) from customers200 c where cust_first_name = 'Abel' and exists (select cust_id from sales200 s where s.cust_id = c.cust_id and amount_sold between 0 and 10000); ----------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ----------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.05 | 5960 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.05 | 5960 | |* 2 | HASH JOIN SEMI | | 1 | 43 | 7 |00:00:00.05 | 5960 | |* 3 | TABLE ACCESS FULL| CUSTOMERS200 | 1 | 43 | 63 |00:00:00.01 | 1520 | |* 4 | TABLE ACCESS FULL| SALES200 | 1 | 918K| 918K|00:00:00.02 | 4440 | ----------------------------------------------------------------------------------------------- 원래는 더 좋은 성능을 보임.
     

    튜닝61. MINUS 를 → NOT EXISTS문으로 변경해서 튜닝하시오.

    📖

    MINUS → NOT EXISTS

    MINUS집합 연산자는 정렬 작업을 일으킵니다. 대용량 테이블을 사용하게 되면, 성능이 느려집니다. 이럴 때 NOT EXISTS문을 사용하면, 정렬 없이 바로 결과를 볼 수 있습니다.
     
    예제1) SQLP 주관식 문제
    아래의 SQL을 NOT EXISTS문으로 수행하시오.
    select deptno from dept minus select deptno from emp; DEPTNO| ------+ 40|
    select deptno from dept d where not exists ( select deptno from emp e where e.deptno = d.deptno); DEPTNO| ------+ 40| 존재하지 않는 것을 찾는 것이므로, 같은 결과가 출력된다.

    튜닝62. 메인쿼리의 조인 조건을 서브쿼리 안으로 파고 들어가게 하세요. ★ -sqlp에도 출제되는 내용

    📖

    조인↔인라인 뷰 관련 힌트

    1. push_pred : 조인 조건을 in line view 안으로 파고 들어가게 하라
    1. no_push_pred : 조인 조건을 in line view 안으로 파고 들어가지 않게 하라
     
    예제1)
    --환경 구성 @demo create index emp_deptno on emp(deptno);
    <튜닝전> select /*+ no_merge(v) no_push_pred(v) */ * from dept d, (select empno, ename, deptno from emp ) v where v.deptno(+) = d.deptno --SIGN없는 쪽 -> 있는 쪽으로 DRIVING and d.loc = 'CHICAGO'; DEPTNO|DNAME|LOC |EMPNO|ENAME |DEPTNO| ------+-----+-------+-----+------+------+ 30|SALES|CHICAGO| 7698|BLAKE | 30| 30|SALES|CHICAGO| 7654|MARTIN| 30| 30|SALES|CHICAGO| 7499|ALLEN | 30| 30|SALES|CHICAGO| 7844|TURNER| 30| 30|SALES|CHICAGO| 7900|JAMES | 30| 30|SALES|CHICAGO| 7521|WARD | 30| select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('auuzb6bh1tbps',0,'ALLSTATS LAST')); -----------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || -----------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 6 |00:00:00.01 | 12 | | | || |* 1 | HASH JOIN OUTER | | 1 | 5 | 6 |00:00:00.01 | 12 | 1476K| 1476K| 784K (0)|| |* 2 | TABLE ACCESS FULL | DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | | | || | 3 | VIEW | | 1 | 14 | 14 |00:00:00.01 | 6 | | | || | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || -----------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 1 - access("V"."DEPTNO"="D"."DEPTNO") | 2 - filter("D"."LOC"='CHICAGO') | | no_merge 때문에 in line view를 해체하지 않아서 1억건 짜리 테이블을 full scan하는 문제가 발생 기껏 full sacn했는데, chicago만 가져옴.. 현장에서는 통계정보를 잘 수집하지 않아서 일부러 no_merge힌트를 주지 않아도 인라인뷰를 해체하지 않는 경우가 발생해서 일부러 현장과 비슷하게 환경을 만들어 준 것. ------------------------------------------- 인라인 뷰 안의 쿼리문에 WHERE절이 없기 때문에 FULL TABLE SCAN을 하게 됨. 그래서 전부 FULL TABLE SCAN하고 또 DEPT테이블과 다 조인했더니 결국 가져오는 것은 부서위치 CHICAGO만 가져와라 라고 하면, 조인 해놓은것 중에 CHICAGO외에는 다 버려야하는 비효율이 발생해버림. 그러지 말고 힌트를 사용해서 메인쿼리쪽의 조인조건을 IN LINE VIEW 안쪽으로 파고들어가게 할 수 있는 힌트가 있음. 즉, where v.deptno(+) = d.deptno 이 조인 조건을 힌트를 사용하여 (select empno, ename, deptno from emp ) 라는 IN LILNE VIEW 안으로 들어가서, 이 안에서 부서번호가 20일 경우 부서번호 20번인 데이터만 ACCESS할 수 있게 함. 단, 이런 환경이 만들기 어려운 환경이라 실행계획이 깔끔하지 않게 나옴. - no_push_pred(v) : 파고들어가지 못하게 하는 힌트 - push_pred(v) : 조인 조건을 인라인 뷰 안으로 파고 들어가게 하는 힌트
    notion image
    notion image
    풀 스캔을 하고 있는 것을 확인할 수 있고, VIEW는 인라인뷰를 해체하지 않았기 때문에 실행계획에 VIEW라고 나온 것
    <튜닝후> select /*+ no_merge(v) push_pred(v) */ * from dept d, (select empno, ename, deptno from emp ) v where v.deptno(+) = d.deptno --SIGN없는 쪽 -> 있는 쪽으로 DRIVING and d.loc = 'CHICAGO'; select empno, ename, deptno from emp where deptno = 20; 이라는 쿼리가 만들어진것.
    notion image

    문제풀며 익히기

     
    Q1. 아래의 sql을 튜닝하시오
    --환경 구성 @demo create index emp_deptno on emp(deptno); < 튜닝전 > select /*+ no_merge(v) no_push_pred(v) */v.deptno, v.avgsal, d.loc from ( select deptno, avg(sal) avgsal from emp group by deptno ) v, dept d where v.deptno (+) = d.deptno and d.loc = 'CHICAGO'; DEPTNO|AVGSAL |LOC | ------+-----------------------------------------+-------+ 30|1566.666666666666666666666666666666666667|CHICAGO| select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('38n6vuswukbhk',0,'ALLSTATS LAST')); -------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 12 | |* 1 | HASH JOIN OUTER | | 1 | 1 | 1 |00:00:00.01 | 12 | | 2 | JOIN FILTER CREATE | :BF0000 | 1 | 1 | 1 |00:00:00.01 | 6 | |* 3 | TABLE ACCESS FULL | DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | | 4 | VIEW | | 1 | 3 | 1 |00:00:00.01 | 6 | | 5 | HASH GROUP BY | | 1 | 3 | 1 |00:00:00.01 | 6 | | 6 | JOIN FILTER USE | :BF0000 | 1 | 14 | 6 |00:00:00.01 | 6 | |* 7 | TABLE ACCESS FULL| EMP | 1 | 14 | 6 |00:00:00.01 | 6 | -------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("V"."DEPTNO"="D"."DEPTNO") 3 - filter("D"."LOC"='CHICAGO') 7 - filter(SYS_OP_BLOOM_FILTER(:BF0000,"DEPTNO"))
    notion image
    < 튜닝후 > select /*+ no_merge(v) push_pred(v) */v.deptno, v.avgsal, d.loc from ( select deptno, avg(sal) avgsal from emp group by deptno ) v, dept d where v.deptno (+) = d.deptno and d.loc = 'CHICAGO'; ------------------------------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ------------------------------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 8 | | | || | 1 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 8 | | | || |* 2 | TABLE ACCESS FULL | DEPT | 1 | 1 | 1 |00:00:00.01 | 6 | | | || | 3 | VIEW PUSHED PREDICATE | | 1 | 1 | 1 |00:00:00.01 | 2 | | | || | 4 | SORT GROUP BY | | 1 | 1 | 1 |00:00:00.01 | 2 | 2048 | 2048 | 2048 (0)|| | 5 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 5 | 6 |00:00:00.01 | 2 | | | || |* 6 | INDEX RANGE SCAN | EMP_DEPTNO | 1 | 5 | 6 |00:00:00.01 | 1 | | | || ------------------------------------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 2 - filter("D"."LOC"='CHICAGO') | 6 - access("DEPTNO"="D"."DEPTNO") | |
    notion image
     

    튜닝63. 메인쿼리의 조건이 인라인뷰 안으로 파고 들어가게 하세요! -공무원 연금공단 튜닝 케이스(흔한 튜닝 케이스)

    📖

    메인쿼리 조인 조건 ↔ 인라인 뷰 관련 힌

    1. push_pred : 조인 조건을 in line view 안으로 파고 들어가게 하라
    1. no_push_pred : 조인 조건을 in line view 안으로 파고 들어가지 않게 하라

    실습

    --환경 구성-- @demo.sql alter table dept add constraint dept_pk primary key(deptno); -- pk제약을 걸어주면, dept테이블에 unique index 제약이 자동으로 생성됨 create index emp_x1 on emp(deptno, job); -- depno와 job 결합컬럼 인덱스 생성(deptno가 선두컬럼임) exec dbms_stats.gather_table_stats(user, 'emp'); exec dbms_stats.gather_table_stats(user, 'dept'); --emp와 dept의 통계정보 수집해준 것 <튜닝전 SQL> select /*+ gather_plan_statistics */ * from (select deptno, empno, ename, job, sal, sal * 1.1 sal2, hiredate from emp where job = 'CLERK' -- job이 검색조건에 있는 것을 눈여겨 보기 union all select deptno, empno, ename, job, sal, sal * 1.2 sal2, hiredate from emp where job = 'SALESMAN' ) v --결합컬럼 인덱스를 deptno를 선두컬럼으로 두었기 때문에 index 못 함 --결국 1번쿼리와 2번쿼리 모두 full table scan하게 됨. where v.deptno = 30; -- 따라서 이 조건을 파고들어가게 하면, -- deptno 덕분에 결합컬럼 인덱스를 탈 수 있게 됨!! <튜닝전 sql 실제 환경처럼 구성해서 돌리기 > select /*+ no_merge(v) full(v.e1) full(v.e2) */ * from (select deptno, empno, ename, job, sal, sal * 1.1 sal2, hiredate from emp e1 where job = 'CLERK' union all select deptno, empno, ename, job, sal, sal * 1.2 sal2, hiredate from emp e2 where job = 'SALESMAN' ) v where v.deptno = 30; DEPTNO|EMPNO|ENAME |JOB |SAL |SAL2|HIREDATE | ------+-----+------+--------+----+----+-----------------------+ 30| 7900|JAMES |CLERK | 950|1045|1981-12-11 00:00:00.000| 30| 7654|MARTIN|SALESMAN|1250|1500|1981-09-10 00:00:00.000| 30| 7499|ALLEN |SALESMAN|1600|1920|1981-02-11 00:00:00.000| 30| 7844|TURNER|SALESMAN|1500|1800|1981-08-21 00:00:00.000| 30| 7521|WARD |SALESMAN|1250|1500|1981-02-23 00:00:00.000| select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('69rnfa84uf4k2',0,'ALLSTATS LAST')); -------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 12 | | 1 | VIEW | | 1 | 4 | 5 |00:00:00.01 | 12 | | 2 | UNION-ALL | | 1 | | 5 |00:00:00.01 | 12 | |* 3 | TABLE ACCESS FULL| EMP | 1 | 2 | 1 |00:00:00.01 | 6 | |* 4 | TABLE ACCESS FULL| EMP | 1 | 2 | 4 |00:00:00.01 | 6 | -------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - filter(("JOB"='CLERK' AND "DEPTNO"=30)) 4 - filter(("JOB"='SALESMAN' AND "DEPTNO"=30))
    < 튜닝 후 sql > select /*+ no_merge(v) push_pred(v) */ * from (select deptno, empno, ename, job, sal, sal * 1.1 sal2, hiredate from emp e1 where job = 'CLERK' union all select deptno, empno, ename, job, sal, sal * 1.2 sal2, hiredate from emp e2 where job = 'SALESMAN' ) v where v.deptno = 30; --실행계획 확인하기 select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('fs2667ctkp1bg',0,'ALLSTATS LAST')); ----------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ----------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 4 || | 1 | VIEW | | 1 | 4 | 5 |00:00:00.01 | 4 || | 2 | UNION-ALL | | 1 | | 5 |00:00:00.01 | 4 || | 3 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 2 | 1 |00:00:00.01 | 2 || |* 4 | INDEX RANGE SCAN | EMP_X1 | 1 | 2 | 1 |00:00:00.01 | 1 || | 5 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 2 | 4 |00:00:00.01 | 2 || |* 6 | INDEX RANGE SCAN | EMP_X1 | 1 | 2 | 4 |00:00:00.01 | 1 || ----------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 4 - access("DEPTNO"=30 AND "JOB"='CLERK') | 6 - access("DEPTNO"=30 AND "JOB"='SALESMAN') |
    notion image
    조인 조건이 아니기 때문에 VIEW PUSHED PREDICATE라고 실행계획에 나타나지 않음! 하지만 파고든것 맞음!
     
     

    ■ SQL 재작성하여 튜닝하기

     
    ❓
    SQL을 재작성해서 튜닝해야하는 이유? 중급 SQL 튜너와 고급 SQL튜너의 차이라고 볼 수 있음

    SQL을 재작성해서 튜닝해야하는 이유?

    1. 힌트만으로는 튜닝이 안되는 SQL일 때
    1. 튜닝해야하는 SQL의 테이블들을 기존 테이블 말고 다른 테이블로 변경해도 같은 결과가 나오는 SQL
    1. 실행 결과는 같은데, SQL을 재작성해야 더 성능이 좋아지는 SQL이 있기 때문.

    튜닝64. SQL을 재작성해서 튜닝하세요 1.

    📖
    실습1) 아래의 SQL을 튜닝하시오
    < 튜닝전 > select decode(no,1, deptno, 2, null) as deptno, sum(sal) from emp e, (select rownum no from dual connect by level <= 2 ) d group by decode(no, 1, deptno, 2, null ) order by deptno; DEPTNO|SUM(SAL)| ------+--------+ 10| 8750| 20| 10875| 30| 9400| | 29025| -------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers -------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 7 | 1 | SORT GROUP BY | | 1 | 3 | 4 |00:00:00.01 | 7 | 2 | MERGE JOIN CARTESIAN | | 1 | 14 | 28 |00:00:00.01 | 7 | 3 | VIEW | | 1 | 1 | 2 |00:00:00.01 | 0 | 4 | COUNT | | 1 | | 2 |00:00:00.01 | 0 | 5 | CONNECT BY WITHOUT FILTERING| | 1 | | 2 |00:00:00.01 | 0 | 6 | FAST DUAL | | 1 | 1 | 1 |00:00:00.01 | 0 | 7 | BUFFER SORT | | 2 | 14 | 28 |00:00:00.01 | 7 | 8 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 7 --------------------------------------------------------------------------------------------------
    notion image
    < 튜닝후 > select deptno, sum(sal) from emp group by rollup(deptno) order by 1 asc; DEPTNO|SUM(SAL)| ------+--------+ 10| 8750| 20| 10875| 30| 9400| | 29025| select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('fkmvfh7c43aag',0,'ALLSTATS LAST')); ------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 6 | | | || | 1 | SORT GROUP BY ROLLUP| | 1 | 3 | 4 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)|| | 2 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || ------------------------------------------------------------------------------------------------------------------| | =========================== 21c부터는 rollup에 order by 기능이 빠졌으므로 order by를 넣어줘야 정렬됨.
    notion image

    문제풀며 익히기

     
    Q1. 아래의 튜닝 후 sql을 튜닝 전 sql로 변경하시오.
    < 튜닝후 > select job, sum(sal) from emp group by rollup(job) order by job asc; JOB |SUM(SAL)| ---------+--------+ ANALYST | 6000| CLERK | 4150| MANAGER | 8275| PRESIDENT| 5000| SALESMAN | 5600| | 29025|
    < 튜닝전 > select decode(no, 1, job, 2, null) as job, sum(sal) from emp e, (select rownum no from dual connect by level <= 2 ) d group by decode(no, 1, job, 2, null ) order by job; JOB |SUM(SAL)| ---------+--------+ ANALYST | 6000| CLERK | 4150| MANAGER | 8275| PRESIDENT| 5000| SALESMAN | 5600| | 29025|
    notion image

    튜닝65. SQL을 재작성해서 튜닝하세요 2.

    📖
    실습1)
    <튜닝전> select job, deptno, null as mgr, sum(sal) from emp group by job, deptno union all select null as job, deptno, mgr, sum(sal) from emp group by deptno, mgr ; JOB |DEPTNO|MGR |SUM(SAL)| ---------+------+----+--------+ PRESIDENT| 10| | 5000| MANAGER | 30| | 2850| MANAGER | 10| | 2450| MANAGER | 20| | 2975| SALESMAN | 30| | 5600| CLERK | 30| | 950| ANALYST | 20| | 6000| CLERK | 20| | 1900| CLERK | 10| | 1300| | 10| | 5000| | 30|7839| 2850| | 10|7839| 2450| | 20|7839| 2975| | 30|7698| 6550| | 20|7566| 6000| | 20|7902| 800| | 20|7788| 1100| | 10|7782| 1300| -------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 18 |00:00:00.01 | 12 | | 1 | UNION-ALL | | 1 | | 18 |00:00:00.01 | 12 | | 2 | HASH GROUP BY | | 1 | 9 | 9 |00:00:00.01 | 6 | | 3 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | 4 | HASH GROUP BY | | 1 | 13 | 9 |00:00:00.01 | 6 | | 5 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | -------------------------------------------------------------------------------------- 버퍼의 개수 : 12개
    • 위의 SQL의 문제점 : EMP테이블을 2번이나 엑세스 하는게 문제이다.
    <튜닝후> select job, deptno, mgr, sum(sal) from emp group by grouping sets((job, deptno), (deptno, mgr)); JOB |DEPTNO|MGR |SUM(SAL)| ---------+------+----+--------+ MANAGER | 20| | 2975| SALESMAN | 30| | 5600| CLERK | 30| | 950| ANALYST | 20| | 6000| CLERK | 20| | 1900| MANAGER | 10| | 2450| MANAGER | 30| | 2850| PRESIDENT| 10| | 5000| CLERK | 10| | 1300| | 10|7782| 1300| | 10| | 5000| | 30|7839| 2850| | 10|7839| 2450| | 20|7839| 2975| | 30|7698| 6550| | 20|7566| 6000| | 20|7902| 800| | 20|7788| 1100| ---------------------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ---------------------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 18 |00:00:00.01 | 8 || | 1 | TEMP TABLE TRANSFORMATION | | 1 | | 18 |00:00:00.01 | 8 || | 2 | LOAD AS SELECT (CURSOR DURATION MEMORY)| SYS_TEMP_0FD9D662B_10F875D | 1 | | 0 |00:00:00.01 | 7 || | 3 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 6 || | 4 | LOAD AS SELECT (CURSOR DURATION MEMORY)| SYS_TEMP_0FD9D662C_10F875D | 1 | | 0 |00:00:00.01 | 0 || | 5 | HASH GROUP BY | | 1 | 13 | 9 |00:00:00.01 | 0 || | 6 | TABLE ACCESS FULL | SYS_TEMP_0FD9D662B_10F875D | 1 | 14 | 14 |00:00:00.01 | 0 || | 7 | LOAD AS SELECT (CURSOR DURATION MEMORY)| SYS_TEMP_0FD9D662C_10F875D | 1 | | 0 |00:00:00.01 | 0 || | 8 | HASH GROUP BY | | 1 | 11 | 9 |00:00:00.01 | 0 || | 9 | TABLE ACCESS FULL | SYS_TEMP_0FD9D662B_10F875D | 1 | 14 | 14 |00:00:00.01 | 0 || | 10 | VIEW | | 1 | 13 | 18 |00:00:00.01 | 0 || | 11 | TABLE ACCESS FULL | SYS_TEMP_0FD9D662C_10F875D | 1 | 13 | 18 |00:00:00.01 | 0 || ---------------------------------------------------------------------------------------------------------------------------------| 버퍼의 개수 : 8개 |

    문제풀며 익히기

     
    Q1. 아래의 sql을 튜닝하시오.
    < 튜닝전 > select job, deptno, null as mgr, sum(sal) from emp group by job, deptno union all select null as job, deptno, mgr, sum(sal) from emp group by deptno, mgr union all select to_char(null) as job, to_number(null) as deptno, to_number(null) as mgr, sum(sal) from emp; JOB |DEPTNO|MGR |SUM(SAL)| ---------+------+----+--------+ PRESIDENT| 10| | 5000| MANAGER | 30| | 2850| MANAGER | 10| | 2450| MANAGER | 20| | 2975| SALESMAN | 30| | 5600| CLERK | 30| | 950| ANALYST | 20| | 6000| CLERK | 20| | 1900| CLERK | 10| | 1300| | 10| | 5000| | 30|7839| 2850| | 10|7839| 2450| | 20|7839| 2975| | 30|7698| 6550| | 20|7566| 6000| | 20|7902| 800| | 20|7788| 1100| | 10|7782| 1300| | | | 29025|
    < 튜닝후 > select job, deptno, mgr, sum(sal) from emp group by grouping sets( (job, deptno), (deptno, mgr), ()) order by job, deptno, mgr asc; JOB |DEPTNO|MGR |SUM(SAL)| ---------+------+----+--------+ ANALYST | 20| | 6000| CLERK | 10| | 1300| CLERK | 20| | 1900| CLERK | 30| | 950| MANAGER | 10| | 2450| MANAGER | 20| | 2975| MANAGER | 30| | 2850| PRESIDENT| 10| | 5000| SALESMAN | 30| | 5600| | 10|7782| 1300| | 10|7839| 2450| | 10| | 5000| | 20|7566| 6000| | 20|7788| 1100| | 20|7839| 2975| | 20|7902| 800| | 30|7698| 6550| | 30|7839| 2850| | | | 29025|
    notion image
     
     

    튜닝66. SQL을 재작성해서 튜닝하세요 3.

    📖
    실습1)
    <튜닝전> select empno, ename,sal, ( select sum(sal) from emp e where e.empno <= b.empno) sumsal from emp b order by empno; /* where e.empno <= b.empno 서브쿼리의 결과를 full scan해서 가지고 있기 때문에 아래와 같이 결과를 낼 수 있음 우변보다 작거나 같은 행을 더한다고 생각하면 이해가 편함 처음에는 7369 <= 7369 7369, 7499 <= 7499 7369, 7499, 7521 <= 7521 . . . */ EMPNO|ENAME |SAL |SUMSAL| -----+------+----+------+ 7369|SMITH | 800| 800| 7499|ALLEN |1600| 2400| 7521|WARD |1250| 3650| 7566|JONES |2975| 6625| 7654|MARTIN|1250| 7875| 7698|BLAKE |2850| 10725| 7782|CLARK |2450| 13175| 7788|SCOTT |3000| 16175| 7839|KING |5000| 21175| 7844|TURNER|1500| 22675| 7876|ADAMS |1100| 23775| 7900|JAMES | 950| 24725| 7902|FORD |3000| 27725| 7934|MILLER|1300| 29025| select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('fp1y2brv5ywg5',0,'ALLSTATS LAST')); ----------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ----------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 90 | | | || | 1 | SORT AGGREGATE | | 14 | 1 | 14 |00:00:00.01 | 84 | | | || |* 2 | TABLE ACCESS FULL| EMP | 14 | 1 | 105 |00:00:00.01 | 84 | | | || | 3 | SORT ORDER BY | | 1 | 14 | 14 |00:00:00.01 | 90 | 2048 | 2048 | 2048 (0)|| | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || ----------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 2 - filter("E"."EMPNO"<=:B1) | 버퍼의 개수 : 90 개
    스칼라 서브쿼리의 단점이 행의 수만큼 반복 수행한다는 것이다. 따라서 스칼라 서브쿼리는 남발하면 좋지 않음. 이를 해결하는 방법은 “캐슁”이라는 기능인데, 한 번 수행한 sql을 메모리에 올리는 것. 하지만 이 sql에서는 해결할 수 없음.
    notion image
    <튜닝후> select empno, ename, sal, sum(sal) over(order by empno asc) as sumsal from emp; EMPNO|ENAME |SAL |SUMSAL| -----+------+----+------+ 7369|SMITH | 800| 800| 7499|ALLEN |1600| 2400| 7521|WARD |1250| 3650| 7566|JONES |2975| 6625| 7654|MARTIN|1250| 7875| 7698|BLAKE |2850| 10725| 7782|CLARK |2450| 13175| 7788|SCOTT |3000| 16175| 7839|KING |5000| 21175| 7844|TURNER|1500| 22675| 7876|ADAMS |1100| 23775| 7900|JAMES | 950| 24725| 7902|FORD |3000| 27725| 7934|MILLER|1300| 29025| select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('dr3zp9gw1uaf0',0,'ALLSTATS LAST')); ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 6 | | | | | 1 | WINDOW SORT | | 1 | 14 | 14 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)| | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | | ---------------------------------------------------------------------------------------------------------------- 버퍼의 개수 : 6개
    notion image

    문제풀며 익히기

     
    Q1. 아래의 sql을 튜닝하시오.
    <튜닝전> select deptno, empno, ename, sal, (select sum(sal) from emp e where e.empno <= b.empno and e.deptno = b.deptno ) sumsal from emp b order by deptno, empno; DEPTNO|EMPNO|ENAME |SAL |SUMSAL ------+-----+------+----+------ 10| 7782|CLARK |2450| 2450 10| 7839|KING |5000| 7450 10| 7934|MILLER|1300| 8750 20| 7369|SMITH | 800| 800 20| 7566|JONES |2975| 3775 20| 7788|SCOTT |3000| 6775 20| 7876|ADAMS |1100| 7875 20| 7902|FORD |3000| 10875 30| 7499|ALLEN |1600| 1600 30| 7521|WARD |1250| 2850 30| 7654|MARTIN|1250| 4100 30| 7698|BLAKE |2850| 6950 30| 7844|TURNER|1500| 8450 30| 7900|JAMES | 950| 9400 select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('4z4b28bmp3ucy',0,'ALLSTATS LAST')); ----------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem ----------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 10 | | | | 1 | SORT AGGREGATE | | 14 | 1 | 14 |00:00:00.01 | 4 | | | |* 2 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 14 | 1 | 42 |00:00:00.01 | 4 | | | |* 3 | INDEX RANGE SCAN | EMP_X1 | 14 | 5 | 70 |00:00:00.01 | 3 | | | | 4 | SORT ORDER BY | | 1 | 14 | 14 |00:00:00.01 | 10 | 2048 | 2048 | 2048 (0) | 5 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | ----------------------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("E"."EMPNO"<=:B1) 3 - access("E"."DEPTNO"=:B1) 버퍼의 개수 : 10
    notion image
    <튜닝후> select deptno, empno, ename, sal, sum(sal) over(partition by deptno order by empno asc) as sumsal from emp ; DEPTNO|EMPNO|ENAME |SAL |SUMSAL| ------+-----+------+----+------+ 10| 7782|CLARK |2450| 2450| 10| 7839|KING |5000| 7450| 10| 7934|MILLER|1300| 8750| 20| 7369|SMITH | 800| 800| 20| 7566|JONES |2975| 3775| 20| 7788|SCOTT |3000| 6775| 20| 7876|ADAMS |1100| 7875| 20| 7902|FORD |3000| 10875| 30| 7499|ALLEN |1600| 1600| 30| 7521|WARD |1250| 2850| 30| 7654|MARTIN|1250| 4100| 30| 7698|BLAKE |2850| 6950| 30| 7844|TURNER|1500| 8450| 30| 7900|JAMES | 950| 9400| select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('3y7acuq0bbhb6',0,'ALLSTATS LAST')); ---------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ---------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 6 | | | | | 1 | WINDOW SORT | | 1 | 14 | 14 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)| | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | | ---------------------------------------------------------------------------------------------------------------- 버퍼의 개수 : 6
    notion image
    notion image
     
    Q2. 아래의 sql을 튜닝하시오
    <튜닝전> select deptno, empno, ename, sal, (select sum(sal) from emp e where e.empno <= b.empno and e.deptno = b.deptno ) sumsal from emp b where job in('CLERK', 'ANALYST') order by deptno, empno; DEPTNO|EMPNO|ENAME |SAL |SUMSAL| ------+-----+------+----+------+ 10| 7934|MILLER|1300| 8750| 20| 7369|SMITH | 800| 800| 20| 7788|SCOTT |3000| 6775| 20| 7876|ADAMS |1100| 7875| 20| 7902|FORD |3000| 10875| 30| 7900|JAMES | 950| 9400| 튜닝전 : select deptno, empno, ename,sal, ( select sum(sal) from emp e where e.empno <= b.empno and e.deptno = b.deptno and job in ('CLERK','ANALYST') ) sumsal from emp b where job in ('CLERK','ANALYST') order by deptno, empno; 스칼라 서브쿼리의 where 절에 job in ('CLERK','ANALYST') 조건을 걸면 차이가 없다
    <튜닝후> select deptno, empno, ename, sal, sum(sal) over ( partition by deptno order by empno asc) as sumsal from emp where job in ('CLERK', 'ANALYST'); DEPTNO|EMPNO|ENAME |SAL |SUMSAL| ------+-----+------+----+------+ 10| 7934|MILLER|1300| 1300| 20| 7369|SMITH | 800| 800| 20| 7788|SCOTT |3000| 3800| 20| 7876|ADAMS |1100| 4900| 20| 7902|FORD |3000| 7900| 30| 7900|JAMES | 950| 950| select deptno, empno, ename, sal, sumsal FROM (select deptno, job, empno, ename, sal, sum(sal) over ( partition by deptno order by empno asc) as sumsal from emp) e where job in ('CLERK', 'ANALYST'); DEPTNO|EMPNO|ENAME |SAL |SUMSAL| ------+-----+------+----+------+ 10|7934 |MILLER|1300| 8750| 20|7369 | SMITH|800 | 800| 20|7788 | SCOTT|3000| 6775| 20|7876 | ADAMS|1100| 7875| 20|7902 | FORD|3000| 10875| 30|7900 | JAMES|950 | 9400| 튜닝 전의 스칼라 쿼리는 job에 대한 검색 조건이 없어서 전체 직업에 대한 누적 급여 값을 반환한 후에 메인 쿼리의 검색 조건이 적용된 값이 출력되기 때문에 튜닝 후의 SQL과 다른 결과를 나타냅니다. 튜닝 전의 값과 같은 결과를 나타내면서 버퍼의 갯수를 줄이기 위해 in line view를 활용하는 방법도 있습니다. select deptno, empno, ename, sumsal from (select job, deptno, empno, ename, sum(sal) over( partition by deptno order by empno ) as sumsal from emp) where job in ('CLERK', 'ANALYST');

    튜닝67. SQL을 재작성해서 튜닝하세요 4.

    📖
    실습1)
    <튜닝전> select a.deptno , a.empno, a.ename, a.sal, b.sal ,no1, no2 from ( select rownum no1, deptno, empno,ename, sal --정렬시 깨지지 않기 위해 두개의 in line view한 듯 from ( select deptno, empno, ename, sal from emp order by deptno,sal) ) a, ( select rownum +1 no2,deptno, empno, ename, sal from ( select deptno, empno, ename, sal from emp order by deptno,sal) ) b where a.no1 = b.no2 (+) -- +1에 의해 rownum 1이 조회되지 않으니까 outer join sign 사용 order by no1; DEPTNO|EMPNO|ENAME |SAL |SAL | ------+-----+------+----+----+ 10| 7782|CLARK |2450| | 10| 7839|KING |5000|2450| 10| 7934|MILLER|1300|5000| 20| 7369|SMITH | 800|1300| 20| 7566|JONES |2975| 800| 20| 7788|SCOTT |3000|2975| 20| 7876|ADAMS |1100|3000| 20| 7902|FORD |3000|1100| 30| 7499|ALLEN |1600|3000| 30| 7521|WARD |1250|1600| 30| 7654|MARTIN|1250|1250| 30| 7698|BLAKE |2850|1250| 30| 7844|TURNER|1500|2850| 30| 7900|JAMES | 950|1500| ---------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ---------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 14 | | | || | 1 | SORT ORDER BY | | 1 | 14 | 14 |00:00:00.01 | 14 | 2048 | 2048 | 2048 (0)|| |* 2 | HASH JOIN OUTER | | 1 | 14 | 14 |00:00:00.01 | 14 | 1376K| 1376K| 1035K (0)|| | 3 | VIEW | | 1 | 14 | 14 |00:00:00.01 | 7 | | | || | 4 | COUNT | | 1 | | 14 |00:00:00.01 | 7 | | | || | 5 | VIEW | | 1 | 14 | 14 |00:00:00.01 | 7 | | | || | 6 | SORT ORDER BY | | 1 | 14 | 14 |00:00:00.01 | 7 | 2048 | 2048 | 2048 (0)|| | 7 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 7 | | | || | 8 | VIEW | | 1 | 14 | 14 |00:00:00.01 | 7 | | | || | 9 | COUNT | | 1 | | 14 |00:00:00.01 | 7 | | | || | 10 | VIEW | | 1 | 14 | 14 |00:00:00.01 | 7 | | | || | 11 | SORT ORDER BY | | 1 | 14 | 14 |00:00:00.01 | 7 | 2048 | 2048 | 2048 (0)|| | 12 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 7 | | | || ---------------------------------------------------------------------------------------------------------------------|
    notion image
    <튜닝후> select deptno, empno, ename, sal, lag(sal,1) over (order by deptno, sal) as sal_lag from emp; DEPTNO|EMPNO|ENAME |SAL |SAL_LAG| ------+-----+------+----+-------+ 10| 7782|CLARK |2450| | 10| 7839|KING |5000| 2450| 10| 7934|MILLER|1300| 5000| 20| 7369|SMITH | 800| 1300| 20| 7566|JONES |2975| 800| 20| 7788|SCOTT |3000| 2975| 20| 7876|ADAMS |1100| 3000| 20| 7902|FORD |3000| 1100| 30| 7499|ALLEN |1600| 3000| 30| 7521|WARD |1250| 1600| 30| 7654|MARTIN|1250| 1250| 30| 7698|BLAKE |2850| 1250| 30| 7844|TURNER|1500| 2850| 30| 7900|JAMES | 950| 1500| ----------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ----------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 6 | | | || | 1 | WINDOW SORT | | 1 | 14 | 14 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)|| | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || ----------------------------------------------------------------------------------------------------------------|
    notion image

    문제풀며 익히기

     
    Q1. 아래의 sql을 튜닝하시오
    select a.deptno , a.empno, a.ename, a.sal, b.sal, no1, no2 from ( select rownum no1, deptno, empno, ename, sal from ( select deptno, empno, ename, sal from emp order by deptno,empno ) ) a, ( select rownum -1 no2,deptno, empno, ename, sal from ( select deptno, empno, ename, sal from emp order by deptno,empno ) ) b where a.no1 = b.no2 (+) order by no2; DEPTNO|EMPNO|ENAME |SAL |SAL |NO1|NO2| ------+-----+------+----+----+---+---+ 10| 7782|CLARK |2450|5000| 1| 1| 10| 7839|KING |5000|1300| 2| 2| 10| 7934|MILLER|1300| 800| 3| 3| 20| 7369|SMITH | 800|2975| 4| 4| 20| 7566|JONES |2975|3000| 5| 5| 20| 7788|SCOTT |3000|1100| 6| 6| 20| 7876|ADAMS |1100|3000| 7| 7| 20| 7902|FORD |3000|1600| 8| 8| 30| 7499|ALLEN |1600|1250| 9| 9| 30| 7521|WARD |1250|1250| 10| 10| 30| 7654|MARTIN|1250|2850| 11| 11| 30| 7698|BLAKE |2850|1500| 12| 12| 30| 7844|TURNER|1500| 950| 13| 13| 30| 7900|JAMES | 950| | 14| |
    <튜닝후> select deptno, empno, ename, sal, lead(sal,1) over (order by deptno, empno) as sal_lead from emp; DEPTNO|EMPNO|ENAME |SAL |SAL_LEAD| ------+-----+------+----+--------+ 10| 7782|CLARK |2450| 5000| 10| 7839|KING |5000| 1300| 10| 7934|MILLER|1300| 800| 20| 7369|SMITH | 800| 2975| 20| 7566|JONES |2975| 3000| 20| 7788|SCOTT |3000| 1100| 20| 7876|ADAMS |1100| 3000| 20| 7902|FORD |3000| 1600| 30| 7499|ALLEN |1600| 1250| 30| 7521|WARD |1250| 1250| 30| 7654|MARTIN|1250| 2850| 30| 7698|BLAKE |2850| 1500| 30| 7844|TURNER|1500| 950| 30| 7900|JAMES | 950| |
     

    튜닝68. MAX를 RANK로 변환해서 SQL을 튜닝하세요.

    📖

    MAX ↔ RANK로 변환

     
    실습1)
    직업 별 최대 월급을 받는 사원의 사원번호, 사원이름, 직업, 월급을 출력하시오!
    <튜닝전> select e.empno, e.ename, e2.job, e2.max_sal from emp e, ( select job, max(sal) as max_sal from emp group by job ) e2 where e.job = e2.job and e.sal = e2.max_sal order by e.empno; EMPNO|ENAME |JOB |MAX_SAL| -----+------+---------+-------+ 7499|ALLEN |SALESMAN | 1600| 7566|JONES |MANAGER | 2975| 7788|SCOTT |ANALYST | 3000| 7839|KING |PRESIDENT| 5000| 7902|FORD |ANALYST | 3000| 7934|MILLER|CLERK | 1300| ------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 6 |00:00:00.01 | 12 | | | || |* 1 | HASH JOIN | | 1 | 5 | 6 |00:00:00.01 | 12 | 1744K| 1744K| 1067K (0)|| | 2 | VIEW | | 1 | 5 | 5 |00:00:00.01 | 6 | | | || | 3 | HASH GROUP BY | | 1 | 5 | 5 |00:00:00.01 | 6 | 1323K| 1323K| || | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || | 5 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || ------------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 1 - access("E"."JOB"="E2"."JOB" AND "E"."SAL"="E2"."MAX_SAL") 버퍼의 개수 : 12 | |
    • 위의 sql 진단 :
      • emp table을 2번 access한 것이 첫 번째 문제 그러면서 조인까지 했음. 하나의 테이블을 읽을 때 보다 2개의 테이블 조인을 하는 것이 더 버퍼 많이 사용함.
    • 해결방법 :
      • emp테이블을 한 번만 엑세스 하게 튜닝해야함. 그리고 조인을 하지 않게 해야함. 즉, 조인문장으로 작성하지 않습니다.
    notion image
    인라인 뷰를 보면 모든 직업을 다 찾아서 GROUP BY해서 직업별 최대월급을 찾은 것이기 때문에 EMP TABLE을 FULL SCAN해서 전체범위처리를 해버린것. 그리고 EMP E와 조인을 했음. ⇒ 따라서 악성 SQL.
    그래서 EMP TABLE 을 한 번만 ACCESS하도록 튜닝해야함.
    <튜닝후> select empno, ename, job, sal from ( select empno, ename, job, sal, rank() over ( partition by job order by sal desc ) as rnk from emp ) where rnk = 1 order by empno; --서브쿼리만 수행했을 때 결과 select empno, ename, job, sal, rank() over(partition by job order by sal desc) as rnk from emp ; /* select empno, ename, job, sal, rank() over(partition by job order by sal desc) as rnk from emp where rnk = 1; --> 실행 순서에 의해 이렇게 사용할 수 없기 때문에 인라인뷰로 만들어 준 것 */ EMPNO|ENAME |JOB |SAL |RNK| -----+------+---------+----+---+ 7788|SCOTT |ANALYST |3000| 1| 7902|FORD |ANALYST |3000| 1| 7934|MILLER|CLERK |1300| 1| 7876|ADAMS |CLERK |1100| 2| 7900|JAMES |CLERK | 950| 3| 7369|SMITH |CLERK | 800| 4| 7566|JONES |MANAGER |2975| 1| 7698|BLAKE |MANAGER |2850| 2| 7782|CLARK |MANAGER |2450| 3| 7839|KING |PRESIDENT|5000| 1| 7499|ALLEN |SALESMAN |1600| 1| 7844|TURNER|SALESMAN |1500| 2| 7654|MARTIN|SALESMAN |1250| 3| 7521|WARD |SALESMAN |1250| 3| -- 결과 EMPNO|ENAME |JOB |SAL | -----+------+---------+----+ 7499|ALLEN |SALESMAN |1600| 7566|JONES |MANAGER |2975| 7788|SCOTT |ANALYST |3000| 7839|KING |PRESIDENT|5000| 7902|FORD |ANALYST |3000| 7934|MILLER|CLERK |1300| -----------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || -----------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 6 |00:00:00.01 | 7 | | | || | 1 | SORT ORDER BY | | 1 | 14 | 6 |00:00:00.01 | 7 | 2048 | 2048 | 2048 (0)|| |* 2 | VIEW | | 1 | 14 | 6 |00:00:00.01 | 7 | | | || |* 3 | WINDOW SORT PUSHED RANK| | 1 | 14 | 6 |00:00:00.01 | 7 | 2048 | 2048 | 2048 (0)|| | 4 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 7 | | | || -----------------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 2 - filter("RNK"=1) | 3 - filter(RANK() OVER ( PARTITION BY "JOB" ORDER BY INTERNAL_FUNCTION("SAL") DESC )<=1) | |
    notion image

    문제풀며 익히기

     
    Q1. 아래의 sql을 튜닝하시오.
    <튜닝전> select e.empno, e.ename, e2.deptno, e2.max_sal from emp e, (select deptno, max(sal) as max_sal from emp group by deptno ) e2 where e.deptno = e2.deptno and e.sal = e2.max_sal order by e.empno; EMPNO|ENAME|DEPTNO|MAX_SAL| -----+-----+------+-------+ 7698|BLAKE| 30| 2850| 7788|SCOTT| 20| 3000| 7839|KING | 10| 5000| 7902|FORD | 20| 3000| ------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 13 | | | || |* 1 | FILTER | | 1 | | 4 |00:00:00.01 | 13 | | | || | 2 | SORT GROUP BY | | 1 | 65 | 14 |00:00:00.01 | 13 | 2048 | 2048 | 2048 (0)|| |* 3 | HASH JOIN | | 1 | 65 | 70 |00:00:00.01 | 13 | 1236K| 1236K| 705K (0)|| | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || | 5 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || ------------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 1 - filter("E"."SAL"=MAX("SAL")) | 3 - access("E"."DEPTNO"="DEPTNO") | |
    notion image
    <튜닝후> select empno, ename, deptno, sal from (select empno, ename, deptno, sal, rank() over(partition by deptno order by sal desc) as rnk from emp ) where rnk = 1 order by empno; EMPNO|ENAME|DEPTNO|SAL | -----+-----+------+----+ 7698|BLAKE| 30|2850| 7788|SCOTT| 20|3000| 7839|KING | 10|5000| 7902|FORD | 20|3000| ----------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | ----------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 4 |00:00:00.01 | 7 | | | | | 1 | SORT ORDER BY | | 1 | 14 | 4 |00:00:00.01 | 7 | 2048 | 2048 | 2048 (0)| |* 2 | VIEW | | 1 | 14 | 4 |00:00:00.01 | 7 | | | | |* 3 | WINDOW SORT PUSHED RANK| | 1 | 14 | 4 |00:00:00.01 | 7 | 2048 | 2048 | 2048 (0)| | 4 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 7 | | | | ----------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("RNK"=1) 3 - filter(RANK() OVER ( PARTITION BY "DEPTNO" ORDER BY INTERNAL_FUNCTION("SAL") DESC )<=1)
    notion image
     
     

    튜닝69. 인라인뷰 사용 쿼리문을 데이터 분석함수로 변경해서 튜닝하세요.

    📖

    인라인뷰 사용 쿼리문을 데이터 분석함수로 변경

    예제1)
    부서번호, 이름, 월급, 자기가 속한 부서번호의 평균월급을 출력하는데,
    자기의 월급이 자기가 속한 부서번호의 평균월급보다 더 큰 사원들만 출력하시오.
    <튜닝전> select e.deptno, e.ename, e.sal, v.avgsal from emp e, (select deptno, avg(sal) as avgsal from emp group by deptno ) v where e.deptno = v.deptno and e.sal > v.avgsal order by 1 asc; DEPTNO|ENAME|SAL |AVGSAL | ------+-----+----+-----------------------------------------+ 10|KING |5000|2916.666666666666666666666666666666666667| 20|JONES|2975| 2175| 20|FORD |3000| 2175| 20|SCOTT|3000| 2175| 30|ALLEN|1600|1566.666666666666666666666666666666666667| 30|BLAKE|2850|1566.666666666666666666666666666666666667| select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('dt44y7h11agh8',0,'ALLSTATS LAST')); ------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 6 |00:00:00.01 | 13 | | | || |* 1 | FILTER | | 1 | | 6 |00:00:00.01 | 13 | | | || | 2 | SORT GROUP BY | | 1 | 4 | 14 |00:00:00.01 | 13 | 2048 | 2048 | 2048 (0)|| |* 3 | HASH JOIN | | 1 | 65 | 70 |00:00:00.01 | 13 | 1316K| 1316K| 1017K (0)|| | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || | 5 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || ------------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 1 - filter("E"."SAL">SUM("SAL")/COUNT("SAL")) | 3 - access("E"."DEPTNO"="DEPTNO") 버퍼의 개수 : 13개 |
    • SQL 진단
        1. EMP 테이블을 2번 엑세스 함
        1. 조인을 해서 결과를 출력했음
    notion image
    <튜닝후> select deptno, ename, sal, avgsal from (select deptno, ename, sal, avg(sal) over(partition by deptno) as avgsal from emp ) where sal > avgsal; DEPTNO|ENAME|SAL |AVGSAL | ------+-----+----+-----------------------------------------+ 10|KING |5000|2916.666666666666666666666666666666666667| 20|SCOTT|3000| 2175| 20|FORD |3000| 2175| 20|JONES|2975| 2175| 30|ALLEN|1600|1566.666666666666666666666666666666666667| 30|BLAKE|2850|1566.666666666666666666666666666666666667| select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('0h1wu19qxqc52',0,'ALLSTATS LAST')); -----------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || -----------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 6 |00:00:00.01 | 6 | | | || |* 1 | VIEW | | 1 | 14 | 6 |00:00:00.01 | 6 | | | || | 2 | WINDOW SORT | | 1 | 14 | 14 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)|| | 3 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || -----------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 1 - filter("SAL">"AVGSAL") 버퍼의 개수 : 6개 | |
    notion image

    문제풀며 익히기

     
    Q1.
    직업, 이름, 월급, 직업별 평균월급을 출력하는데, 자기의 월급이 자기가 속한 직업의 평균 월급보다 더 작은 사원들만 출력하는데, 튜닝된 sql로 작성하시오.
    select job, ename, sal, avgsal from ( select job, ename, sal, avg(sal) over(partition by job) as avgsal from emp ) where sal < avgsal; JOB |ENAME |SAL |AVGSAL | --------+------+----+-----------------------------------------+ CLERK |SMITH | 800| 1037.5| CLERK |JAMES | 950| 1037.5| MANAGER |CLARK |2450|2758.333333333333333333333333333333333333| SALESMAN|WARD |1250| 1400| SALESMAN|MARTIN|1250| 1400| -----------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || -----------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 5 |00:00:00.01 | 6 | | | || |* 1 | VIEW | | 1 | 14 | 5 |00:00:00.01 | 6 | | | || | 2 | WINDOW SORT | | 1 | 14 | 14 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)|| | 3 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || -----------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 1 - filter("SAL"<"AVGSAL") | |
    notion image
     
     
     

    튜닝70. 스칼라 서브쿼리를 분석함수로 튜닝하세요!

    📖
    예제1) (SQLP 주관식 문제로 자주 출제)
    이름, 월급, 사원테이블 전체의 토탈월급을 출력하시오.
    <튜닝전> select ename, sal, (select sum(sal) from emp) as sumsal from emp; ENAME |SAL |SUMSAL| ------+----+------+ KING |5000| 29025| BLAKE |2850| 29025| CLARK |2450| 29025| JONES |2975| 29025| MARTIN|1250| 29025| ALLEN |1600| 29025| TURNER|1500| 29025| JAMES | 950| 29025| WARD |1250| 29025| FORD |3000| 29025| SMITH | 800| 29025| SCOTT |3000| 29025| ADAMS |1100| 29025| MILLER|1300| 29025| 문제점 : 서브쿼리 사용으로 서브쿼리의 select가 emp table이 14건이라서 14번 수행됨.
    • 해결방법 : 메모리 캐싱(스칼라 서브쿼리를 메모리 캐싱)
      • 처음 select 하면 → PGA(오라클 메모리)에 올림 → 다음 SELECT부터는 올려놓은 데이터를 가져와서 PASING과 EXECUTE를 하지 않게 함
        즉, 스칼라 서브쿼리를 부하를 줄이기 위해서 메모리 캐싱을 합니다 그럼에도 불구하고 EMP 테이블을 2번 엑세스하는 SQL이므로 EMP 테이블을 1번만 엑세스되게 해줘야함
    <튜닝후> select ename, sal, sum(sal) over() as sumsal from emp; --over 뒤의 괄호 안에 아무것도 작성하지 안으면, 전체토탈월급이 나오는 것 ENAME |SAL |SUMSAL| ------+----+------+ KING |5000| 29025| BLAKE |2850| 29025| CLARK |2450| 29025| JONES |2975| 29025| MARTIN|1250| 29025| ALLEN |1600| 29025| TURNER|1500| 29025| JAMES | 950| 29025| WARD |1250| 29025| FORD |3000| 29025| SMITH | 800| 29025| SCOTT |3000| 29025| ADAMS |1100| 29025| MILLER|1300| 29025|
     

    문제풀며 익히기

     
    Q1. 아래의 sql을 튜닝하시오
    <튜닝전> select deptno, ename, sal, ( select sum(sal) from emp s where s.deptno = e.deptno ) as sumsal from emp e order by deptno asc; DEPTNO|ENAME |SAL |SUMSAL| ------+------+----+------+ 10|KING |5000| 8750| 10|CLARK |2450| 8750| 10|MILLER|1300| 8750| --> king의 월급을 뽑아서 스칼라 서브쿼리의 연결조건 e.deptno에 넣음 --> 메모리의 빈 공간에 올려 놓음 -->(메모리의 공간이 부족하면, pashing 충돌 발생 즉, 먼저 올라간게 밀려나는 현상(?)) --> 따라서 clark과 miller는 캐싱 데이터를 가져오게 됨 20|JONES |2975| 10875| 20|FORD |3000| 10875| 20|SMITH | 800| 10875| 20|SCOTT |3000| 10875| 20|ADAMS |1100| 10875| --> jones의 월급을 뽑아서 스칼라 서브쿼리의 e.deptno연결조건에 넣음 --> 메모리의 빈 공간에 올려놓음 ... 반복 30|BLAKE |2850| 9400| 30|MARTIN|1250| 9400| 30|ALLEN |1600| 9400| 30|TURNER|1500| 9400| 30|JAMES | 950| 9400| 30|WARD |1250| 9400| -----------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || -----------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 12 | | | || | 1 | SORT GROUP BY | | 1 | 65 | 14 |00:00:00.01 | 12 | 2048 | 2048 | 2048 (0)|| |* 2 | HASH JOIN OUTER | | 1 | 65 | 70 |00:00:00.01 | 12 | 1316K| 1316K| 972K (0)|| | 3 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || -----------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 2 - access("S"."DEPTNO"="E"."DEPTNO") | |
    notion image
    notion image
    <튜닝후> select deptno, ename, sal, sum(sal) over(partition by deptno) as sumsal from emp; DEPTNO|ENAME |SAL |SUMSAL| ------+------+----+------+ 10|KING |5000| 8750| 10|CLARK |2450| 8750| 10|MILLER|1300| 8750| 20|ADAMS |1100| 10875| 20|SCOTT |3000| 10875| 20|SMITH | 800| 10875| 20|FORD |3000| 10875| 20|JONES |2975| 10875| 30|WARD |1250| 9400| 30|JAMES | 950| 9400| 30|ALLEN |1600| 9400| 30|MARTIN|1250| 9400| 30|BLAKE |2850| 9400| 30|TURNER|1500| 9400| ----------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ----------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 6 | | | || | 1 | WINDOW SORT | | 1 | 14 | 14 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)|| | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || ----------------------------------------------------------------------------------------------------------------|
    notion image
     

    튜닝71. 스칼라 서브쿼리의 특징을 알아야 튜닝할 수 있어요. - sqlp

    📖
    예제1)
    이름, 월급, 사원 테이블 전체의 토탈월급, 사원테이블 전체의 최대월급, 사원테이블 전체의 최소월급을 출력하시오.
    <튜닝전> select ename, sal, (select sum(sal) from emp ) as sumsal, (select max(sal) from emp ) as maxsal, (select min(sal) from emp ) as minsal from emp; ENAME |SAL |SUMSAL|MAXSAL|MINSAL| ------+----+------+------+------+ KING |5000| 29025| 5000| 800| BLAKE |2850| 29025| 5000| 800| CLARK |2450| 29025| 5000| 800| JONES |2975| 29025| 5000| 800| MARTIN|1250| 29025| 5000| 800| ALLEN |1600| 29025| 5000| 800| TURNER|1500| 29025| 5000| 800| JAMES | 950| 29025| 5000| 800| WARD |1250| 29025| 5000| 800| FORD |3000| 29025| 5000| 800| SMITH | 800| 29025| 5000| 800| SCOTT |3000| 29025| 5000| 800| ADAMS |1100| 29025| 5000| 800| MILLER|1300| 29025| 5000| 800| ------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 7 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 6 | | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | 3 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 6 | | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | 5 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 6 | | 6 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | 7 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 7 | -------------------------------------------------------------------------------------
    • 문제점 : emp table을 4번이나 select 하고 있다는 문제 발생
    notion image
    <튜닝후> select ename, sal, sum(sal) over() as sumsal, max(sal) over() as maxsal, min(sal) over() as minsal from emp; ENAME |SAL |SUMSAL|MAXSAL|MINSAL| ------+----+------+------+------+ KING |5000| 29025| 5000| 800| BLAKE |2850| 29025| 5000| 800| CLARK |2450| 29025| 5000| 800| JONES |2975| 29025| 5000| 800| MARTIN|1250| 29025| 5000| 800| ALLEN |1600| 29025| 5000| 800| TURNER|1500| 29025| 5000| 800| JAMES | 950| 29025| 5000| 800| WARD |1250| 29025| 5000| 800| FORD |3000| 29025| 5000| 800| SMITH | 800| 29025| 5000| 800| SCOTT |3000| 29025| 5000| 800| ADAMS |1100| 29025| 5000| 800| MILLER|1300| 29025| 5000| 800| | ----------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ----------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 6 | | | || | 1 | WINDOW BUFFER | | 1 | 14 | 14 |00:00:00.01 | 6 | 2048 | 2048 | 2048 (0)|| | 2 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 6 | | | || ----------------------------------------------------------------------------------------------------------------| |
    notion image
     
    예제2) 아래의 sql을 튜닝하시오.
    <튜닝전> select ename, sal, (select sum(sal) from emp where job = 'SALESMAN' ) as sumsal, (select max(sal) from emp where job = 'SALESMAN' ) as maxsal, (select min(sal) from emp where job = 'SALESMAN') as minsal from emp; ENAME |SAL |SUMSAL|MAXSAL|MINSAL| ------+----+------+------+------+ KING |5000| 5600| 1600| 1250| BLAKE |2850| 5600| 1600| 1250| CLARK |2450| 5600| 1600| 1250| JONES |2975| 5600| 1600| 1250| MARTIN|1250| 5600| 1600| 1250| ALLEN |1600| 5600| 1600| 1250| TURNER|1500| 5600| 1600| 1250| JAMES | 950| 5600| 1600| 1250| WARD |1250| 5600| 1600| 1250| FORD |3000| 5600| 1600| 1250| SMITH | 800| 5600| 1600| 1250| SCOTT |3000| 5600| 1600| 1250| ADAMS |1100| 5600| 1600| 1250| MILLER|1300| 5600| 1600| 1250| ---------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ---------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 14 |00:00:00.01 | 7 || | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 2 || | 2 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 3 | 4 |00:00:00.01 | 2 || |* 3 | INDEX SKIP SCAN | EMP_X1 | 1 | 3 | 4 |00:00:00.01 | 1 || | 4 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 2 || | 5 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 3 | 4 |00:00:00.01 | 2 || |* 6 | INDEX SKIP SCAN | EMP_X1 | 1 | 3 | 4 |00:00:00.01 | 1 || | 7 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 2 || | 8 | TABLE ACCESS BY INDEX ROWID BATCHED| EMP | 1 | 3 | 4 |00:00:00.01 | 2 || |* 9 | INDEX SKIP SCAN | EMP_X1 | 1 | 3 | 4 |00:00:00.01 | 1 || | 10 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 7 || ---------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 3 - access("JOB"='SALESMAN') | filter("JOB"='SALESMAN') | 6 - access("JOB"='SALESMAN') | filter("JOB"='SALESMAN') | 9 - access("JOB"='SALESMAN') | filter("JOB"='SALESMAN') | |
    notion image
    < 튜닝후 : 위의 SQL을 데이터 분석 함수로 사용하기 어렵기 때문에 아래와 같이 함 > #1. 연결연산자 사용으로 연결 select ename, sal, (select sum(sal) || max(sal) || min(sal) from emp where job = 'SALESMAN') AS total from emp; ENAME |SAL |TOTAL | ------+----+------------+ KING |5000|560016001250| BLAKE |2850|560016001250| CLARK |2450|560016001250| JONES |2975|560016001250| MARTIN|1250|560016001250| ALLEN |1600|560016001250| TURNER|1500|560016001250| JAMES | 950|560016001250| WARD |1250|560016001250| FORD |3000|560016001250| SMITH | 800|560016001250| SCOTT |3000|560016001250| ADAMS |1100|560016001250| MILLER|1300|560016001250| !! 참고 !! 스칼라 서브쿼리는 딱 한개의 컬럼 값만 return 즉, 출력할 수 있기 때문에 아래와 같이 쉼표를 사용하면 아래와 같은 에러 발생 따라서 연결연산자를 사용해야함. !!ERROR!! SQL Error [913] [42000]: ORA-00913: 값의 수가 너무 많습니다 select ename, sal, (select sum(sal) , max(sal) , min(sal) from emp where job = 'SALESMAN') AS total from emp; ========================================================= #2. rpad를 사용하여 10자리씩 공백을 채워넣고 구분하여 원하는 값 잘라내기 select ename, sal, substr(total,1,10) as sumsal, --월급합에 해당하는 것 잘라와보기 substr(total,11,10) as maxsal, substr(total,21,10) as minsal from ( select ename, sal, (select rpad(sum(sal), 10, ' ') || rpad(max(sal), 10, ' ') || rpad(min(sal), 10, ' ') from emp where job = 'SALESMAN') AS total from emp ) ; ENAME |SAL |SUMSAL |MAXSAL |MINSAL | ------+----+----------+----------+----------+ KING |5000|5600 |1600 |1250 | BLAKE |2850|5600 |1600 |1250 | CLARK |2450|5600 |1600 |1250 | JONES |2975|5600 |1600 |1250 | MARTIN|1250|5600 |1600 |1250 | ALLEN |1600|5600 |1600 |1250 | TURNER|1500|5600 |1600 |1250 | JAMES | 950|5600 |1600 |1250 | WARD |1250|5600 |1600 |1250 | FORD |3000|5600 |1600 |1250 | SMITH | 800|5600 |1600 |1250 | SCOTT |3000|5600 |1600 |1250 | ADAMS |1100|5600 |1600 |1250 | MILLER|1300|5600 |1600 |1250 |
    notion image
    notion image
     
     
     
     

    문제풀며 익히기

     
     
     
    Q1.

    튜닝72. 스칼라 서브쿼리를 데이터 분석함수로 변경하세요.

    📖
    예제1) — FIRST VALUE() OVER()
    부서번호, 사원이름, 월급, 자기가 속한 부서번호의 가장 월급을 적게 받는 사원의 이름을 출력하시오.
    <튜닝전> select deptno, ename, sal, (select ename from emp s where s.deptno = e.deptno order by s.sal asc fetch first 1 rows only) as low_sal_ename from emp e order by deptno asc; DEPTNO|ENAME |SAL |LOW_SAL_ENAME| ------+------+----+-------------+ 10|KING |5000|MILLER | 10|CLARK |2450|MILLER | 10|MILLER|1300|MILLER | 20|ADAMS |1100|SMITH | 20|SCOTT |3000|SMITH | 20|SMITH | 800|SMITH | 20|FORD |3000|SMITH | 20|JONES |2975|SMITH | 30|WARD |1250|JAMES | 30|JAMES | 950|JAMES | 30|ALLEN |1600|JAMES | 30|MARTIN|1250|JAMES | 30|BLAKE |2850|JAMES | 30|TURNER|1500|JAMES |
    notion image
    <튜닝후> select deptno, ename, sal, first_value(ename) over (partition by deptno order by sal asc) low_sal_ename from emp; 부서번호별로 partition을 나누고, 월급을 asc하게 정렬한 후 first_value 즉, 처음의 값만 추출 DEPTNO|ENAME |SAL |LOW_SAL_ENAME| ------+------+----+-------------+ 10|MILLER|1300|MILLER | 10|CLARK |2450|MILLER | 10|KING |5000|MILLER | 20|SMITH | 800|SMITH | 20|ADAMS |1100|SMITH | 20|JONES |2975|SMITH | 20|FORD |3000|SMITH | 20|SCOTT |3000|SMITH | 30|JAMES | 950|JAMES | 30|MARTIN|1250|JAMES | 30|WARD |1250|JAMES | 30|TURNER|1500|JAMES | 30|ALLEN |1600|JAMES | 30|BLAKE |2850|JAMES |
    notion image

    문제풀며 익히기

     
    Q1.
    다음의 sql을 튜닝하시오.
    <튜닝전> select deptno, ename, sal, (select ename from emp s where s.deptno = e.deptno order by s.sal desc fetch first 1 rows only) as high_sal_ename from emp e order by deptno asc; DEPTNO|ENAME |SAL |HIGH_SAL_ENAME| ------+------+----+-------------+ 10|KING |5000|KING | 10|CLARK |2450|KING | 10|MILLER|1300|KING | 20|ADAMS |1100|FORD | 20|SCOTT |3000|FORD | 20|SMITH | 800|FORD | 20|FORD |3000|FORD | 20|JONES |2975|FORD | 30|WARD |1250|BLAKE | 30|JAMES | 950|BLAKE | 30|ALLEN |1600|BLAKE | 30|MARTIN|1250|BLAKE | 30|BLAKE |2850|BLAKE | 30|TURNER|1500|BLAKE |
    <튜닝후> select deptno, ename, sal, first_value(ename) over (partition by deptno order by sal desc) low_sal_ename from emp; --last_value : 아래의 sql은 원하는 결과가 나오지 않음-- --왜? defualt값이 처음행부터 현재행이기 때문이다. -- select deptno, ename, sal, last_value(ename) over (partition by deptno order by sal asc) high_sal_ename from emp; DEPTNO|ENAME |SAL |HIGH_SAL_ENAME| ------+------+----+-------------+ 10|MILLER|1300|MILLER | 10|CLARK |2450|CLARK | 10|KING |5000|KING | 20|SMITH | 800|SMITH | 20|ADAMS |1100|ADAMS | 20|JONES |2975|JONES | 20|FORD |3000|SCOTT | 20|SCOTT |3000|SCOTT | 30|JAMES | 950|JAMES | 30|MARTIN|1250|WARD | 30|WARD |1250|WARD | 30|TURNER|1500|TURNER | 30|ALLEN |1600|ALLEN | 30|BLAKE |2850|BLAKE | --따라서 rows between을 생략하지 않고 정확히 해줘야함-- select deptno, ename, sal, last_value(ename) over (partition by deptno order by sal asc rows between unbounded preceding and unbounded following) as high_sal_ename from emp; DEPTNO|ENAME |SAL |HIGH_SAL_ENAME| ------+------+----+-------------+ 10|MILLER|1300|KING | 10|CLARK |2450|KING | 10|KING |5000|KING | 20|SMITH | 800|FORD | 20|ADAMS |1100|FORD | 20|JONES |2975|FORD | 20|SCOTT |3000|FORD | 20|FORD |3000|FORD | 30|JAMES | 950|BLAKE | 30|WARD |1250|BLAKE | 30|MARTIN|1250|BLAKE | 30|TURNER|1500|BLAKE | 30|ALLEN |1600|BLAKE | 30|BLAKE |2850|BLAKE | 설명: unbounded preceding : 첫번째행 unbounded following : 마지막 행
    fist_value와 last_value의 차이점
    질문:
    sql에서, first_value와 last_value의 차이점을 알려줘
    chat gpt 답변:
    FIRST_VALUE와 LAST_VALUE는 SQL에서 윈도우 함수(window function) 중 일부입니다. 이 두 함수는 윈도우(특정한 범위 내의 행들)에서 각각 첫 번째 값과 마지막 값을 가져오는 데 사용됩니다. 하지만 그들 간에 중요한 차이점이 있습니다:
    1. FIRST_VALUE:
        • FIRST_VALUE 함수는 윈도우에서 첫 번째 값을 가져옵니다. 이 함수는 윈도우의 정렬 순서에 따라 첫 번째 행의 값을 반환합니다. 기본적으로는 윈도우 내에서 가장 먼저 오는 값을 선택합니다.
    1. LAST_VALUE:
        • LAST_VALUE 함수는 윈도우에서 마지막 값을 가져옵니다. 하지만 이 함수는 기본적으로 윈도우의 끝을 나타내는 것이 아니라, 현재 행을 포함한 윈도우의 마지막 행에 도달할 때까지 지정된 정렬 순서에 따라 마지막 값을 유지합니다. 윈도우의 끝에 도달하지 않으면 마지막 행의 값을 그대로 반환합니다.
    예를 들어, 다음은 FIRST_VALUE와 LAST_VALUE를 사용한 간단한 예제입니다:
    notion image
    이 쿼리에서는 your_table의 각 id 그룹 내에서 value 값을 정렬된 some_column 기준으로 윈도우 함수를 사용하여 첫 번째 값(first_val)과 마지막 값(last_val)을 가져오고 있습니다.
    참고로, LAST_VALUE 함수는 기본적으로 윈도우의 마지막 행에서 현재 행까지의 범위에서 값을 가져올 뿐만 아니라, 추가적인 설정이 필요할 수 있습니다. 이 함수를 사용할 때 ROWS BETWEEN와 같은 윈도우 프레임(window frame)을 명시하여 원하는 범위를 명확하게 지정하는 것이 중요합니다.
     

    튜닝73. 스칼라 서브쿼리를 데이터 분석함수로 튜닝하세요! -NTH_VALUE

    📖

    NTH_VALUE

     
     
    예제1)
    부서번호, 이름, 월급, 자기가 속한 부서번호에서 월급의 순위가 2등인 사원의 이름을 출력하시오.
    <튜닝후> select deptno, ename, min(sal), nth_value( ename, 2 ) over (partition by deptno order by min(sal) desc rows between unbounded preceding and unbounded following ) as second_ename from emp group by deptno, ename; DEPTNO|ENAME |MIN(SAL)|SECOND_ENAME| ------+------+--------+------------+ 10|KING | 5000|CLARK | 10|CLARK | 2450|CLARK | 10|MILLER| 1300|CLARK | 20|SCOTT | 3000|FORD | 20|FORD | 3000|FORD | 20|JONES | 2975|FORD | 20|ADAMS | 1100|FORD | 20|SMITH | 800|FORD | -- 같은 값 때문에 ford가 출력됨. -- desc하게 정렬된 순서 중 2번째에 위치한 ford가 출력되는 것 30|BLAKE | 2850|ALLEN | 30|ALLEN | 1600|ALLEN | 30|TURNER| 1500|ALLEN | 30|MARTIN| 1250|ALLEN | 30|WARD | 1250|ALLEN | 30|JAMES | 950|ALLEN |

    문제풀며 익히기

     
     
     
    Q1.

    튜닝74. IN LINE VIEW를 데이터 분석함수로 변경하세요!

    📖
    예제1) 부서번호, 직업, 부서번호별 직업별 토탈월급을 출력하시오.
    <튜닝전> select deptno, decode(no, 1, job, 2, '부서토탈:') job, sum(sal) from emp e, (select rownum no from dual connect by level <= 2) --숫자 1,2를 출력하는 계층형 질의문 --따라서 1과 14개의 행이 조인하고, 2와 14개의 행이 조인하게 됨 group by deptno, decode(no, 1, job, 2, '부서토탈:') order by 1,2; # 1. group by 전 월급 결과 보기 select deptno, decode(no, 1, job, 2, '부서토탈:') job, SAL from emp e, (select rownum no from dual connect by level <= 2) ; DEPTNO|JOB |SAL | ------+---------+----+ 10|PRESIDENT|5000| --1이면 job 30|MANAGER |2850| --즉 1과 14번 조인한것 10|MANAGER |2450| 20|MANAGER |2975| 30|SALESMAN |1250| 30|SALESMAN |1600| 30|SALESMAN |1500| 30|CLERK | 950| 30|SALESMAN |1250| 20|ANALYST |3000| 20|CLERK | 800| 20|ANALYST |3000| 20|CLERK |1100| 10|CLERK |1300| 10|부서토탈: |5000| ---2이면 토탈월급 30|부서토탈: |2850| --2와 14번 조인한 것의 결과 10|부서토탈: |2450| 20|부서토탈: |2975| 30|부서토탈: |1250| 30|부서토탈: |1600| 30|부서토탈: |1500| 30|부서토탈: | 950| 30|부서토탈: |1250| 20|부서토탈: |3000| 20|부서토탈: | 800| 20|부서토탈: |3000| 20|부서토탈: |1100| 10|부서토탈: |1300| DEPTNO|EMPNO |SUM(SAL)| ------+---------+--------+ 10|CLERK | 1300| 10|MANAGER | 2450| 10|PRESIDENT| 5000| 10|부서토탈: | 8750| 20|ANALYST | 6000| 20|CLERK | 1900| 20|MANAGER | 2975| 20|부서토탈: | 10875| 30|CLERK | 950| 30|MANAGER | 2850| 30|SALESMAN | 5600| 30|부서토탈: | 9400|
    notion image
     

    튜닝75. 상호관련 서브쿼리문을 이용한 UPDATE 문을 MERGE문으로 변경하세요.

    📖

    상호관련 서브쿼리문을 이용한 UPDATE문 → MERGE문

     
    예제1) 사원 테이블에 loc컬럼을 추가하시오.
    @demo alter table emp add loc varchar2(20);
     
    예제2) 지금 추가한 loc컬럼에 해당 사원의 부서위치로 값을 갱신하시오.
    <튜닝전> update emp e set loc = (select loc from dept d where d.deptno = e.deptno); /* 메인테이블에 있는 업데이트 컬럼이 서브쿼리 안으로 들어가기 때문에 메인쿼리부터 수행되는 것을 알 수 있음 emp테이블에서 첫 행인 king의 deptno 10을 e.deptno에 넣고 dept테이블과 조인하여 d.deptno에 10이 할당되고 10의 loc를 넣어둠 emp테이블이 14행이기 때문에 총 14번 반복함. (단, 서브쿼리 사용으로 최대한 캐싱을 하긴 함. 10번 deptno는 메모리에 올려두는 ) */ SELECT ename, loc FROM emp; ENAME |LOC | ------+--------+ KING |NEW YORK| BLAKE |CHICAGO | CLARK |NEW YORK| JONES |DALLAS | MARTIN|CHICAGO | ALLEN |CHICAGO | TURNER|CHICAGO | JAMES |CHICAGO | WARD |CHICAGO | FORD |DALLAS | SMITH |DALLAS | SCOTT |DALLAS | ADAMS |DALLAS | MILLER|NEW YORK| rollback;
     
    예제3) update절의 서브쿼리문으로 위의 sql을 튜닝하시오.
    <튜닝후> update ( select e.loc as emp_loc, d.loc as dept_loc from emp e, dept d where e.deptno = d.deptno ) set emp_loc = dept_loc; dept_loc 전체가 한번에 emp_loc에 쏙 들어가게 되는 것 ======================================================== --서브쿼리만 실행 select e.loc as emp_loc, d.loc as dept_loc from emp e, dept d where e.deptno = d.deptno; EMP_LOC|DEPT_LOC| -------+--------+ |NEW YORK| |CHICAGO | |NEW YORK| |DALLAS | |CHICAGO | |CHICAGO | |CHICAGO | |CHICAGO | |CHICAGO | |DALLAS | |DALLAS | |DALLAS | |DALLAS | |NEW YORK| --update **21c는 가능** update ( select e.loc as emp_loc, d.loc as dept_loc from emp e, dept d where e.deptno = d.deptno ) set emp_loc = dept_loc; **19c /*+ bypass_ujvc */ 힌트 사용 없어짐(11g까지만 됨) ** 19c PK제약조건을 사용해야 UPDATE 가능 ** alter table dept add constraint dept_deptno_pk primary ket(deptno); update ( select e.loc as emp_loc, d.loc as dept_loc from emp e, dept d where e.deptno = d.deptno ) set emp_loc = dept_loc;
     
    예제4) 이번에는 오라클에서 권장하는 merge문을 시용해서 update하시오
    merge into emp e using dept d on (d.deptno = d.deptno) when matched then update set e.loc = d.loc; select ename, loc from emp;

    문제풀며 익히기

     
    Q1.
    아래의 환경을 만드시오.
    drop table sales100; drop table sales200; drop table time2; -- 실습 환경만들기 create table sales100 as select * from sh.sales; create table sales200 as select rownum rn, prod_id, cust_id, time_id, channel_id, promo_id, quantity_sold, amount_sold from sales100; alter table sales200 add date_id date; create table time2 ( rn number(10), date_id date ); begin for i in 1 .. 918843 loop insert into time2 values( i , to_date('1961/01/02','YYYY/MM/DD')+ i ); end loop; end; / commit; select rn, date_id from sales200; -- select rn, date_if from time2;
     
    Q2. time2테이블을 이용하여 sales200테이블의 date_id컬럼의 값을 갱신하시오!
    <튜닝전> update sales200 s set date_id = ( select date_id from time2 t where t.rn = s.rn );
    <튜닝 후> merge into sales200 s using time2 t on (s.rn = t.rn) when matched then update set s.date_id = t.date_id; 918843 행이 병합되었습니다.
     
     
     
     

    튜닝76. FULL TABLE SCAN이 더 유리한 SQL도 있어요. - 특급 DBA의 TIP 포함 ★★★★★

    📖
    💡
    FULL TABLE SCAN을 할 수 밖에 없는 경우

    FULL TABLE SCAN을 할 수 밖에 없는 경우

    1. 인덱스 생성 시
    1. 테이블 통계 정보를 수집할 때
    1. 테이블을 다른 table space로 이동 시킬 때
    1. where절이 없거나 인덱스가 없는 컬럼의 데이터를 검색할 때
    ⇒ 이런 경우, full table scan을 할 수 밖에 없기 때문에 full table scan을 최대한 빠르게 할 수 있게 해야함
     

    1. 인덱스 생성시

    # 1. 인덱스 생성시 @demo <튜닝전> create index emp_sal on emp(sal); <튜닝후> create index emp_sal on emp(sal) nologging --log정보를 생성하지 않는다는 옵션 parallel 8; -- 병렬로 작업: 8개의 프로세서가 동시에 인덱스 생성을 도움 alter index emp_sal logging; --되돌리기 alter index emp_sal parallel 1; --되돌리기 !!되돌리기!! logging과 병렬도를 위와 같이 원래대로 돌려놓아야, 실행계획을 제대로 볼 수 있음. 바꾸지 않으면 옵티마이저가 더 좋은 인덱스가 있어도 emp_sal이라는 index만 사용하려는 부작용이 발생하기 때문
    • NOLOGGING : DB장애로 복구 시 INDEX 복구 안됨
      • 따라서 만들 때만 빠르게 만들고 다시 LOGGING으로 바꾸기 혹은 DB장애 후 복구시 그냥 INDEX 재생성
    • CPU_COUNT : SHOW PARAMETER CPU_COUNT
      • CPU코어 수를 볼 수 있는 명령어로. 병렬도는 CPU코어수의 2배까지 줄 수 있다.
     

    2. 테이블 통계정보 수집 시

    # 2. 테이블 통계정보 수집 시 <튜닝전> exec dbms_stats.gather_table_stats(user,'emp');
     

    3. 테이블을 다른 TABLE SPACE로 이동시킬 때

    → 오라클 관리 수업에서 배울 것.
     
     

    4. WHERE절이 없거나 인덱스가 없는 컬럼의 데이터를 검색할 때

    <튜닝전> select job, count(*) from emp group by job;
    <튜닝후> alter session set db_file_multiblock_read_count = 256; alter session set hash_area_size = 1000000000; alter session set sort_area_size = 1000000000; create index emp_job on emp(job); --인덱스를 걸어줄 수 있는 상황이면 걸어주는게 무조건 좋음. --안되는 상황이면 인덱스 생성은 제외하고 진행 select /*+ index_ffs(emp emp_job) paralle_index(emp,emp_job,8) */ job, count(*) from emp where job is not null --index fast full scan을 위해 줘야하는 조건 group by job;
     

    문제풀며 익히기

     
    Q1. 아래의 sql을 튜닝하시오.
    <튜닝전> @demo select deptno, sum(sal) from emp group by deptno;
    <튜닝후> alter session set db_file_multiblock_read_count = 256; alter session set hash_area_size = 1000000000; alter session set sort_area_size = 1000000000; create index emp_deptno_sal on emp(deptno,sal) nologging parallel 8; alter index emp_deptno_sal logging; alter index emp_deptno_sal parallel 1; select /*+ index_ffs(emp emp_deptno_sal) paralle_index(emp,emp_deptno_sal,8) */ deptno, sum(sal) from emp where deptno is not null --index fast full scan을 위해 줘야하는 조건 group by deptno; DEPTNO SUM(SAL) ---------- ---------- 10 8750 20 10875 30 9400
    병렬처리시 볼 수 있는 실행계획
    notion image
     

    튜닝77. 과거 버전의 실행계획을 볼 수 있어야해요.

    📖

    과거 버전의 실행계획 보기

    오라클 버젼 업그레이드를 할 때 제일 걱정하는 부분이 오라클을 업그레이드 하면예전 버젼에서 잘 수행되던 SQL이 업그레이드를 한 이후에 느려지지 않을지를 걱정합니다. 혹시 느려지게 되면 예전 버젼의 실행계획을 확인해서 그 실행계획으로 튜닝을 해주면 됩니다.
     
    ===============================
    구(Old) 장비 ——————→ 신(New) 장비
    오라클 구 버전 오라클 새 버전
    (오라클은 3~4년 주기로 버전 upgrade함)
    ===============================
     
    *오라클의 입장
    오라클 과거 버전에 대해 기술지원이 어려울 수 있다. 기술지원은 되지만, 문제가 발생했을 때 관련 FETCH를 만들어 줄 수 없으니, UPGRADE를 하기를 권장합니다. (왜? 그 버전을 만들었던 개발자들이 퇴사했기 때문. 그래서 계속 버전 업데이트함)
     
    *고객 회사의 입장
    오라클의 과거버전 FETCH 미지원 때문에 신규 프로젝트(차세대 프로젝트) 지원 시 개발자, DBA들이 필요함.
     
    *업그레이드 시 문제점
    ===============================
    오라클 구버젼 ----------------> 오라클 신버젼 똑같은 SQL 똑같은 SQL
    ===============================
    오라클 구버전 —> 신버전으로 업그레이드 시 같은 SQL인데도 빨리 검색되던 SQL이 느려지거나 돌아가지 않는 경우가 발생.
    이런 일 때문에 업그레이드 한 회사에서 DBA가 튜너들을 불러서 업그레이드시 느려진 SQL을 파악해서 튜닝함
     
    이때, 똑같은 SQL이 왜 느려졌는지 알기 위해서 과거 버전의 실행계획을 볼 줄 알아야함.
     
    ⇒ 따라서 DBA나 튜너가 없어질 수 없음. 오라클의 정책 덕분에

    예제

     
    예제1) 아래의 sql의 예전 버전의 실행계획을 확인합니다.
    @demo insert into emp(empno, ename, sal, deptno) values(1122, 'JACK', 3000, 70); commit; select e.ename, d.loc from emp e full outer join dept d on (e.deptno = d.deptno); ENAME |LOC | ------+--------+ KING |NEW YORK| CLARK |NEW YORK| MILLER|NEW YORK| JONES |DALLAS | FORD |DALLAS | SMITH |DALLAS | SCOTT |DALLAS | ADAMS |DALLAS | BLAKE |CHICAGO | MARTIN|CHICAGO | ALLEN |CHICAGO | TURNER|CHICAGO | JAMES |CHICAGO | WARD |CHICAGO | JACK | | |BOSTON | -----------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || -----------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 16 |00:00:00.01 | 13 | | | || | 1 | VIEW | VW_FOJ_0 | 1 | 15 | 16 |00:00:00.01 | 13 | | | || |* 2 | HASH JOIN FULL OUTER| | 1 | 15 | 16 |00:00:00.01 | 13 | 1797K| 1797K| 1092K (0)|| | 3 | TABLE ACCESS FULL | DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | | || | 4 | TABLE ACCESS FULL | EMP | 1 | 14 | 15 |00:00:00.01 | 7 | | | || -----------------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 2 - access("E"."DEPTNO"="D"."DEPTNO") | |
    notion image
    A. select /*+ optimizer_features_enable('10.2.0.1') opt_param('_optimizer_native_full_outer_join','off') */ e.ename, d.loc from emp e full outer join dept d on (e.deptno = d.deptno); select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('a7n6d8zs5a3ff',1,'ALLSTATS LAST')); ------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem || ------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 16 |00:00:00.01 | 24 | | | || | 1 | VIEW | | 1 | 15 | 16 |00:00:00.01 | 24 | | | || | 2 | UNION-ALL | | 1 | | 16 |00:00:00.01 | 24 | | | || |* 3 | HASH JOIN OUTER | | 1 | 14 | 15 |00:00:00.01 | 12 | 1922K| 1922K| 1000K (0)|| | 4 | TABLE ACCESS FULL| EMP | 1 | 14 | 15 |00:00:00.01 | 6 | | | || | 5 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | | || |* 6 | HASH JOIN ANTI | | 1 | 1 | 1 |00:00:00.01 | 12 | 1797K| 1797K| 1092K (0)|| | 7 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 6 | | | || | 8 | TABLE ACCESS FULL| EMP | 1 | 14 | 15 |00:00:00.01 | 6 | | | || ------------------------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 3 - access("E"."DEPTNO"="D"."DEPTNO") | 6 - access("E"."DEPTNO"="D"."DEPTNO") | | =================================================================== ★ /*+ optimizer_features_enable('10.2.0.1') */★ :예전버전의 실행계획을 확인하는 힌트 괄호 안에 구 버전 번호를 넣는 것. 10g버전을 넣어봄. .1은 fetch버전1을 의미 버전 모르겠으면 다 넣어용 /* opt_param('_optimizer_native_full_outer_join','off') */ 히든 파라미터를 sql level로 변환할 수 있는 힌트 앞에 언더바(_)가 붙은게 히든 파라미터 이는 예전의 full outer join 실행계획으로 수행해달라는 힌트 (과거 실행계획 확인과 관련 없음! 그냥 알려주기 위해서 넣으신것) [옵션] on : 최신 full outer join 문법으로 실행계획을 수행해달라 off : 과거 full outer join 문법으로 실행계획을 수행해달라
    notion image
     
     
     

    튜닝78. Dynamic Sampling이라는 기능이 뭔지 알아야해요★★★

    📖

    Dynamic Sampling

    테이블 통계정보를 수집하지 않아도 기본적인 테이블 통계정보를SQL을 실행할 때 수집을 하는데, 이 기능의 바로 “Dynamic Sampling” 기능입니다.
     
     

    예제

     
    예제1) 다이나믹 샘플링 기능이 켜있는지 확인하시오
    show parameter optimizer_dynamic_sampling 혹은 show parameter dynamic SQL> show parameter optimizer_dynamic_sampling NAME TYPE VALUE ------------------------------------ ---------------------- ------------------------------ optimizer_dynamic_sampling integer 2 ============================================================= value 값이 1이상이면 켜져있는 것 이 값을 10까지 줄 수 있음 숫자가 크면 클 수록 더 많은 통계정보가 수집됨
    notion image
     
    예제2) 다이나믹 샘플링 기능을 끄시오.—sys 유저에서 실시하는게 좋음
    C:\trace>sqlplus sys/oracle_4U as sysdba SQL> alter system set optimizer_dynamic_sampling=0; 시스템이 변경되었습니다. SQL> set lines 3000 SQL> set pages 4000 SQL> show parameter optimizer_dynamic_sampling NAME TYPE VALUE ------------------------------------ ---------------------- ------------------------------ optimizer_dynamic_sampling integer 0
    notion image
    다이나믹 샘플링 설정 후 DB를 내렸다 올려줘야함.
    startup force ---------------- SQL> startup force ORA-32004: obsolete or deprecated parameter(s) specified for RDBMS instance ORACLE 인스턴스가 시작되었습니다. Total System Global Area 1610611352 bytes Fixed Size 9857688 bytes Variable Size 1006632960 bytes Database Buffers 587202560 bytes Redo Buffers 6918144 bytes 데이터베이스가 마운트되었습니다. 데이터베이스가 열렸습니다. SQL>
    notion image
     
    예제3) emp테이블의 통계정보를 지웁니다.
    SQL> exec dbms_stats.delete_table_stats('C##SCOTT','EMP'); SQL> connect c##scott/tiger 연결되었습니다. SQL> select table_name, last_analyzed from user_tables where table_name='EMP'; TABLE_NAME|LAST_ANALYZED| ----------+-------------+ EMP | | ==> NULL이면 지워진 것
    notion image
     
    예제4) 아래의 테스트 테이블을 생성합니다.
    -- 테스트할 테이블을 생성합니다.-- --EMP 99테이블 생성 drop table emp99; create table emp99 (empno number(10), ename varchar2(20)); --PL/SQL로 for loop문을 10000번 돌려서 emp99에 scott||i 을 계속 넣음 begin for i in 1 .. 10000 loop insert into emp99 values( i, 'scott'||i); end loop; commit; end; / PL/SQL 처리가 정상적으로 완료되었습니다. -- empno 1인 scott1 을 제외하고 ename을 scott99로 update update emp99 set ename='scott99' where empno> 1; 9999 행이 업데이트되었습니다. commit; select count(*) from emp99 where ename='scott1'; COUNT(*) ---------- 1 select count(*) from emp99 where ename='scott99'; COUNT(*) ---------- 9999 create index emp99_ename on emp99(ename);
     
    예제5) 아래의 sql은 옵티마이저가 인덱스를 타겠는가?
    A. yes.
    select count(*) from emp99 where ename = 'scott1'; COUNT(*) ---------- 1 ---------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | ---------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 2 | 1 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 2 | 1 | |* 2 | INDEX RANGE SCAN| EMP99_ENAME | 1 | 1 | 1 |00:00:00.01 | 2 | 1 | ----------------------------------------------------------------------------------------------------
    notion image
     
    예제6) 아래의 sql은 옵티마이저가 인덱스를 타는가?
    A. yes. 하지만 full scan이 나은 상황인데, 테이블 통계정보가 없기 때문에
    옵티마이저가 그냥 인덱스를 탄 것.
    <튜닝전> select count(*) from emp99 where ename = 'scott99'; COUNT(*) ---------- 9999 ---------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | ---------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 28 | 26 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 28 | 26 | |* 2 | INDEX RANGE SCAN| EMP99_ENAME | 1 | 1 | 9999 |00:00:00.01 | 28 | 26 | ----------------------------------------------------------------------------------------------------
    notion image
    emp99에 대한 테이블 통계정보가 없고, 다이나익 샘플링 기능까지 꺼져있어서 옵티마이저가 정확하게 판단하지 못하고, full table scan을 해야하는데, 인덱스 스캔을 해버림.(scott99가 해당 테이블의 대부분의 정보를 차지하기 때문에 테이블 풀 스캔이 더 비용이 적게 드는 계획임)
    • 고객사가 테이블 통계정보를 절대 수집하지 않겠다고 하면 아래와 같이 dynamic_sampling힌트를 줌
    <튜닝후> select /*+ dynamic_sampling(emp99 10) */ count(*) from emp99 where ename = 'scott99'; -------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 31 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 31 | |* 2 | TABLE ACCESS FULL| EMP99 | 1 | 9999 | 9999 |00:00:00.01 | 31 | -------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter("ENAME"='scott99') Note ----- - dynamic statistics used: dynamic sampling (level=0) => dynamic sampling 기능을 통해 통계정보 사용
    테이블 통계정보 수집을 하지 않거나 통계정보 수집이 오래된 경우와 심지어 다이나믹 샘플링 기능까지 꺼져있으면, 이렇게 튜닝하면 됨.
    notion image
     
     

    문제풀며 익히기

     
    Q1.
    emp테이블의 통계정보를 수집하면 위의 힌트 없이 옵티마이저가 제대로 full table scan을 하는지 확인하시오.
    A. NO.
    테이블 통계정보를 수집해줬는데, INDEX RANGE SCAN을 함. 이런 경우는 히스토그램 정보를 수집해줘야 제대로 실행계획이 출력됨.
    왜? 데이터의 격차가 큰 경우 히스토그램 정보를 수집해줘야 제대로 실행계획 출력 됨
    (튜닝예제79에서 확인하기)
    exec dbms_stats.gather_table_stats(user,'EMP'); select count(*) from emp99 where ename = 'scott99'; ------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 28 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 28 | |* 2 | INDEX RANGE SCAN| EMP99_ENAME | 1 | 1 | 9999 |00:00:00.01 | 28 | -------------------------------------------------------------------------------------------
    notion image
     
     

    튜닝79. 데이터의 편차가 심한 컬럼은 히스토그램 통계정보를 수집해줘야해요.

    📖

    실습

    method_opt를 권장.
    왜? auto로 되어있어서 히스토그램으로 수집할지 그냥 통계정보를 수집할지 알아서 하기 때문이다.
    #1. 테이블 통계정보를 지웁니다.(EMP테이블의 통계정보 삭제) exec dbms_stats.delete_table_stats( user, 'emp'); #2. 다음과 같이 컬럼 통계정보를 수집하거나 method_opt를 써서 통계정보를 수집하세요. --히스토그램 수집 방법 1 : 컬럼 통계정보 수집 : 11g이전에 사용하던 것. 방법2를 권장 analyze table emp99 compute statistics for columns ename ; /* ENAME에 대한 즉, 특정 컬럼에 대한 통계정보를 더 중점적으로 수집 */ --히스토그램 수집 방법 2 : METHOD_OPT exec dbms_stats.gather_table_stats( user, 'emp99',method_opt=>'for all columns size auto' ); /* method_opt=>'for all columns size auto' AUTO로 설정해놓으면 아래의 결과를 옵티마이저에게 알려줌 SCOTT99 =99건 SCOTT1 =1건 */
    #3. 아래의 SQL의 실행계획을 확인하시오. --index scan이 실행계획에 나와줘야함-- select count(*) from emp99 where ename = 'scott1'; ------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 2 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 2 | |* 2 | INDEX RANGE SCAN| EMP99_ENAME | 1 | 1 | 1 |00:00:00.01 | 2 | ------------------------------------------------------------------------------------------- | | --full table scan이 실행계획에 나와줘야함-- select count(*) from emp99 where ename = 'scott99'; -------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | -------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 31 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 31 | |* 2 | TABLE ACCESS FULL| EMP99 | 1 | 2287 | 9999 |00:00:00.01 | 31 | --------------------------------------------------------------------------------------
    scott1을 조회할때는 index range scan, scott99를 조회할 때는 index fast full scan을 합니다.
    회사에 위와 같은 컬럼들이 1~2개씩 있습니다. 위와 같은 컬럼은 히스토그램 통계정보가 필요한 컬럼입니다.
    notion image

    문제풀며 익히기

     
    Q1. emp테이블에 job컬럼의 히스토그랭 통계정보를 수집하시오.
    analyze table emp compute statistics for columns job ; exec dbms_stats.gather_table_stats( user, 'emp',method_opt=>'for all columns size auto' );
     

    튜닝80. 바인드 변수를 사용해서 SQL을 작성하세요.—바인드 피킹 기능 끄기

    📖

    바인드 변수 사용

    SQL이 메모리에서 공유가 되려면, 똑같은 SQL이어야 함 예시와 같이 끝이 다른 sql이 리터럴 sql임. 이들은 메모리에 공유되지 않아서 hard parsing하게 됨. 따라서 바인드 변수를 사용해야한다.

    장점

    SOFT PARSING을 할 수 있게 됨

    문제점*주의 필요*

    바인드 변수로 변경하게 되면, 히스토그램 통계 정보를 사용할 수 없게 됨.
    다음과 같이 리터럴 SQL로 실행되면, 실행할 때 마다 실행 계획이 달라지게 된다.
    select count(*) from emp99 where ename = 'scott1'; select count(*) from emp99 where ename = 'scott99'; 통계정보를 수집되고 있다면, ename에 인덱스가 있는데, soctt1은 1건 이기 대문에 index range scan함 scott99는 index가 있어도 99건 이기 때문에 full table scan을 하게 됨 바인드 변수를 사용할 경우 index가 있으면 index를 사용해야겠다. 라고 첫 실행계획이 고정이 되어버려서 히스토그램 통계정보를 사용할 수 없음. 즉, 둘 다 인덱스 스캔 하거나 둘 다 full scan을 하게 됨.
    다음과 같이 바인드 변수를 사용하게 되면 v_ename에 뭐가 들어올지 모르니까 실행계획이 생성된 다음에 값이 바인딩 되어서 soctt1이 들어오던 scott99가 들어오던 같은 실행계획으로 실행됨.
    select count(*) from emp99 where ename = :v_ename;
    진짜로 실행계획이 똑같은지 확인해보기
    SQL> variable v_ename varchar2(20) SQL> SQL> exec :v_ename := 'scott1'; PL/SQL 처리가 정상적으로 완료되었습니다 SQL> select count(*) from emp99 where ename= :v_ename; COUNT(*) ---------- 1 SQL> @p ------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 2 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 2 | |* 2 | INDEX RANGE SCAN| EMP99_ENAME | 1 | 1 | 1 |00:00:00.01 | 2 | ------------------------------------------------------------------------------------------- ================================================================ SQL> exec :v_ename := 'scott99' PL/SQL 처리가 정상적으로 완료되었습니다 SQL> select count(*) from emp99 where ename= :v_ename; COUNT(*) ---------- 9999 SQL> @p ------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 28 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 28 | |* 2 | INDEX RANGE SCAN| EMP99_ENAME | 1 | 1 | 9999 |00:00:00.01 | 28 | -------------------------------------------------------------------------------------------
    따라서 데이터의 편차가 큰 테이블에 바인드변수를 사용하면, 오히려 위와 같이 손해가 발생하게 됨.
     
    왜 이런 일이 벌어졌는가?
    A, parsing 과정에서 실행계획을 생성하기 때문이다.
    select count(*) from emp99 where ename = 'scott1'; 1. parsing -> 2. 실행(execute) -> 3. 패치(fetch) select count(0) from emp99 where ename = :v_ename; 1. parsing -> 2. 바인딩 -> 3. 실행(execute) -> 4. 패치(fetch) => 파싱이 발생 한 후 바인딩을 하기 때문에 문제가 발생 바인딩은 더 뒤에 하니까.
     

    문제점 해결 방법 BIND PEEKING -BUT문제점 있음

    오라클에서 이 문제를 해결하려고 바인드 피킹(PEEKING)이라는 기능을 제공. 즉, 입력된 값을 훔쳐보겠다는 의밀. 파싱 과정에서 무슨 값이 들어올 지 훔쳐 봄 Default로 켜져 있는 상태 하지만. 좋은게 아니라서 보통 꺼둠.
    파싱 후 실행계획이 만들어진 이후에 값이 바인딩이 되는데, 바인드 파킹은 미리 바인드 변수에 입력될 값을 미리 확인하고 실행계획을 생성하겠다는 것. 그런데 처음에 피킹한 값으로 실행계획이 고정되어 버리기 때문에 다음에 scott1이 들어오던 scott99가 들어오던 같은 실행계획으로 실행되어서 현업에서는 이 바인드 피킹 기능을 다 끈다.
    성능 상 문제가 많은 기능..
    왜? 처음에 들어온 값을 peeking한 후 실행 계획을 고정해버리기 때문! 다음부터는 soft parsing을 하기 때문에
     

    BIND PEEKING의 문제점 해결 방법

    위와 같이 EMP99의 ENAME과 같은 돌연변이 컬럼을 이용한 SQL은 그럼 어떻게 튜닝하는가? ⇒ 바인드 변수를 사용하지 않고 리터럴 sql로 작성해줘야함.
    select count(*) from emp99 where ename = 'scott1'; select count(*) from emp99 where ename = 'scott99';
    위와 같은 컬럼 말고 다른 컬럼들은 아래와 같이 바인드 변수 사용을 권장.
    select empno, ename from emp where empno = 7788; select empno, ename from emp where empno = 7902; => variable v_empno varchar2(20) exec :v_empno := '7788'; select empno , ename from emp where empno = :v_empno;
    그리고 혹시라도 위의 바인드 변수를 사용한 쿼리의 성능 저하가 생길지 모르는 데이터가 update나 insert가 발생하게 될 때 (ex. 사원번호가 한 명만 7788이고 나머지는 9999로 변경되었을 때, 혹은 어느 하나의 데이터가 많이 중복되게될 때) 바인드 피킹 때문에 성능이 떨어지게 됨.
    ⇒ 따라서 바인드 피킹 기능을 끄는 것을 권장함. 현업은 대부분 끔 (문제1로 가기)
     
     
    예시) 다음의 SQL은 메모리에서 공유가 될까요?
    A. NO. 리터럴 SQL 구분. 따라서 다르기 때문에 HARD PARSING을 하게됨.
    select count(*) from emp99 where ename = 'scott1'; select count(*) from emp99 where ename = 'scott99'; ------------------------------------------------ 리터럴 SQL이 많으면, 성능이 느려짐. 왜? HARD PARSING을 하게 되니까! 위의 SQL이 서로 공유가 되려면, 다음과 같이 바인드 변수를 사용해줘야함. -------------------------------------------------- # 1. 바인드 변수(호스트 변수) 선언, 바인드 변수의 타입 및 크기 지정 variable v_ename varchar2(20) # 2. 바인드 변수에 값 할당 exec :v_ename := 'scott1'; # 3. 바인드변수를 사용한 sql문 select count(*) from emp99 where ename= :v_ename; COUNT(*) ---------- 1 PL/SQL 처리가 정상적으로 완료되었습니다. SQL> exec :v_ename := 'scott99'; PL/SQL 처리가 정상적으로 완료되었습니다. SQL> select count(*) from emp99 where ename= :v_ename; COUNT(*) ---------- 9999
    notion image

    문제풀며 익히기

     
    Q1. 바인드 피킹 기능을 끄시오. —-sys로 접속해서 해야함
    #1. 바인드 피킹 on off(true/false로 되어있음) 상태 확인하기 SQL> connect sys/oracle_4U as sysdba 연결되었습니다. col "Parameter" format a40 col "Session Value" format a15 col "Instance Value" Format a15 select a.ksppinm "Parameter", b.ksppstvl "Session Value", c.ksppstvl "Instance Value" from x$ksppi a, x$ksppcv b, x$ksppsv c where a.indx = b.indx and a.indx = c.indx and a.ksppinm like '%&m_name%'; #1-1. 파일에 넣어두기 SQL> ed h.sql--아래의 내용 메모장에 넣고 저장 후 메모장 닫기 /* col "Parameter" format a40 col "Session Value" format a15 col "Instance Value" Format a15 select a.ksppinm "Parameter", b.ksppstvl "Session Value", c.ksppstvl "Instance Value" from x$ksppi a, x$ksppcv b, x$ksppsv c where a.indx = b.indx and a.indx = c.indx and a.ksppinm like '%&m_name%'; */ #2.----------아래와 같이 입력하시오가 나오면 peek입력--------------- SQL> @h m_name의 값을 입력하십시오: peek Parameter Session Value Instance Value ---------------------------------------- --------------- --------------- _px_bind_peek_sharing TRUE TRUE _optim_peek_user_binds TRUE TRUE _xpl_peeked_binds_log_size 8192 8192 #3. 바인드 피킹 끄기 alter system set "_optim_peek_user_binds" = false; #4. db내렸다가 올리기 SQL> startup force ORA-32004: obsolete or deprecated parameter(s) specified for RDBMS instance ORACLE 인스턴스가 시작되었습니다. Total System Global Area 1610611352 bytes Fixed Size 9857688 bytes Variable Size 1006632960 bytes Database Buffers 587202560 bytes Redo Buffers 6918144 bytes 데이터베이스가 마운트되었습니다. 데이터베이스가 열렸습니다. SQL> #5. 바인드 피킹 TRUE/FALSE여부 확인하기 SQL> @h m_name의 값을 입력하십시오: peek Parameter Session Value Instance Value ---------------------------------------- --------------- --------------- _px_bind_peek_sharing TRUE TRUE _optim_peek_user_binds FALSE FALSE _xpl_peeked_binds_log_size 8192 8192 SQL>
    notion image
    notion image
    notion image
     
     

    ■ 파티션 테이블(Partition table)

     

    튜닝81. 대용량 테이블은 파티션 테이블로 재구성하세요.

    📖

    Partition Table(파티션 테이블)

    파티셔닝을 왜 사용하는 것인가 ? 그림을 보고 대답하면 ?
    notion image
    ~> 빠르게 data 를 검색하려고
    따라서 데이터가 커지면 파티션 테이블을 반드시 사용해줘야함. FULL TABLE SCAN을 해도 범위가 짧아져서 효율적
    (한 곳에 모두 저장한 것을 heap table이라고 함. table이 너무 커지면 index도 효과를 발휘할 수 없게 됨)

    1.2 파티셔닝의 장점

    1. 관리적 측면 —DBA에게 아주 유용
      1. : 보관주기가 지난 data 들을 별도로 백업하고 지우는 일이 아주 쉬워진다.
    1. 성능적 측면
      1. : 파티셔닝 하지 않은 테이블이 대용량일 때 인덱스를 이용해서 data 를 검색하더라도 data 의 양이 많아서 인덱스를 이용해 건건이 테이블을 액세스하는 방식은 일정량이 넘는 순간 full table scan 보다 더 못한 결과가 나온다.
        그렇다고 full table scan 도 부담스럽다면 바로 그때 파티션을 나누면 full table scan 을 하더라도 일부 파티션 세그먼트만 읽고 멈출 수 있다.
        즉, 인덱스로 읽기에도 데이터양이 많아 부담스럽고, full table scan 을 하기에도 부담스럽다면, 파티셔닝하여 분할된 영역만 읽도록 하자 ! ~> 파티셔닝 기법
     

    파티션 뷰 생성하기

    실습1) Base table 생성 —SCOTT USER 에서 진행

    SQL> connect c##scott/tiger #1. 파티션 뷰를 정의할 때 사용할 Base 테이블을 만든다. drop table p1; drop table p2; drop table p3; CREATE TABLE p1 AS SELECT * FROM emp WHERE deptno = 10; --부서번호 10번만 가지고 만들어진 테이블 P1 CREATE TABLE p2 AS SELECT * FROM emp WHERE deptno = 20; CREATE TABLE p3 AS SELECT * FROM emp WHERE deptno = 30; #2. 체크 제약을 반드시 설정해야 함 ALTER TABLE p1 ADD CONSTRAINT c_deptno_10 CHECK(deptno < 20); -- 20 미만만 입력되도록 CHECK제약을 걸어줌 ALTER TABLE p2 ADD CONSTRAINT c_deptno_20 CHECK(deptno >= 20 AND deptno < 30); -- 20 이상 30 미만 값만 입력되도록 CHECK제약을 걸어줌 ALTER TABLE p3 ADD CONSTRAINT c_deptno_30 CHECK(deptno >= 30 AND deptno < 40); -- 30 이상 40 미만 값만 입력되도록 CHECK제약을 걸어줌 #3. 각 사원번호에 대한 인덱스 생성 CREATE INDEX p1_empno_idx ON p1(empno); CREATE INDEX p2_empno_idx ON p2(empno); CREATE INDEX p3_empno_idx ON p3(empno); #4. 테이블 통계정보 수집 analyze TABLE p1 compute statistics; analyze TABLE p2 compute statistics; analyze TABLE p3 compute statistics;

    실습2) 파티션 뷰 생성하기

    create or replace view emp_partition as select * from p1 union all select * from p2 union all select * from p3; --부서번호를 기준으로 파티셔닝 한 것을 확인할 수 있음 select * from emp_partition; EMPNO|ENAME |JOB |MGR |HIREDATE |SAL |COMM|DEPTNO| -----+------+---------+----+-----------------------+----+----+------+ 7839|KING |PRESIDENT| |1981-11-17 00:00:00.000|5000| | 10| 7782|CLARK |MANAGER |7839|1981-05-09 00:00:00.000|2450| | 10| 7934|MILLER|CLERK |7782|1982-01-11 00:00:00.000|1300| | 10| 7566|JONES |MANAGER |7839|1981-04-01 00:00:00.000|2975| | 20| 7902|FORD |ANALYST |7566|1981-12-11 00:00:00.000|3000| | 20| 7369|SMITH |CLERK |7902|1980-12-09 00:00:00.000| 800| | 20| 7788|SCOTT |ANALYST |7566|1982-12-22 00:00:00.000|3000| | 20| 7876|ADAMS |CLERK |7788|1983-01-15 00:00:00.000|1100| | 20| 7698|BLAKE |MANAGER |7839|1981-05-01 00:00:00.000|2850| | 30| 7654|MARTIN|SALESMAN |7698|1981-09-10 00:00:00.000|1250|1400| 30| 7499|ALLEN |SALESMAN |7698|1981-02-11 00:00:00.000|1600| 300| 30| 7844|TURNER|SALESMAN |7698|1981-08-21 00:00:00.000|1500| 0| 30| 7900|JAMES |CLERK |7698|1981-12-11 00:00:00.000| 950| | 30| 7521|WARD |SALESMAN |7698|1981-02-23 00:00:00.000|1250| 500| 30|
    notion image
     
    실습3)
    EMP_PARTITION에서 DEPTNO가 10번인 사원의 모든 컬럼을 출력하는 쿼리문을 실행하고, 실제 실행계획을 보고서 10번 부서번호에 대한 파티션만 읽었는지 확인하시오.
    select * from emp_partition where deptno = 10; EMPNO|ENAME |JOB |MGR |HIREDATE |SAL |COMM|DEPTNO| -----+------+---------+----+-----------------------+----+----+------+ 7839|KING |PRESIDENT| |1981-11-17 00:00:00.000|5000| | 10| 7782|CLARK |MANAGER |7839|1981-05-09 00:00:00.000|2450| | 10| 7934|MILLER|CLERK |7782|1982-01-11 00:00:00.000|1300| | 10| ------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.01 | 3 | | 1 | VIEW | EMP_PARTITION | 1 | 5 | 3 |00:00:00.01 | 3 | | 2 | UNION-ALL | | 1 | | 3 |00:00:00.01 | 3 | |* 3 | TABLE ACCESS FULL | P1 | 1 | 3 | 3 |00:00:00.01 | 3 | |* 4 | FILTER | | 1 | | 0 |00:00:00.01 | 0 | |* 5 | TABLE ACCESS FULL| P2 | 0 | 1 | 0 |00:00:00.01 | 0 | |* 6 | FILTER | | 1 | | 0 |00:00:00.01 | 0 | |* 7 | TABLE ACCESS FULL| P3 | 0 | 1 | 0 |00:00:00.01 | 0 | ------------------------------------------------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- 3 - filter("DEPTNO"=10) 4 - filter(NULL IS NOT NULL) 5 - filter("DEPTNO"=10) 6 - filter(NULL IS NOT NULL) 7 - filter("DEPTNO"=10) null is not null이란 의미는 특정 파티션만 읽었음을 의미 즉, p2, p3는 수행되지 않았음을 의미한다. 버퍼의 개수가 해당하는 부분에 0인것을 보면 알 수 있다.
     

    파티션 테이블 생성하기

    기존 테이블을 파티션 테이블로 구성하기

    emp테이블을 파티션 테이블로 구성하시오.
    • 파티션 테이블 명 : emp_partition2
      • 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; ========================== deptno => 파티션 키 컬럼 range => 범위 기준 파티션을 의미함 p1, p2, p3는 파티션 이름 즉, 각각이 하나의 서랍임 less than : 미만을 의미함. 괄호안의 숫자 미만 따라서 emp테이블을 불러서 emp_partition2라는 이름으로 파티션 테이블을 생성한 것 ============================= select * from emp_partition2; EMPNO|ENAME |JOB |MGR |HIREDATE |SAL |COMM|DEPTNO| -----+------+---------+----+-----------------------+----+----+------+ 7839|KING |PRESIDENT| |1981-11-17 00:00:00.000|5000| | 10| 7782|CLARK |MANAGER |7839|1981-05-09 00:00:00.000|2450| | 10| 7934|MILLER|CLERK |7782|1982-01-11 00:00:00.000|1300| | 10| 7566|JONES |MANAGER |7839|1981-04-01 00:00:00.000|2975| | 20| 7902|FORD |ANALYST |7566|1981-12-11 00:00:00.000|3000| | 20| 7369|SMITH |CLERK |7902|1980-12-09 00:00:00.000| 800| | 20| 7788|SCOTT |ANALYST |7566|1982-12-22 00:00:00.000|3000| | 20| 7876|ADAMS |CLERK |7788|1983-01-15 00:00:00.000|1100| | 20| 7698|BLAKE |MANAGER |7839|1981-05-01 00:00:00.000|2850| | 30| 7654|MARTIN|SALESMAN |7698|1981-09-10 00:00:00.000|1250|1400| 30| 7499|ALLEN |SALESMAN |7698|1981-02-11 00:00:00.000|1600| 300| 30| 7844|TURNER|SALESMAN |7698|1981-08-21 00:00:00.000|1500| 0| 30| 7900|JAMES |CLERK |7698|1981-12-11 00:00:00.000| 950| | 30| 7521|WARD |SALESMAN |7698|1981-02-23 00:00:00.000|1250| 500| 30|
        notion image
     
     
     
     

    튜닝82. 범위를 검색하는 대용량 테이블은 range partition table로 생성하세요.

    📖
     

    RANGE PARTITION TABLE

    range 파티션은 범위를 기준으로 파티셔닝을 한 파티션 테이블이다.
     
    실습1) —고객사 가서 파티션 테이블 만들어주고 오라고 하면 이런식으로 만들면 됨
    emp table의 월급 파티션 키 컬럼으로 해서 파티션 테이블을 생성하시오.
    create table emp_partition3 ( empno number(4,0), ename varchar2(10), job varchar2(9), mgr number(4,0), hiredate date, sal number(7,2), comm number(7,2), deptno number(2,0) ) partition by range(sal) (partition p1_sal values less than(1000), partition p2_sal values less than(2000), partition p3_sal values less than(3000), partition p4_sal values less than(4000), partition p5_sal values less than(5000), partition p6_sal values less than(maxvalue)); --king은 이곳에 들어갈 것 --5000이상의 값이 들어오는 것에 대해 미리 행동을 취하는 것 //insert를 해보면서 데이터가 잘 들어가는지 까지 확인해야함!!// insert into emp_partition3 select * from emp; ====================================================== 파티션 테이블은 각 구역이 나누어져 있기 때문에 insert가 시간이 더 오래걸림

    튜닝83. PARTITION 별 데이터가 잘 분배되었는지 확인하기 ★★

    📖

    PARTITION TABLE별 데이터 분배 확인하기★★

    #1. partition table 생성-c##Scott에서 create table emp_partition3 ( empno number(4,0), ename varchar2(10), job varchar2(9), mgr number(4,0), hiredate date, sal number(7,2), comm number(7,2), deptno number(2,0) ) partition by range(sal) (partition p1_sal values less than(1000), partition p2_sal values less than(2000), partition p3_sal values less than(3000), partition p4_sal values less than(4000), partition p5_sal values less than(5000), partition p6_sal values less than(maxvalue)); #2. emp_partition3 테이블의 통계정보를 수집 exec dbms_stats.gather_table_stats('c##scott', 'emp_partition3'); #3. 데이터 딕셔너리를 조회 select table_name, partition_name, num_rows from user_tab_partitions where table_name = 'EMP_PARTITION3'; --대문자 사용해야함 TABLE_NAME |PARTITION_NAME|NUM_ROWS| --------------+--------------+--------+ EMP_PARTITION3|P1_SAL | 2| EMP_PARTITION3|P2_SAL | 6| EMP_PARTITION3|P3_SAL | 3| EMP_PARTITION3|P4_SAL | 2| EMP_PARTITION3|P5_SAL | 0| EMP_PARTITION3|P6_SAL | 1|
    notion image

    문제풀며 익히기

     
    Q1. (SQL문제)
    EMP테이블에서 년도, 년도별 인원수를 출력하시오.
    select to_char(hiredate, 'rrrr'), count(*) from emp group by to_char(hiredate, 'rrrr') order by 1 asc; TO_CHAR(HIREDATE,'RRRR')|COUNT(*)| ------------------------+--------+ 1980 | 1| 1981 | 10| 1982 | 2| 1983 | 1|
    이렇게 연도별로 파티션을 만든다면, 년도 별 데이터 수를 확인해봐야함. 원래는 데이터가 가장 많은 81년 데이터를 반으로 나누던지 하는데, 년도별 데이터로 관리할 것이면 굳이 나눌 필요는 없음. 그래야 관리하기가 편하기 때문
    notion image
     
    Q2.
    emp 테이블을 년도별로 파티션 될 수 있도록 파티션 테이블을 생성하시오.
    • 파티션 테이블 명 : emp_partition4
    • partition
      • 1980년도 데이터 → p1
        1981년도 데이터 → p2
        1982년도 데이터 → p3
        1983년도 데이터 → p4
         
    ※ 파티션 테이블 생성 시 date형식 주의 사항 !
    to_date('19810101', 'RRRRMMDD') 로 하면, 에러 발생!
    to_date('19810101', 'YYYYMMDD') 로 작성해야함.
    --파티션 테이블 생성-- create table emp_partition4 ( empno number(4,0), ename varchar2(10), job varchar2(9), mgr number(4,0), hiredate date, sal number(7,2), comm number(7,2), deptno number(2,0) ) partition by range(hiredate) (partition p1 values less than(to_date('19810101', 'YYYYMMDD')), partition p2 values less than(to_date('19820101', 'YYYYMMDD')), partition p3 values less than(to_date('19830101', 'YYYYMMDD')), partition p4 values less than(to_date('19840101', 'YYYYMMDD')), partition p5 values less than(maxvalue)); -- 파티션 테이블에 값 넣기-- insert into emp_partition4 select * from emp; 14 행이 생성되었습니다. ======================================================= ※ 주의사항 ! to_date('19840101','RRRRMMDD') 로 하면 에러가 발생할 수 있습니다. to_date('19840101','YYYYMMDD') 로 작성해야 합니다. create table emp_partition5 ( empno number(4,0) , ename varchar2(10), job varchar2(9), mgr number(4,0), hiredate date, sal number(7,2), comm number(7,2), deptno number(2,0) ) partition by range(hiredate) ( partition p1 values less than( to_date('19810101','RRRRMMDD') ), partition p2 values less than( to_date('19820101','RRRRMMDD') ), partition p3 values less than( to_date('19830101','RRRRMMDD') ), partition p4 values less than( to_date('19840101','RRRRMMDD')), partition p5 values less than( maxvalue ) ); 12행에 오류: ORA-14120: DATE 열에 파티션 바운드가 불완전하게 지정되었습니다
     
    Q3.
    데이터를 이행하고 난 후에는 반드시 데이터가 정확하게 잘 입력되었는지 확인하는 작업을 해야함. 즉, emp와 emp_partition4의 개수가 동일한지 확인해야함. 확인하세요.
    select count(*) from emp; select count(*) from emp_partition4; -------------------------------------- SQL> select count(*) from emp; COUNT(*) ---------- 14 SQL> select count(*) from emp_partition4; COUNT(*) ---------- 14
    notion image
     
    Q4.
    파티션 별로 데이터가 어떻게 들어갔는지 확인하세요.
    #1. emp_partition4의 테이블의 통계정보를 수집 exec dbms_stats.gather_table_stats('c##scott', 'emp_partition4'); #2. 데이터 딕셔너리 조회 select table_name, partition_name, num_rows from user_tab_partitions where table_name = 'EMP_PARTITION4'; TABLE_NAME |PARTITION_NAME|NUM_ROWS| --------------+--------------+--------+ EMP_PARTITION4|P1 | 1| EMP_PARTITION4|P2 | 10| EMP_PARTITION4|P3 | 2| EMP_PARTITION4|P4 | 1| EMP_PARTITION4|P5 | 0|
    notion image
     

    튜닝84. 대용량 테이블을 불러와서 RANGE PARTITION 테이블을 생성할 수 있어야해요.

    📖
    대용량 테이블을 csv 파일을 사용해서 만들 것
    • ORDERS TABLE 생성하기 (ORDER라는 키워드는 오라클 내부 예약어라서 사용 불가)
      • create table orders ( order_id number(12, 0) not null , order_date timestamp(6) with local time zone not null , order_mode varchar2(8 byte) , customer_id number(6, 0) not null , order_status number(2, 0) , order_total number(8, 2) , sales_rep_id number(6, 0) , promotion_id number(6, 0) ) ;
     
    • ORDERS TABLE 생성 후 csv 데이터를 입력하시오.(데이터 로드) dbeaver에서 값 import (SQLdeveloper에서 데이터 임폴트 할때 날짜 형식을 YY/MM/DD:HH24:MI:SS.FX.... 로 해야함)
      • notion image
        orders.csv
        6.2KB
        혹은 create table orders as select * from oe.orders; select * from orders; select count(*) from orders;
    SQL> select count(*) from orders; COUNT(*) ---------- 105

    문제풀며 익히기

     
    Q1.
    order_date를 가지고 다음의 sql을 작성하시오.
    select to_char(order_date, 'YYYY'), count(*) from orders group by to_char(order_date, 'YYYY'); TO_CHAR( COUNT(*) -------- ---------- 2007 69 2008 19 2006 16 2004 1
    notion image
     
    Q2.
    위의 결과를 토대로 연도별 파티션 테이블 생성하시오.
    • PARTITION
      • 2004 → P1
        2005 → P2
        2006 → P3
        2007 → P4
        2008 → P5
    create table orders_partition ( order_id number(12, 0) not null , order_date timestamp(6) with local time zone not null , order_mode varchar2(8 byte) , customer_id number(6, 0) not null , order_status number(2, 0) , order_total number(8, 2) , sales_rep_id number(6, 0) , promotion_id number(6, 0) ) partition by range(order_date) (partition p1 values less than(to_date('20050101', 'YYYYMMDD')), partition p2 values less than(to_date('20060101', 'YYYYMMDD')), partition p3 values less than(to_date('20070101', 'YYYYMMDD')), partition p4 values less than(to_date('20080101', 'YYYYMMDD')), partition p5 values less than(to_date('20090101', 'YYYYMMDD')), partition p6 values less than(maxvalue)); !!ERROR!! 13행에 오류: ORA-30078: 분할영역 범위는 TIME/TIMESTAMP WITH TIME ZONE 리터럴이어야 합니다 ================아래와 같이 해줘야하 함======================= create table orders_partition ( order_id number(12, 0) not null , order_date date --데이터 형식 변경 , order_mode varchar2(8 byte) , customer_id number(6, 0) not null , order_status number(2, 0) , order_total number(8, 2) , sales_rep_id number(6, 0) , promotion_id number(6, 0) ) partition by range(order_date) (partition p1 values less than(to_date('20050101', 'YYYYMMDD')), partition p2 values less than(to_date('20060101', 'YYYYMMDD')), partition p3 values less than(to_date('20070101', 'YYYYMMDD')), partition p4 values less than(to_date('20080101', 'YYYYMMDD')), partition p5 values less than(to_date('20090101', 'YYYYMMDD')), partition p6 values less than(maxvalue)); insert into orders_partition select * from orders; 105 행이 생성되었습니다.
     
    Q3.
    orders 테이블에서 order_date가 2004년 데이터를 조회하는 아래의 sql을 튜닝하시오.
    <튜닝전> select count(*) from orders where order_date between to_date('20060101', 'YYYYMMDD') and to_date('20061231', 'YYYMMDD'); COUNT(*)| --------+ 16| ---------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ---------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 7 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 7 | |* 2 | FILTER | | 1 | | 16 |00:00:00.01 | 7 | |* 3 | TABLE ACCESS FULL| ORDERS | 1 | 16 | 16 |00:00:00.01 | 7 | ---------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - filter(TIMESTAMP' 2006-12-31 00:00:00'>=TIMESTAMP' 2006-01-01 00:00:00') 3 - filter(("ORDER_DATE">=TIMESTAMP' 2006-01-01 00:00:00' AND "ORDER_DATE"<=TIMESTAMP' 2006-12-31 00:00:00')) Note ----- - dynamic statistics used: dynamic sampling (level=2)
    notion image
    <튜닝후> select count(*) from orders_partition where order_date between to_date('20060101', 'YYYYMMDD') and to_date('20061231', 'YYYYMMDD'); COUNT(*) ---------- 16 ------------------------------------------------------------------------------------------------------ | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | ------------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 35 | | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 35 | | 2 | PARTITION RANGE SINGLE| | 1 | 1 | 16 |00:00:00.01 | 35 | |* 3 | TABLE ACCESS FULL | ORDERS_PARTITION | 1 | 1 | 16 |00:00:00.01 | 35 | ------------------------------------------------------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- 3 - filter("ORDER_DATE"<=TO_DATE(' 2006-12-31 00:00:00', 'syyyy-mm-dd hh24:mi:ss')) Note ----- - dynamic statistics used: dynamic sampling (level=2) =============================================================== PARTITION RANGE SINGLE : 해당하는 서랍만 읽었다는 의미. 이것에만 집중하기. sql자체는 이 경우 튜닝전이 더 좋은 실행계획을 가져옴
    notion image
     
     

    튜닝85. 파티션에 데이터가 골고루 분배 되게 하고 싶다면 HASH PARTITION을 만드세요.

    📖

    HASH PARTITION

    파티션 테이블의 데이터 검색 성능을 높이려면, 파티션마다 골고루 데이터가 분배되어야 한다. 그런데 해쉬 파티션은 오라클이 알아서 데이터를 골고루 분배 를 해줍니다. 하지만, RANGE PARTITION과 다르게 어떤 데이터가 어느 파티션에 들어있는지 바로 알 수 없는 단점이 있습니다. (즉, 그냥 분배만 잘되면 된다고 보는 PARTITON. 분배 잘되는 대신 비가시적임)
     

    HASH PARTITION 생성하기

    create table orders_hash ( order_id number(12, 0) not null , order_date date , order_mode varchar2(8 byte) , customer_id number(6, 0) not null , order_status number(2, 0) , order_total number(8, 2) , sales_rep_id number(6, 0) , promotion_id number(6, 0) ) partition by hash(order_date) partitions 5; 테이블이 생성되었습니다. --partition by hash를 해주고, 파티션의 개수만 정해주면 됨. insert into orders_hash select * from orders;
    -- 통계정보 수집-- exec dbms_stats.gather_table_stats('c##scott', 'orders_hash'); --파티션 분배 확인하기-- select table_name, partition_name, num_rows from user_tab_partitions where table_name = 'ORDERS_HASH'; TABLE_NAME |PARTITION_NAME|NUM_ROWS| -----------+--------------+--------+ ORDERS_HASH|SYS_P761 | 17| ORDERS_HASH|SYS_P762 | 33| ORDERS_HASH|SYS_P763 | 23| ORDERS_HASH|SYS_P764 | 26| ORDERS_HASH|SYS_P765 | 6| 알아서 파티션 명을 만들어 최대한 파티션 별 알맞게 분배해줌
    notion image
     
     

    문제풀며 익히기

     
    Q1.
    emp 테이블로 emp_hash 파티션 테이블을 생성하는데 파티션 키를 job 으로 해서 생성하시오! (해쉬 파티션으로 생성하세요)
    create table emp_hash (empno number(4,0), ename varchar2(10), job varchar2(9), mgr number(4,0), hiredate date, sal number(7,2), comm number(7,2), deptno number(2,0) ) partition by hash(job) partitions 3; insert into emp_hash select * from emp; 14 행이 생성되었습니다. -- 통계정보 수집-- exec dbms_stats.gather_table_stats('c##scott', 'emp_hash'); --파티션 분배 확인하기-- select table_name, partition_name, num_rows from user_tab_partitions where table_name = 'EMP_HASH'; TABLE_NAME|PARTITION_NAME|NUM_ROWS| ----------+--------------+--------+ EMP_HASH |SYS_P766 | 0| EMP_HASH |SYS_P767 | 10| EMP_HASH |SYS_P768 | 4|
    notion image
     
    Q2. 아래의 SQL을 튜닝하시오.
    작은 테이블의 경우 오히려 버퍼의 개수가 많아질 수 있음. PARTITION HASH SINGLE 이 실행계획에 나온 것에 집중하기 이는 다른 것은 읽지 않고 해당하는 파티션만 읽었다는 의미임
    <튜닝전> select ename, sal, job from emp where job = 'PRESIDENT'; ENAME|SAL |JOB | -----+----+---------+ KING |5000|PRESIDENT| --dbeaver에서 실행계획 확인 과정 -- select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('62j961yxrngm5',0,'ALLSTATS LAST')); ------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 6 || |* 1 | TABLE ACCESS FULL| EMP | 1 | 3 | 1 |00:00:00.01 | 6 || ------------------------------------------------------------------------------------|
    notion image
    <튜닝후> select ename, sal, job from emp_hash where job = 'PRESIDENT'; --dbeaver에서 실행계획 확인 과정-- select sql_id, child_number, sql_text from v$sql where sql_text like '%emp%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('5u3wdzjbyhj75',0,'ALLSTATS LAST')); --------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || --------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 36 || | 1 | PARTITION HASH SINGLE| | 1 | 3 | 1 |00:00:00.01 | 36 || |* 2 | TABLE ACCESS FULL | EMP_HASH | 1 | 3 | 1 |00:00:00.01 | 36 || --------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 2 - filter("JOB"='PRESIDENT') | |
    notion image
     
    실습 위해 준비해야할 데이터 : 대용량 orders table 로 재생성
    drop table orders; CREATE TABLE ORDERS AS WITH TEMP_ORDERS AS( SELECT /*+ MATERIALIZE */ TRUNC(DBMS_RANDOM.VALUE(1, 2193)) ORDER_DATE_CD , DECODE(TRUNC(DBMS_RANDOM.VALUE(0, 2)), 1, 'direct', 'online') ORDER_MODE , 'C'||LPAD(TRUNC(DBMS_RANDOM.VALUE(1, 50001)), 5, '0') CUSTOMER_ID , 'E'||LPAD(TRUNC(DBMS_RANDOM.VALUE(1, 643)), 3, '0') EMPLOYEE_ID , TRUNC(DBMS_RANDOM.VALUE(1, 11)) ORDER_STATUS , 0 ORDER_TOTAL FROM DUAL CONNECT BY LEVEL <= 1000000) SELECT 'O'||LPAD(ROW_NUMBER() OVER(ORDER BY A.ORDER_DATE_CD), 8, '0') ORDER_ID , TO_DATE(B.DATE_ID||LPAD(TRUNC(DBMS_RANDOM.VALUE(0, 24)), 2, '0')|| LPAD(TRUNC(DBMS_RANDOM.VALUE(0, 60)), 2, '0')|| LPAD(TRUNC(DBMS_RANDOM.VALUE(0, 60)), 2, '0'), 'YYYYMMDDHH24MISS') ORDER_DATE , ORDER_MODE, CUSTOMER_ID, EMPLOYEE_ID, ORDER_STATUS, ORDER_TOTAL FROM TEMP_ORDERS A , (SELECT /*+ NO_MERGE */ ROWNUM DATE_CD , YYYY||MM||DD DATE_ID FROM (SELECT LPAD(ROWNUM,2,'0') MM FROM DUAL CONNECT BY LEVEL <= 12) , (SELECT LPAD(ROWNUM,2,'0') DD FROM DUAL CONNECT BY LEVEL <= 31) , (SELECT ROWNUM + 2006 YYYY FROM DUAL CONNECT BY LEVEL <= 6) WHERE DD<=DECODE(MM,'01','31','03','31','04','30','05','31','06','30', '07','31','08','31','09','30','10','31','11','30','12','31', '02',DECODE(MOD(TO_NUMBER(YYYY),4),0,'29','28')) ) B WHERE A.ORDER_DATE_CD = B.DATE_CD;
    SQL> select count(*) from orders; COUNT(*) ---------- 1000000
     

    튜닝85-1 HASH PARTITION 대용량 테이블로 실행해본 버전/파티션 pruning - 자동화 스크립트 포함

    문제풀며 익히기

     
    Q1. ORDERS table을 이용하여 orders_hash_table을 생성하시오.
    • 조건
      • 파티션의 개수 : 40개 —임의로 정하면 됨
      • 파티션 키 컬럼 : order_date
    -- 파티션 테이블 생성-- create table orders_hash_table ( order_id varchar2(17), order_date date, order_mode varchar2(6), customer_id varchar2(11), employee_id varchar2(7), order_status number, order_total number ) partition by hash(order_date) partitions 40; 테이블이 생성되었습니다.
     
    Q2. ORDERS 테이블의 데이터를 orders_hash_table에 입력하시오.
    set timing on insert into orders_hash_table select * from orders; 1000000 행이 생성되었습니다. 경 과: 00:00:05.84
     
    Q3. orders_hash_table에 데이터가 파티션 마다 잘 분배되었는지 확인하시오.
    exec dbms_stats.gather_table_stats('c##scott','emp_hash'); select table_name, partition_name, num_rows from user_tab_partitions where table_name = 'EMP_HASH';
    --통계정보 수집 PL/SQL로 만들기-- ed t.sql set verify off accept p_table prompt '통계정보를 수집할 테이블명을 입력하세요 : ' declare v_table varchar2(20) := '&p_table'; begin dbms_stats.gather_table_stats(user,v_table); end; /
    notion image
    --파티션 테이블의 데이터 분배 확인하는 스크립트 pl/sql로 저장하기-- ed pa.sql set verify off set serveroutput on accept p_table prompt '파티션 테이블명을 입력하세요 ~' begin for r in ( select table_name, partition_name, num_rows from user_tab_partitions where table_name= upper('&p_table') ) loop dbms_output.put_line( r.table_name || chr(9) || r.partition_name || chr(9) || r.num_rows ); end loop; end; /
     
    Q4. 아래의 sql을 튜닝하시오.
    (날짜가 랜덤으로 생성되니까 각자 자리에 있는 날짜로 해야함)
    <튜닝전> --날짜 하나 추출 -- SELECT ORDER_date FROM orders ORDER BY ORDER_date DESC FETCH FIRST 1 ROWS ONLY; ORDER_DATE | -----------------------+ 2012-12-31 23:58:59.000| select * from orders where order_date = to_date('2012-12-31:235859', 'RRRR-MM-DD:HH24MISS'); ORDER_ID |ORDER_DATE |ORDER_MODE|CUSTOMER_ID|EMPLOYEE_ID|ORDER_STATUS|ORDER_TOTAL| ---------+-----------------------+----------+-----------+-----------+------------+-----------+ O00999929|2012-12-31 23:58:59.000|online |C33555 |E132 | 1| 0| select sql_id, child_number, sql_text from v$sql where sql_text like '%orders%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('7q6wfddd9r1g3',0,'ALLSTATS LAST')); --------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || --------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.02 | 6544 || |* 1 | TABLE ACCESS FULL| ORDERS | 1 | 1 | 1 |00:00:00.02 | 6544 || --------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 1 - filter("ORDER_DATE"=TO_DATE('2012-12-31:235859','RRRR-MM-DD:HH24MISS')) | |
    버퍼의 개수 : 6544개
    notion image
    <튜닝후> select * from orders_hash_table where order_date = to_date('2012-12-31:235859', 'RRRR-MM-DD:HH24MISS'); select sql_id, child_number, sql_text from v$sql where sql_text like '%orders%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('8fhz0rj17bgbu',0,'ALLSTATS LAST')); ----------------------------------------------------------------------------------------------------| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ----------------------------------------------------------------------------------------------------| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 276 || 1 | PARTITION HASH SINGLE| | 1 | 1 | 1 |00:00:00.01 | 276 || * 2 | TABLE ACCESS FULL | ORDERS_HASH_TABLE | 1 | 1 | 1 |00:00:00.01 | 276 || ----------------------------------------------------------------------------------------------------| | redicate Information (identified by operation id): | -------------------------------------------------- | | 2 - filter("ORDER_DATE"=TO_DATE('2012-12-31:235859','RRRR-MM-DD:HH24MISS')) |
    버퍼의 개수 : 276개
    notion image
     
     

    주의사항

    SQLP 시험문제 ★
    ※ 파티션 pruning 이 안되는 경우 ? ★ SQLP 시험문제
    PRUNE : 쓸데없는 가지를 치다. 불필요한 부분을 제거하다.
    1. where 절 컬럼이 가공되었을 때
    1. 등치 조건이 아닐때 (hash 파티션일 때 만)
     
    ※참고사항(주의)
    hash 파티션이 아닌경우에는 등치 조건 아니라도 single을 탈 수 있음 또한 WHERE절에 조건이 여러개라도 PARTITION RANGE SINGLE을 탈 수 있음.
    예를들어 between.. and로 조건을 주어도, 그 조건이 한 파티션 안에 있다면, single 가능
    반면에 그 범위가 여러 파티션을 걸쳐 있다면, PARTITION RANGE ALL을 타게 됨
    EX)
    < 파티션 pruning 되는 경우 > select * from orders_hash_table where order_date = to_date('2012-12-31:235859', 'RRRR-MM-DD:HH24MISS'); --DBeaver 실행계획 확인 과정-- select sql_id, child_number, sql_text from v$sql where sql_text like '%orders_hash%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('8fhz0rj17bgbu',0,'ALLSTATS LAST')); -----------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || -----------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 276 || | 1 | PARTITION HASH SINGLE| | 1 | 1 | 1 |00:00:00.01 | 276 || |* 2 | TABLE ACCESS FULL | ORDERS_HASH_TABLE | 1 | 1 | 1 |00:00:00.01 | 276 || -----------------------------------------------------------------------------------------------------| ===================================================== **PARTITION HASH SINGLE** 파티션 푸루닝 해서 해당 파티션만 읽은것. 만약에 all이라면 파티션 푸루닝을 못해서 모든 파티션을 읽은 것 |
    notion image
    < 파티션 pruning 안되는 경우 : where절 가공 > SELECT * FROM orders_hash_table WHERE to_char(order_date, 'RRRR-MM-DD') = '2007-01-01'; select sql_id, child_number, sql_text from v$sql where sql_text like '%orders_hash%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('6cvb33pss40p6',0,'ALLSTATS LAST')); -----------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads || -----------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 200 |00:00:00.06 | 3902 | 3504 || | 1 | PARTITION HASH ALL| | 1 | 10000 | 200 |00:00:00.06 | 3902 | 3504 || |* 2 | TABLE ACCESS FULL| ORDERS_HASH_TABLE | 18 | 10000 | 200 |00:00:00.06 | 3902 | 3504 || -----------------------------------------------------------------------------------------------------------| ==================================== ** PARTITION HASH ALL ** 파티션 pruning을 못해서 모든 파티션을 읽은 것
    notion image
     

    튜닝86. 리스트 파티션(LIST PARTITION)을 만드세요! -SQLP시험에 출제되는 파티션

    📖

    LIST PARTITION

    사용자에 의해서 정해진 그룹핑된 기준에 따라 데이터를 분할 저장하는 파티션
    ex) 우리반 테이블에 통신사를 파티션 키로해서 분할 하는 것
     

    LIST PARTITION 만들기 실습

    # 1. order_status를 키로하는 list partition 만들기 -- order_status 데이터 형태 살펴보기 -- SELECT order_status, count(*) FROM ORDERS GROUP BY order_status; ORDER_STATUS|COUNT(*)| ------------+--------+ 9| 99548| 7| 100264| 3| 99966| 1| 100160| 5| 99984| 8| 99575| 6| 99934| 2| 100241| 10| 100242| 4| 100086| -- order_status를 키로하는 list partition 만들기 create table orders_list_parti partition by list(order_status) ( partition p_1 values (1), partition p_2 values (2), partition p_3 values (3), partition p_4 values (4), partition p_5 values (5), partition p_6 values (6), partition p_7 values (7), partition p_8 values (8), partition p_9 values (9), partition p_10 values (10) ) as select * from orders; /* values 옆 괄호에 order_status의 값을 넣어둔 것인데, 숫자라 싱글쿼테이션 없음 */
    notion image
    #2. orders_list_parti 테이블의 파티션의 데이터가 골고루 분배되었는지 확인하시오. SQL> @t 통계정보를 수집할 테이블명을 입력하세요 : orders_list_parti PL/SQL 처리가 정상적으로 완료되었습니다. 경 과: 00:00:01.32 SQL> @pa 파티션 테이블명을 입력하세요 : orders_list_parti ORDERS_LIST_PARTI P_1 100160 ORDERS_LIST_PARTI P_10 100242 ORDERS_LIST_PARTI P_2 100241 ORDERS_LIST_PARTI P_3 99966 ORDERS_LIST_PARTI P_4 100086 ORDERS_LIST_PARTI P_5 99984 ORDERS_LIST_PARTI P_6 99934 ORDERS_LIST_PARTI P_7 100264 ORDERS_LIST_PARTI P_8 99575 ORDERS_LIST_PARTI P_9 99548 PL/SQL 처리가 정상적으로 완료되었습니다. 경 과: 00:00:00.02
    notion image
     
    예제1)
    employee_id가 E520 이고 order_status가 1이고, order_total 이 8353인 주문 data의 모든 컬럼을 출력하시오!
    (값이 랜덤으로 생성되기 때문에, 각자 컴퓨터에서 값을 조회해 그 중 하나의 값을 선택해서 해보시오)
    <튜닝전> SELECT employee_id, order_status, order_total FROM ORDERS ORDER BY employee_id DESC FETCH FIRST 1 ROWS ONLY; EMPLOYEE_ID|ORDER_STATUS|ORDER_TOTAL| -----------+------------+-----------+ E642 | 2| 0| select * from orders where employee_id = 'E642' and order_status = 2 and order_total = 0; select sql_id, child_number, sql_text from v$sql where sql_text like '%orders%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('cdmmkuuq6m8xf',0,'ALLSTATS LAST')); --------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || --------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 170 |00:00:00.02 | 6561 || |* 1 | TABLE ACCESS FULL| ORDERS | 1 | 156 | 170 |00:00:00.02 | 6561 || --------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 1 - filter(("EMPLOYEE_ID"='E642' AND "ORDER_STATUS"=2 AND "ORDER_TOTAL"=0)) | |
    버퍼의 개수 : 6561개
    notion image
    <튜닝후> select * from orders_list_parti where employee_id = 'E642' and order_status = 2 and order_total = 0; select sql_id, child_number, sql_text from v$sql where sql_text like '%orders%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('c0fc93g59a5cw',0,'ALLSTATS LAST')); -----------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || -----------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 170 |00:00:00.01 | 674 || | 1 | PARTITION LIST SINGLE| | 1 | 156 | 170 |00:00:00.01 | 674 || |* 2 | TABLE ACCESS FULL | ORDERS_LIST_PARTI | 1 | 156 | 170 |00:00:00.01 | 674 || -----------------------------------------------------------------------------------------------------| | Predicate Information (identified by operation id): | --------------------------------------------------- | | 2 - filter(("EMPLOYEE_ID"='E642' AND "ORDER_TOTAL"=0)) | |
    버퍼의 개수 : 674개
    notion image
     

    문제풀며 익히기

     
    Q1.
    우리반 테이블의 통신사 컬럼을 파티션 키로 해서 리스트 파티션을 생성하시오 - 파티션 이름 : emp18_list
    create table emp18_list partition by list(telecom) ( partition p_1 values ('sk'), partition p_2 values ('lg'), partition p_3 values ('kt'), partition p_4 values ('알뜰kt') ) as select * from emp18; SQL> @t 통계정보를 수집할 테이블명을 입력하세요 : emp18_list PL/SQL 처리가 정상적으로 완료되었습니다. 경 과: 00:00:00.08 SQL> @pa 파티션 테이블명을 입력하세요 : emp18_list EMP18_LIST P_1 12 EMP18_LIST P_2 9 EMP18_LIST P_3 7 EMP18_LIST P_4 1 PL/SQL 처리가 정상적으로 완료되었습니다.
     
     

    튜닝87. 결합 파티션(복합 파티션)을 만드세요! -

    📖

    결합 파티션(복합 파티션)

    파티션 키 컬럼이 주 파티션 키 컬럼과 보조 파티션 키 컬럼으로 나뉘는 파티션 ex) 우리반 테이블 = 주 파티션 키 : 통신사, 보조 파티션 키 : 서울,경기,인천
    notion image
     

    결합 파티션(복합 파티션) 생성 실습

    CREATE TABLE orders_range_list_parti partition BY range(order_date) --주 파티션 subpartition BY list(order_status) --보조 파티션 --보조 파티션 정의-- subpartition template ( subpartition l1 VALUES (1), subpartition l2 VALUES (2), subpartition l3 VALUES (3), subpartition l4 VALUES (4), subpartition l5 VALUES (5), subpartition l6 VALUES (6), subpartition l7 VALUES (7), subpartition l8 VALUES (8), subpartition l9 VALUES (9), subpartition l10 VALUES (10), subpartition l11 VALUES (DEFAULT) ) /* -- 보조 파티션의 값에 속하지 않는 데이터는 여기(111)로 들어감 ex. 11번은 보조 파티션이 없으므로 이곳에 들어가는 것*/ --주 파티션 정의-- ( partition Q1_1201 VALUES less than ( to_date('20120201','YYYYMMDD') ), partition Q1_1202 VALUES less than ( to_date('20120301','YYYYMMDD') ), partition Q1_1203 VALUES less than ( to_date('20120401','YYYYMMDD') ), partition Q1_1204 VALUES less than ( to_date('20120501','YYYYMMDD') ), partition Q1_1205 VALUES less than ( to_date('20120601','YYYYMMDD') ), partition Q1_1206 VALUES less than ( to_date('20120701','YYYYMMDD') ), partition Q1_1207 VALUES less than ( to_date('20120801','YYYYMMDD') ), partition Q1_1208 VALUES less than ( to_date('20120901','YYYYMMDD') ), partition Q1_1209 VALUES less than ( to_date('20121001','YYYYMMDD') ), partition Q1_1210 VALUES less than ( to_date('20121101','YYYYMMDD') ), partition Q1_1211 VALUES less than ( to_date('20121201','YYYYMMDD') ), partition Q1_1212 VALUES less than ( maxvalue ) ) as select * from orders; /* -- 주 파티션의 값에 속하지 않는 데이터는 여기로(Q1_1212) 들어감 ex. 2013년 */
     

    문제풀며 익히기

     
    Q1. 아래의 sql의 성능을 비교하시오.
    <튜닝전> select count(*) from orders where order_status = 3; COUNT(*)| --------+ 99966| select sql_id, child_number, sql_text from v$sql where sql_text like '%orders%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('cs1303ptgvrw1',0,'ALLSTATS LAST')); | ---------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers || ---------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.02 | 6544 || | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.02 | 6544 || |* 2 | TABLE ACCESS FULL| ORDERS | 1 | 100K| 99966 |00:00:00.02 | 6544 || ---------------------------------------------------------------------------------------| |
    버퍼의 개수 : 6544개
    notion image
    <튜닝후> select count(*) from orders_range_list_parti where order_status = 3; select sql_id, child_number, sql_text from v$sql where sql_text like '%orders%' AND sql_text NOT LIKE '%v$sql%' ORDER BY last_load_time DESC; SELECT * FROM TABLE(dbms_xplan.display_cursor('bpnq0hxxaysp4',0,'ALLSTATS LAST')); ----------------------------------------------------------------------------------------------------------------------| | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads || ----------------------------------------------------------------------------------------------------------------------| | 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 672 | 660 || | 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 672 | 660 || | 2 | PARTITION RANGE ALL | | 1 | 100K| 99966 |00:00:00.01 | 672 | 660 || | 3 | PARTITION LIST SINGLE| | 12 | 100K| 99966 |00:00:00.01 | 672 | 660 || | 4 | TABLE ACCESS FULL | ORDERS_RANGE_LIST_PARTI | 12 | 100K| 99966 |00:00:00.01 | 672 | 660 || ----------------------------------------------------------------------------------------------------------------------| |
    버퍼의 개수 : 672개
    notion image
     
     
     

    튜닝88. DAB는 추가적으로 INTERVAL 파티션을 알고 있어야 합니다. - DBA에게 유용한 파티션 ★

    📖

    INTERVAL PARTITION

    interval partition이란 range파티션의 파티션 기준에 따라서(일단위 파티션, 월단위 파티션) 파티션이 자동으로 추가되는 기능
    ex) 현재가 5월 달인데, 6월 달이 되면, 6월 달에 data를 입력하는 순간 6월 달 데이터를 담기 위한 새로운 파티션이 자동으로 생성된다.(dba가 파티션 관리하기 너무 편함)
     

    INTERVAL PARTITION 생성 실습

    create table order33 (order_no number, order_date date ) partition by range(order_date) INTERVAL (NUMTOYMINTERVAL(1, 'MONTH')) ( partition p200907 values less than(to_date('2023/09/01', 'yyyy/mm/dd')) , partition p200908 values less than(to_date('2023/10/01', 'yyyy/mm/dd')) , partition p200909 values less than(to_date('2023/11/01', 'yyyy/mm/dd')) , partition p200910 values less than(to_date('2023/12/01', 'yyyy/mm/dd')) , partition p200911 values less than(to_date('2024/01/01', 'yyyy/mm/dd')) ); -- 파티션에 해당되지 않는 값 넣어보기 insert into order33 values(9, to_date('2024/01/03', 'yyyy/mm/dd')); insert into order33 values(9, to_date('2024/02/06', 'yyyy/mm/dd')); ------------------------없는 값 넣어보기------------------------ SQL> insert into order33 2 values(9, to_date('2024/01/03', 'yyyy/mm/dd')); 1 개의 행이 만들어졌습니다. 경 과: 00:00:00.01 SQL> insert into order33 2 values(9, to_date('2024/02/06', 'yyyy/mm/dd')); 1 개의 행이 만들어졌습니다. 경 과: 00:00:00.01 => 에러 발생하지 않고 잘 들어감 ---------------새로운 파티션이 자동으로 생성됐는지 확인하기----------------- SQL> @t 통계정보를 수집할 테이블명을 입력하세요 : order33 PL/SQL 처리가 정상적으로 완료되었습니다. 경 과: 00:00:00.04 SQL> @pa 파티션 테이블명을 입력하세요 : order33 ORDER33 P200907 0 ORDER33 P200908 0 ORDER33 P200909 0 ORDER33 P200910 0 ORDER33 P200911 0 ORDER33 SYS_P810 1 ORDER33 SYS_P811 1 PL/SQL 처리가 정상적으로 완료되었습니다. ================================================ 새로운 파티션이 자동으로 생성됨.
     

    문제풀며 익히기

     
     
    Q1.

    튜닝89. 파티션 테이블에 파티션 인덱스를 생성하세요.-DBA가 반드시 알고 있어야 함★★★(SQLP에도 출제)(성능 고도화 책의 맨 658p에 있는 개념)

    📖

    PARTITION INDEX

    성능 고도화 책 2권의 658페이지(★SQLP시험. 그나마 쉽게 나옴)
    notion image
    비파티션 테이블 : 파티션이 아닌 테이블 파티션 테이블 : 파티션인 테이블(그림에서는 월 별로 나뉨)
    로컬 파티션은 파티션과 인덱스의 파티션의 개수가 같음. 글로벌 파티션은 파티션과 인덱스의 파티션 개수가 다름. 그래서 아예 거의 안쓴다고 보면 됨.
     
    따라서 파티션에 인덱스를 건다면, 로컬 파티션이나 비파티션 인덱스 사용
    ex)
    notion image
     

    정리

    현업에서 주로 로컬 파티션 인덱스를 사용하는 편입니다. 글로벌 파티션 인덱스와 비 파티션 인덱스는 거의 사용하지 않는 편입니다.

    파티션 인덱스에서 로컬 파티션 인덱스 vs 글로벌 파티션 인덱스

    1. LOCAL PARTITION INDEX (현업에서 가장 많이 사용) : 테이블의 파티션 개수와 인덱스의 파티션 개수가 서로 일치
    1. 글로벌 파티션 인덱스 : 테이블의 파티션 개수와 인덱스의 파티션 개수가 서로 불일치
     
    • 시험문제 1 : 로컬 파티션 인덱스란?
      • 테이블의 파티션의 개수와 인덱스의 파티션 개수가 서로 일치하고, 서로 1:1대응관계를 가진다.
    • 시험문제 2 : 로컬 파티션 인덱스의 장점은 무엇인가?
      • 파티션 구성 변경이 생기더라도 인덱스를 재생성할 필요가 없습니다. 즉, 테이블에 해당 파티션이 없어지면, 오라클이 알아서 관련된 로컬 파티션 인덱스를 삭제합니다.
        파티션 테이블도 너무 커지면 안되니까 DROP하는 경우가 있는데, 로컬 파티션 인덱스는 해당 파티션만 DROP했을 경우 로컬 파티션 인덱스도 같이 DROP됨. 글로벌 파티션 인덱스는 같이 DROP되지 않아서 하나하나 해줘야함.
         
        글로벌 파티션 인덱스의 단점:  
        기존 파티션 테이블의 파티션을 drop하면 인덱스가 unusable  로 상태가 변경되기 때문에 그때마다 인덱스를 재생성해야한다는것 입니다.

    예제

     
    예제1) 아래의 리스트 파티션을 다시 확인 하시오
    create table emp18_list partition by list(telecom) ( partition p_1 values ('sk'), partition p_2 values ('lg'), partition p_3 values ('kt'), partition p_4 values ('알뜰kt') ) as select * from emp18;
     
    예제2) 우리반 테이블을 파티셔닝한 파티션 이름을 확인하기
    SQL> @t 통계정보를 수집할 테이블명을 입력하세요 : emp18_list PL/SQL 처리가 정상적으로 완료되었습니다. 경 과: 00:00:00.07 SQL> @pa 파티션 테이블명을 입력하세요 : emp18_list EMP18_LIST P_1 12 EMP18_LIST P_2 9 EMP18_LIST P_3 7 EMP18_LIST P_4 1 PL/SQL 처리가 정상적으로 완료되었습니다. 경 과: 00:00:00.00 SQL>
     
    예제3) emp18_list에 로컬 파티션 인덱스를 생성하시오,
    create index emp18_list_local_indx on emp18_list(telecom) local; --마지막에 local을 붙여주면 됨.
    예제4) 예제 3번에서 생성한 local파티션 인덱스의 파티션 개수 확인하시오.
    • 테이블의 파티션의 개수 = 인덱스의 파티션의 개수
    exec dbms_stats.gather_index_stats('c##scott', 'emp18_list_local_indx'); select index_name, partition_name, status from user_ind_partitions where index_name = upper('emp18_list_local_indx'); INDEX_NAME |PARTITION_NAME|STATUS| ---------------------+--------------+------+ EMP18_LIST_LOCAL_INDX|P_1 |USABLE| EMP18_LIST_LOCAL_INDX|P_2 |USABLE| EMP18_LIST_LOCAL_INDX|P_3 |USABLE| EMP18_LIST_LOCAL_INDX|P_4 |USABLE| ================================= 인덱스의 파티션의 개수도 4개 이므로, 파티션의 파티션의 개수와 동일한 것을 알 수 있음 여기서 주목해야할 것은 상태(status)가 USABLE인 것을 확인해두기.
    notion image
     
    예제5) KT파티션을 테이블에서 삭제하시오.
    ----파티션 명 확인---- SQL> @pa 파티션 테이블명을 입력하세요 : emp18_list EMP18_LIST P_1 12 --sk EMP18_LIST P_2 9 --lg EMP18_LIST P_3 7 --kt EMP18_LIST P_4 1 --알뜰kt -------------kt 파티션 drop하기--------------- alter table emp18_list drop partition p_3; select * from emp18_list; EMPNO|ENAME|GENDER|BIRTH |TELECOM|MAJOR |BEFORE_JOB |EMAIL |ADDRESS |AGE|T_PRICE| -----+-----+------+-----------------------+-------+-------+------------+-------------------------+---------+---+-------+ 2|전수진 |여 |1992-12-08 00:00:00.000|sk |동양화과 |사무직 |grdycorvina@gmail.com |서울특별시 송파구| 30| 17000| 5|서형진 |남 |1993-06-11 00:00:00.000|sk |문예창작과 |웹 개발자 |shj984@daum.net |서울시 용산구 | 30| 17000| 19|이승학 |남 |1998-09-14 00:00:00.000|sk |정보통신공학과|학생 |ok98lee@naver.com |경기도 용인시 | 25| 17000| 8|박성환 |남 |1996-09-25 00:00:00.000|sk |수학과 |회사원 |harrypark960925@gmail.com|강남구 역삼동 | 27| 17000| 6|홍귀표 |남 |1991-02-08 00:00:00.000|sk |토목환경공학과|직업군인 |hgp17759@gmail.com |경기도 용인시 | 32| 17000| 24|전시유 |남 |1998-04-21 00:00:00.000|sk |보건행정학과 |웹 개발자 |a2106853@naver.com |서울시 강서구 | 25| 17000| 27|오정훈 |남 |1996-10-29 00:00:00.000|sk |정보통신과 |클라우드 엔지니어 |5wjdgns2@naver.com |서울시 관악구 | 27| 17000| 17|서희운 |남 |1995-10-09 00:00:00.000|sk |산업경영공학과|학생 |shw9510@gmail.com |서울시 서초구 | 28| 17000| 15|강민지 |여 |1998-06-08 00:00:00.000|sk |경찰학전공 |학생 |mjkang001@naver.com |대전광역시 유성구| 25| 17000| 28|최순애 |여 |1977-11-20 00:00:00.000|sk |컴퓨터공학과 |프로그래머(퇴사) |asting26@naver.com |경기도 과천시 | 46| 17000| 7|김준환 |남 |1993-12-17 00:00:00.000|sk |태권도학과 |요리사 |kimjunhwan11@nate.com |서울시 강북구 | 29| 17000| 9|김이현 |여 |1995-06-15 00:00:00.000|sk |식품영양학과 |대통령실 홈페이지 운영|kimlh95@naver.com |서울시 강남구 | 28| 17000| 3|오운학 |남 |1996-11-13 00:00:00.000|lg |제과제빵학과 |제빵사 |11tazan@naver.com |경기도 안양시 | 27| 16000| 20|손혜민 |여 |1997-11-02 00:00:00.000|lg |사회복지와심리|사회복지사 |hyemiin0406@daum.net |경기도 용인시 | 26| 16000| 22|나윤호 |남 |1994-05-25 00:00:00.000|lg |항공우주기계 |단기계약직 |rosensierra@naver.com |서울시 서대문구 | 29| 16000| 16|이남화 |남 |1996-01-11 00:00:00.000|lg |산업경영공학과|생산관리 |dlskaghk1@naver.com |경기도 용인시 | 27| 16000| 29|노찬혁 |남 |2000-10-23 00:00:00.000|lg |X |반도체설비셋업 |chanhyuk2218@gmail.com |경기도 용인시 | 23| 16000| 12|진현수 |여 |1992-12-23 00:00:00.000|lg |식품영양학과 |영양사 |hyunsoo9039@naver.com |경기도 군포시 | 30| 16000| 25|정슬기 |여 |1996-05-10 00:00:00.000|lg |법학과 |편집디자이너 |jsulgi96051@naver.com |경기도 성남시 | 27| 16000| 18|하상우 |남 |1996-10-22 00:00:00.000|lg |관광경영학과 |무역 | hasw1022@naver.com | 인천 논현동 | 27| 16000| 11|유일 |남 |1996-11-28 00:00:00.000|lg |신학과 |전기관련업무 |yuil5699@naver.com |경기도 시흥시 | 27| 16000| 10|김영훈 |남 |1992-05-13 00:00:00.000|알뜰kt |경영학과 |영어 과외선생님 |yhk9205@naver.com |경기도 성남시 | 31| 15000|
     
     
    예제6)
    파티션을 drop해도 local파티션의 인덱스의 상태가 정상인지 확인하시오.
    select index_name, partition_name, status from user_ind_partitions where index_name = upper('emp18_list_local_indx'); INDEX_NAME |PARTITION_NAME|STATUS| ---------------------+--------------+------+ EMP18_LIST_LOCAL_INDX|P_1 |USABLE| EMP18_LIST_LOCAL_INDX|P_2 |USABLE| EMP18_LIST_LOCAL_INDX|P_4 |USABLE| =============================================== P_3 로컬 파티션 인덱스가 사라진 것을 알 수 있음 나머지는 상태 정상으로 계속 사용 가능
    해당 파티션을 drop해도 로컬 파티션 인덱스의 상태는 정상입니다. 그래서 DBA들이 좋아하는 것
    notion image
     
     
     

    문제풀며 익히기

     
    Q1.
    EMP테이블의 DEPTNO를 파티션 키로 해서 리스트 파티션을 생성하세요. - 파티션 이름 : EMP_DEPTNO_LIST
    ----list partition 생성---- create table emp_deptno_list partition by list(deptno) ( partition p_10 values (10), partition p_20 values (20), partition p_30 values (30) ) as select * from emp; ----확인하기---- select * from emp_deptno_list; EMPNO|ENAME |JOB |MGR |HIREDATE |SAL |COMM|DEPTNO| -----+------+---------+----+-----------------------+----+----+------+ 7839|KING |PRESIDENT| |1981-11-17 00:00:00.000|5000| | 10| 7782|CLARK |MANAGER |7839|1981-05-09 00:00:00.000|2450| | 10| 7934|MILLER|CLERK |7782|1982-01-11 00:00:00.000|1300| | 10| 7566|JONES |MANAGER |7839|1981-04-01 00:00:00.000|2975| | 20| 7902|FORD |ANALYST |7566|1981-12-11 00:00:00.000|3000| | 20| 7369|SMITH |CLERK |7902|1980-12-09 00:00:00.000| 800| | 20| 7788|SCOTT |ANALYST |7566|1982-12-22 00:00:00.000|3000| | 20| 7876|ADAMS |CLERK |7788|1983-01-15 00:00:00.000|1100| | 20| 7698|BLAKE |MANAGER |7839|1981-05-01 00:00:00.000|2850| | 30| 7654|MARTIN|SALESMAN |7698|1981-09-10 00:00:00.000|1250|1400| 30| 7499|ALLEN |SALESMAN |7698|1981-02-11 00:00:00.000|1600| 300| 30| 7844|TURNER|SALESMAN |7698|1981-08-21 00:00:00.000|1500| 0| 30| 7900|JAMES |CLERK |7698|1981-12-11 00:00:00.000| 950| | 30| 7521|WARD |SALESMAN |7698|1981-02-23 00:00:00.000|1250| 500| 30|
    notion image
     
    Q2.
    emp_deptno_list 파티션 테이블의 deptno에 로컬 파티션 인덱스를 생성하시오.
    • 파티션 인덱스 이름 : emp_list_local_indx
    create index emp_list_local_indx on emp_deptno_list(deptno) local; 인덱스가 생성되었습니다.
     
     

    튜닝90. 저장 공간을 절약하고 싶다면, 글로벌 파티션 인덱스를 생성하세요.

    📖
    notion image
    로컬 파티션 인덱스는 저장공간이 많이 사용되기 때문에, 저장공간을 절약하고 싶다면, 글로벌 파티션 인덱스를 사용하시오.

    글로벌 파티션 인덱스의 단점

    기존 파티션 테이블의 파티션을 drop하면 , 인덱스가 unusable로 상태가 변경되기 때문에 그때마다 인덱스를 재생성 해야 한다는 것
     

    글로벌 파티션 인덱스 생성 실습

    drop table emp_deptno_list; ----list partition 생성----- create table emp_deptno_list partition by list(deptno) ( partition p_10 values (10), partition p_20 values (20), partition p_30 values (30) ) as select * from emp; ---- list partition의 deptno에 글로벌 파티션 인덱스 생성----- create index emp_deptno_list_global on emp_deptno_list(deptno) global partition by hash(deptno) partitions 2; ======================================== hash partition으로 만들어줬음 파티션의 개수는 2개로 지정해줌 테이블의 파티션 개수는 3개인데, 글로벌 파티션 인덱스의 개수는 2개
    alter table emp_deptno_list drop partition p_10; select * from emp_deptno_list; select index_name, partition_name, status from user_ind_partitions where index_name = upper('emp_deptno_list_global'); INDEX_NAME |PARTITION_NAME|STATUS | ----------------------+--------------+--------+ EMP_DEPTNO_LIST_GLOBAL|SYS_P814 |UNUSABLE| EMP_DEPTNO_LIST_GLOBAL|SYS_P815 |UNUSABLE| ---------------------------- unusable 즉 비정상 이므로, 있어도 못 씀 인덱스를 다시 만들어줘야함 ============================== alter table emp_deptno_list drop partition p_20 update global indexes; --그래서 위와 같은 방법이 있는데, 이건 관리 수업때 할 것
    notion image
     
    Share article
    Contents
    4장. 시스템 튜닝 및 성능진단■ 인덱스(Index) 튜닝INDEX UNIQUE SCANUNIQUE INDEX 그림으로 보기■ 조인 튜닝검색 조건이 있었을 때의 해쉬조인 순서는?SORT MERGE JOINJOIN 방법 섞어서 사용하기인라인뷰(from절의 서브쿼리)와 조인■ 서브쿼리(Sub-query) 튜닝■ SQL 재작성하여 튜닝하기■ 파티션 테이블(Partition table)Partition Table(파티션 테이블)

    Hye-Min Son

    RSS·Powered by Inblog