쿼리 옵티마이저는 SQL 구문의 성능을 직접적으로 책임지고 있다.
쿼리 옵티마이저의 구성은 여러 초기화 파라미터 뿐만 아니라 시스템 통계 및 오브젝트 통계로 이루어져 있다.
1. 언제나 두 개의 초기화 파라미터, optimizer_mode와 db_file_multiblock_read_count는 조정해야 한다.
2. 이 단계에서 조정할 초기화 파라미터는 기본값으로도 양호하기 때문에 이 단계는 선택사항이다.
3. 시스템 통계와 오브젝트 통계는 쿼리 옵티마이저에게 중요한 정보를 제공하기 때문에 반드시 수집해야 한다.
4. workarea_size_policy 초기화 파라미터를 설정하야 메모리에 데이터를 저장하는 오퍼레이션이 사용할 작업 영역의 크기를 수동으로 설정할지 자동으로 설정할지 선택할 수 있다.
5. 작업 영역의 크기가 자동일 경우, pgg_aggregate_targert 초기화 파라미터를 설정한다.
6. 작업 영역의 크기가 수동인 경우, 실제 크기는 메모리를 사용하는 오퍼레이션 유형에 따라 달라진다.
7. 구성의 첫번째 부분이 준비가 되면 애플리케이션을 테스트한다. 테스트가 실시하는 동안 요구된 성능을 제공하지 못하는 컴포넌트에 대해 실행 계획을 수집한다. 이렇게 수집된 실행 계획을 분서하여 문제가 무엇인지 추론할 수 있어야 한다.
8. 대부분의 SQL 구문에 대해 쿼리 옵티마이저가 효과적인 실행 계획을 생성한다면 구성이 훌룡하다고 볼 수 있다.
9. 쿼리 옵티마이저가 인덱스를 너무 많이 혹은 너무 적게 사용하는 경향이 이거나 네스티드 루프 조인을 선호한다면 이 문제를 해경하기 위해 일반적으로 optimizer_index_caching 및 optimize_index_cost_adj 초기화 파라미터를 조정해 볼 수 있다.
쿼리 옵티마이저를 성공적으로 구성하려면 옵티마이저의 작동 방식 및 각각의 초기화 파타미터가 옵티마이저에 미치는 영향을 반드시 이해해야 한다. 이러한 지식을 바탕으로 임의로 구겅을 조정하거나 최근 인터넷에서 발견한 좋은 값을 붙여넣기 하는 대신에 다음과 같은 것들을 수행해야 한다.
h5.optimizer_mode
이 파라미터는 더 빠른 처리 보다 보다 적은 리소스 사용 또는 다른 무언가를 의미할 수 있다.
optimizer_mode 초기화 파라미터의 값을 선택하기 위해서는 쿼리 옵티마이저가 첫 로우의 빠른 전송과 마지막 로우의 빠른 전송 중에 어떠한 실행 계획을 생성하는 것이 더 중요한지 자문해봐야 한다.
기본값은 all_rows이다 INSERT, DELETE, MERGE, UPDATE 구문은 항상 all_row로 최적화된다.
h6.optimizer_features_enable
데이터베이스 버전이 올라갈 때마다 오라클은 쿼리 옵티마이저에 대해 새로운 기능을 도입하거나 이전 버전에서 비활성화되었던 기능을 활성화한다. 이전 버전을 사용하기를 원하면 optimizer_features_enable을 이전 버전으로 설정하면 된다.
SQL> SELECT value
2 FROM v$parameter_valid_values
3 WHERE name = 'optimizer_features_enable';
VALUE
--------------------------------------------------------------------------------
8.0.0
8.0.3
8.0.4
8.0.5
8.0.6
8.0.7
8.1.0
8.1.3
8.1.4
8.1.5
8.1.6
VALUE
--------------------------------------------------------------------------------
8.1.7
9.0.0
9.0.1
9.2.0
9.2.0.8
10.1.0
10.1.0.3
10.1.0.4
10.1.0.5
10.2.0.1
10.2.0.2
VALUE
--------------------------------------------------------------------------------
10.2.0.3
10.2.0.4
10.2.0.5
11.1.0.6
11.1.0.7
11.2.0.1
11.2.0.2
11.2.0.3
11.2.0.4
12.1.0.1
12.1.0.2
VALUE
--------------------------------------------------------------------------------
12.2.0.1
12.2.0.1.1
35 행이 선택되었습니다.
optimizer_features_enable 초기화 파라미터는 인스턴스 및 세션 레벨에서 변경 가능한 동적 파라미터이다.
h6.db_file_multiblock_read_count
멀티블록 읽기를 하는 동안 데이터베이스 엔진이 사용하는 최대 디스크 I/O의 크기는 db_block_size와 db_file_multiblock_read_count 초기화 파라미터의 값을 곱해서 결정된다. 따라서 멀티블락 읽기를 하는 동안 읽게 될 블록의 최대 개수는 최대 디스크 I/O 크기를 테이블스페이스의 블록 크기로 나누면 된다. 다시 말해 기본 블록 크기일 경우 db_file_multiblock_read_count 초기화 파라미터가 읽어 들일 블록의 최대 개수를 지정한다. 이 파라미터는 단순히 최댓값 을 의미할 뿐인데, 왜냐하면 일반적으로 다음과 같은 세 가지 경우에는 초기화 파라미터에서 지정된 값보다 더 작은 값으로 멀티블록 읽기가 일어날 수 있기 때문이다.
위에 그림은 수동 세그먼트 공간 관리를 사용하는 테이블스페이스에 저장된 세그먼트의 구조를 보여준다. 첫 번째 익스텐트의 첫 번째 블록은 세그먼트 헤더이다. 일부 블록(4,9,10,19,20)은 버퍼 캐시에 캐싱되어 있다. 이 세그먼트를 버퍼 캐시로 읽어들이는 데이터베이스 엔진의 프로세스는 db_file_multibolck_read_count 파라미터가 32보다 크거나 같은 값으로 설정되어 있더라도 한 번의 물리적 멀티블록 읽기로 읽어 들일 수 없다.
db_file_multiblock_read_count 초기화 파라미터가 8로 설정되어 있다면, 버퍼 캐시 읽기가 다음과 같이 수행된다.
요약하자면 오라클 프로세스는 2번의 싱글블록 읽기와 6번의 멀티블록 읽기를 수행한다. 멀티블록 읽기로 읽어 들인 블록의 평균 개수는 대략 4이다. 평균 크기가 8보다 작다는 사실은 오라클이 시스템 통계에서 mbrc 값을 도입한 이유를 설명해준다.
workload 시스템 통계를 사용할 수 있는 경우에는 db_file_multiblock_read_count 초기화 파라미터의 값을 사용하지 않고 I/O 비용을 계산한다. 멀티블록 I/O 비용은 공식 9-1에 따라 계산된다.
Formula 9-1. I/O cost of multiblock read operations with workload statistics blocks mreadtim io_cost mbrc sreadtim
nowworkload 통계의 경우 공식 9-1에서 변수가 다음과 같이 대체된다.
비용 계산 공식을 살펴봤으니 이제 db_file_multiblock_read_count 초기화 파라미터의 설정값을 찾는 방법을 살펴볼 차례이다. 이때 가장 중요한 것은 멀티블록 읽기가 성능에 큰 영향을 미칠 수 있다는 사실이다. 따라서 db_file_multiblock_read_count 초기화 파라티머는 최적의 성능을 얻을 수 있도록 신중하게 설치해야 한다. 일반적으로 디스트 I/O 크기가 1MB일 때 최적에 가까운 성능을 제공하긴 하지만, 가끔은 그보다 높거나 낮은 값이 더 나은 결과를 보여줄 때도 있다. 게다가 이 파라미터의 값이 높을 경우 일반적으로 디스트 I/O 오퍼레이션을 수행하는데 더 적은 CPU를 사용한다. 서로 다른 값을 사용하여 간단한 full table scan을 수행해본다면 이 초기화 파라미터가 미치는 영향에 대해 유용한 정보를 얻을 수 있으며, 최적의 값을 찾는데 도움을 받을 수 있다. assess_dbmbrc.sql 스크립트를 발췌한 다음 PL/SQL 코드는 이러한 목적에 사용할 수 있다.
BEGIN dbms_output.put_line('dbfmbrc blocks seconds cpu');
FOR i IN 0..10
LOOP
l_dbfmbrc := power(2,i);
EXECUTE IMMEDIATE 'ALTER SESSION SET db_file_multiblock_read_count = '||l_dbfmbrc;
EXECUTE IMMEDIATE 'ALTER SYSTEM FLUSH BUFFER_CACHE';
SELECT sum(decode(name, 'physical reads', value)),
sum(decode(name, 'CPU used by this session', value))
INTO l_starting_blocks, l_starting_cpu
FROM v$mystat ms JOIN v$statname USING (statistic#)
WHERE name IN ('physical reads','CPU used by this session');
l_starting_time := dbms_utility.get_time();
SELECT count(*) INTO l_count FROM t;
l_ending_time := dbms_utility.get_time();
SELECT sum(decode(name, 'physical reads', value)),
sum(decode(name, 'CPU used by this session', value))
INTO l_ending_blocks, l_ending_cpu
FROM v$mystat ms JOIN v$statname USING (statistic#)
WHERE name IN ('physical reads','CPU used by this session');
l_time := round((l_ending_time-l_starting_time)/100,1);
l_blocks := l_ending_blocks-l_starting_blocks;
l_cpu := l_ending_cpu-l_starting_cpu;
dbms_output.put_line(l_dbfmbrc||' '||l_blocks||' '||to_char(l_time)||' '||to_char(l_cpu)); END LOOP; END;
모든 초기화 파라미터를 기본값으로 설정한 11.2 데이터베이스에서 위의 PL/SQL 블록을 실행하여 측정한 나의 테스트 시스템에 대한 특성을 보여주고 있다. 여기에서 주목할 점은 다음과 같다.
h6.optimizer_dynamic_sampling
12.1 버전부터는 동적 샘플링이라는 용어 대신에 동적 통계라는 용어가 사용되지만 이 책에서는 옛날 이름인 동적 샘플링을 사용한다.
optimizer_dynamic_sampling 초기화 파라미터는 동적 파라미터이며 인스턴스 레벨 및 세션 레벨에서 변경할 수 있다. 12.1 멀터터넨트 환경에서는 PDB 레벨에서 설정할 수도 있다. 또한 dynamic_sampling 힌트를 사용하여 구분 레벨에서 값을 지정할 수도 있다. 힌트에서 다음과 같은 두 가지 문법을 사용할 수 있다.
쿼리 옵티마이저는 두 가지 유형의 통계를 수집하기 위하여 동적 샘플링을 사용할 수 있다. 첫 번째는 다음과 같다.
두 번째 유형은 다음과 같다.
SQL> CREATE TABLE t_noidx (id, n1, n2, pad) AS SELECT rownum, rownum,
cast(round(dbms_random.value(1,100)) AS VARCHAR2(100)), cast(dbms_random.string('p',1000) AS VARCHAR2(1000)) FROM dual CONNECT BY level <= 1000
2 3 ;
테이블이 생성되었습니다.
SQL> CREATE TABLE t_idx (id CONSTRAINT t_idx_pk PRIMARY KEY, n1, n2, pad) AS SELECT * FROM t_noidx;
테이블이 생성되었습니다.
SELECT * FROM t_noidx t1, t_noidx t2 WHERE t1.id = t2.id AND t1.id < 19
SELECT * FROM t_idx t1, t_idx t2 WHERE t1.id = t2.id AND t1.id < 19
레벨이 1로 설정되어 있는 경우, 두 번째 쿼리가 참조하는 테이블에 인덱스가 있기 때문에 첫 번째 쿼리만 동적 샘플링이 수행된다.
SELECT NVL(SUM(C1),0),
NVL(SUM(C2),0),
COUNT(DISTINCT C3),
NVL(SUM(CASE WHEN C3 IS NULL THEN 1 ELSE 0 END),0)
FROM (
SELECT 1 AS C1,
CASE WHEN "T1"."ID"<19 THEN 1 ELSE 0 END AS C2,
"T1"."ID" AS C3
FROM "CHRIS"."T_NOIDX" SAMPLE BLOCK (20 , 1) SEED (1) "T1"
) SAMPLESUB
여기서는 다음과 같은 것들을 주목해야 한다.
SELECT NVL(SUM(C1),0),
NVL(SUM(C2),0),
NVL(SUM(C3),0)
FROM ( SELECT 1 AS C1,
1 AS C2,
1 AS C3
FROM "CHRIS"."T_IDX" "T1"
WHERE "T1"."ID"<19
AND ROWNUM <= 2500
) SAMPLESUB
레벨 2로 설정되어 있는 경우에는 오브젝트 통계가 없을 때 언제나 동적 샘플링이 사용되기 때문에 양쪽 테스트 쿼리에 대해 동적 샘플링을 제공한다.
BEGIN dbms_stats.gather_table_stats( ownname => user,
tabname => 't_noidx',
method_opt => 'for all columns size 1');
dbms_stats.gather_table_stats( ownname => user,
tabname => 't_idx',
method_opt => 'for all columns size 1',
cascade => true);
END;
레벨 3 이상으로 설정되면 쿼리 옵티마이저는 조건절의 선택도를 추정하기 위해 데이터 딕셔너리 통계나 하드 코딩된 값을 사용하는 대신 테이블의 로우 샘플링을 통해 선택도를 측정하는 동적 샘플링을 수행한다.
SELECT * FROM t_idx WHERE id = 19
SELECT * FROM t_idx WHERE round(id) = 19
첫 번째의 경우 쿼리 옵티마이저는 컬럼 통계와 히스토그램을 기반으로 id=19 조건절의 선택도를 추정할 수 있다. 따라서 어떤 동적 샘플링도 필요로 하지 않는다. 그 대신 두 번째 쿼리에 대해 쿼리 옵티마이저는 round(id)=19 조건절의 선택도를 추론할 수 없다.
SELECT NVL(SUM(C1),0),
NVL(SUM(C2),0),
COUNT(DISTINCT C3)
FROM ( SELECT 1 AS C1,
CASE WHEN ROUND("T_IDX"."ID")=19 THEN 1 ELSE 0 END AS C2,
ROUND("T_IDX"."ID") AS C3
FROM "CHRIS"."T_IDX" SAMPLE BLOCK (20 , 1) SEED (1) "T_IDX"
) SAMPLESUB
레벨 4 이상으로 설정한 경우 동일 테이블에서 WHERE 절이 두 개 이상의 칼럼을 참조할 때도 쿼리 옵티마이저는 동적 샘플링을 수행한다.
SELECT * FROM t_idx WHERE id < 19 AND n1 < 19
또한 이 경우에 쿼리 옵티마이저는 앞서와 동일한 구조를 가진 쿼리를 사용하여 동적 샘플링을 수행한다.
SELECT NVL(SUM(C1),0),
NVL(SUM(C2),0)
FROM ( SELECT 1 AS C1,
CASE WHEN "T_IDX"."ID"<19 AND "T_IDX"."N1"<19 THEN 1 ELSE 0 END AS C2
FROM "CHRIS"."T_IDX" SAMPLE BLOCK (20 , 1) SEED (1) "T_IDX"
) SAMPLESUB
요약하면 일반적으로 레벌1과 2는 그다지 유용하지 않다는 사실을 알 수 있다.
h6.optimizer_index_cost_adj
optimizer_index_cost_adj 초기화 파라미터는 index scan을 통해서 테이블을 액세스할 때의 비용을 변경하기 위한 목적으로 사용된다.
이 초기화 파라미터가 비용 계산 공식에 미치는 영향을 이해하려면 쿼리 옵티마이저가 index range scan에 기초하는 테이블 엑세스와 관련된 비용을 어떻게 계산하는지 알고 있어야 한다.
index range scan은 여러 개의 키에 대한 인덱스 검색이다. 다음과 같은 오퍼레이션이 수행된다.
1. 인덱스의 루트 블록을 엑세스 한다.
2. 브랜치 블록을 거쳐서 첫 번째 키가 포함된 리프 블록을 찾아간다.
3. 검색 기준을 충족하는 각각의 키에 대해 다음을 수행한다.
index range scan에 의해 수행되는 물리적 읽기의 수는 첫 번째 키를 포함하는 리프 블록을 찾기 위해 액세스 하는 브랜치 블록의 수, 스캔되는 리프 블록의 수를 모두 더한 것과 동일하다. 이에 대해 optimizer_index_cost_adj 초기화 파라미터가 적용된 보정값까지 고려한 다음의 공식이 제공된다.
SQL> ALTER SESSION SET OPTIMIZER_INDEX_COST_ADJ = 100;
SQL> SELECT * FROM t WHERE val1 = 11 AND val2 = 11;
-----------------------------------------------
| Id | Operation | Name | -----------------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | TABLE ACCESS BY INDEX ROWID| T |
|* 2 | INDEX RANGE SCAN | T_VAL2_I |
-----------------------------------------------
1 - filter("VAL1"=11)
2 - access("VAL2"=11)
SQL> ALTER SESSION SET OPTIMIZER_INDEX_COST_ADJ = 10;
-----------------------------------------------
| Id | Operation | Name |
-----------------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | TABLE ACCESS BY INDEX ROWID| T |
|* 2 | INDEX RANGE SCAN | T_VAL1_I |
-----------------------------------------------
1 - filter("VAL2"=11) 2 - access("VAL1"=11)
SQL> ALTER INDEX t_val1_i RENAME TO t_val3_i;
SQL> SELECT * FROM t WHERE val1 = 11 AND val2 = 11;
-----------------------------------------------
| Id | Operation | Name |
-----------------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | TABLE ACCESS BY INDEX ROWID| T |
|* 2 | INDEX RANGE SCAN | T_VAL2_I |
-----------------------------------------------
1 - filter("VAL1"=11) 2 - access("VAL2"=11)
h6.optimizer_secure_view_merging
optimizer_secure_view_merging 초기화 파라미터는 뷰 병합 및 조건 이돈 같은 쿼리 변환을 제어할 수 있다. 파라미터 기본값은 TRUE이다.
CREATE TABLE t ( id NUMBER(10) PRIMARY KEY, class NUMBER(10), pad VARCHAR2(10) )
CREATE OR REPLACE VIEW v AS SELECT * FROM t WHERE f(class) = 1
CREATE OR REPLACE FUNCTION spy (id IN NUMBER, pad IN VARCHAR2) RETURN NUMBER AS
BEGIN dbms_output.put_line('id='||id||' pad='||pad);
RETURN 1;
END;
SQL> SELECT id, pad
2 FROM v
3 WHERE id BETWEEN 1 AND 5;
ID PAD
---------- ---------
1 DrMLTDXxxq 4 AszBGEUGEL
SQL> SELECT id, pad
2 FROM v
3 WHERE id BETWEEN 1 AND 5
4 AND spy(id, pad) = 1;
ID PAD
---------- ---------
1 DrMLTDXxxq
4 AszBGEUGEL
id=1 pad=DrMLTDXxxq
id=2 pad=XOZnqYRJwI
id=3 pad=nlGfGBTxNk
id=4 pad=AszBGEUGEL
id=5 pad=qTSRnFjRGb
SQL> SELECT id, pad
2 FROM v
3 WHERE id BETWEEN 1 AND 5
4 AND spy(id, pad) = 1;
ID PAD
---------- ---------
1 DrMLTDXxxq
4 AszBGEUGEL
id=1 pad=DrMLTDXxxq
id=4 pad=AszBGEUGEL
h3.PGA 관리
특정 App에 대한 최적의 값을 찾는 유일한 방법은 테스트를 실시하여 좋은 성능을 얻는데 얼마나 많은 PGA가 필요한지 측정하는 것이다. 실제로 메모리의 용량은 성능에만 영향을 미치고 오퍼레이션의 동작 방식에는 영향을 미치지 않는다.
h4.workarea_size_policy
workarea_size_policy 초기화 파라미터는 작업 영역의 크기를 어떤 방식으로 구성할지 결정한다. 다음의 두 값 중 하나로 설정할 수 있다.
h5.pga_aggregate_targer
pga_aggregate_targer 초기화 파라미터를 통해서 하나의 데이터베이스 인스턴스가 할당할 전제 PGA 메모리의 크기를 바이트 단위로 지정할 수 있다.
메모리 관리자가 세션에 할당되는 PGA의 양을 자동으로 조절한다.
pga_aggregate_targer 초기화 파라미터를 통해 설정한 목표값이 너무 낮을 경우(여기서는 128MB), 메모리 관리자는 이를 준수하지 않는다.
SQL> SELECT name, value, unit
2 FROM v$pgastat
3 WHERE name IN ('aggregate PGA target parameter',
4 'maximum PGA allocated',
5 'over allocation count');
NAME VALUE UNIT
------------------------------ ---------- ----
aggregate PGA target parameter 134217728 bytes
maximum PGA allocated 418658304 bytes
over allocation count 94
SQL> SELECT name, value, unit
2 FROM v$pgastat
3 WHERE name LIKE '% PGA allocated' OR name LIKE '% workareas';
NAME VALUE UNIT
------------------------------------- ---------- ----
total PGA allocated 999358464 bytes
maximum PGA allocated 1015480320 bytes
total PGA used for auto workareas 372764672 bytes
maximum PGA used for auto workareas 614833152 bytes
total PGA used for manual workareas 0 bytes
maximum PGA used for manual workareas 0 bytes
SQL> SELECT name, value, unit
2 FROM v$pgastat
3 WHERE name LIKE 'aggregate PGA %';
NAME VALUE UNIT
------------------------------ ----------- ----
aggregate PGA target parameter 1073741824 bytes
aggregate PGA auto target 910411776 bytes
SQL> execute pga_pkg.allocate(500000)
SQL> SELECT name, value, unit
2 FROM v$pgastat
3 WHERE name LIKE 'aggregate PGA %';
NAME VALUE UNIT
------------------------------ ----------- ----
aggregate PGA target parameter 1073741824 bytes
aggregate PGA auto target 375754752 bytes
SQL> execute dbms_session.reset_package;
SQL> SELECT name, value, unit
2 FROM v$pgastat
3 WHERE name LIKE 'aggregate PGA %';
NAME VALUE UNIT
------------------------------ ----------- ----
aggregate PGA target parameter 1073741824 bytes
aggregate PGA auto target 910411776 bytes
h5.pga_aggregate_limit
pga_aggregate_limit 초기화 파라미터는 12.1 버전의 새로운 기능이다.
pga_aggregate_limit 초기화 파라미터의 기본값은 다음 중에서 가장 큰 값으로 설정된다.
*2GB
*pga_aggregate_target 초기화 파라미터의 값의 두 배
*process 초기화 파라미터 값에 3MB를 곱한 값
SQL> ALTER SYSTEM SET pga_aggregate_limit = 1G;
ALTER SYSTEM SET pga_aggregate_limit = 1G
*
ERROR at line 1:
ORA-02097: parameter cannot be modified because specified value is invalid
ORA-00093: pga_aggregate_limit must be between 2048M and 100000G
한계에 도달하면 데이터베이스 엔진은 호출을 중단하거나 심지어 세션을 죽인다. 호출이 중단되면 다음과 같은 오류가 발생한다.
ORA-04036: PGA memory used by the instance exceeds PGA_AGGREGATE_LIMIT
ORA-03113: end-of-file on communication channel
Process ID: 5125
Session ID: 17 Serial number: 39
PGA memory used by the instance exceeds PGA_AGGREGATE_LIMIT of 2048 MB
Immediate Kill Session#: 17, Serial#: 39
Immediate Kill Session: sess: 0x77eb7478 OS pid: 5125
h6.sort_area_size
수동 PGA 메모리 관리를 사용하는 경우 sort_area_size 초기화 파라미터를 사용하여 머지 조인, 정렬, 집계에 사용되는 작업 영역의 크기를 바이트 단위로 지정한다.
h6.sort_area_retained_size
단지 정렬 Operation이 일어나는 동안 사용되는 메모리의 양을 지정할 뿐이다.
h6.hash_area_size
하나의 작업 영역의 크기를 지정할 뿐이며, 하나의 세션은 여러 개의 작업 영역을 할당할 수 있다.
h6.bitmap_merge_area_size
비트맵 인덱스와 관련된 비트맵을 병합하는데 사용되는 작업 영역의 크기를 바이트 단위로 지정한다.