(새 문서: = SQL 성능 관리 프로그램 = == 프로그램 개요 == == SQL 인스펙션 == === SQL 수집 및 분석 === === 조치 결과 보고서 === == SQL 플랜 분석 == === 플랜 분석 보고서 === == DB별 SQL 성능 저하 SQL == === TOP 10 SQL ===) |
편집 요약 없음 |
||
| (같은 사용자의 중간 판 4개는 보이지 않습니다) | |||
| 4번째 줄: | 4번째 줄: | ||
== SQL 인스펙션 == | == SQL 인스펙션 == | ||
=== SQL | |||
=== Oracle 19c SQL 실행 계획 저장 및 관리 프로그램=== | |||
====실행 계획 저장용 테이블 생성==== | |||
<source lang=sql> | |||
-- 실행 계획 히스토리 테이블 | |||
CREATE TABLE sql_plan_history ( | |||
plan_id NUMBER GENERATED ALWAYS AS IDENTITY, | |||
statement_id VARCHAR2(100), | |||
sql_text CLOB, | |||
plan_hash_value NUMBER, | |||
execution_time TIMESTAMP DEFAULT SYSTIMESTAMP, | |||
username VARCHAR2(128) DEFAULT USER, | |||
module VARCHAR2(64), | |||
action VARCHAR2(64), | |||
optimizer_cost NUMBER, | |||
cardinality NUMBER, | |||
bytes_size NUMBER, | |||
cpu_cost NUMBER, | |||
io_cost NUMBER, | |||
elapsed_seconds NUMBER, | |||
rows_processed NUMBER, | |||
execution_type VARCHAR2(20), -- EXPLAIN, ACTUAL | |||
notes VARCHAR2(4000), | |||
CONSTRAINT pk_sql_plan_history PRIMARY KEY (plan_id) | |||
); | |||
-- 실행 계획 상세 정보 테이블 | |||
CREATE TABLE sql_plan_details ( | |||
detail_id NUMBER GENERATED ALWAYS AS IDENTITY, | |||
plan_id NUMBER, | |||
operation VARCHAR2(128), | |||
options VARCHAR2(128), | |||
object_name VARCHAR2(128), | |||
object_type VARCHAR2(30), | |||
optimizer VARCHAR2(255), | |||
position NUMBER, | |||
cost NUMBER, | |||
cardinality NUMBER, | |||
bytes_size NUMBER, | |||
partition_start VARCHAR2(255), | |||
partition_stop VARCHAR2(255), | |||
cpu_cost NUMBER, | |||
io_cost NUMBER, | |||
temp_space NUMBER, | |||
access_predicates VARCHAR2(4000), | |||
filter_predicates VARCHAR2(4000), | |||
projection VARCHAR2(4000), | |||
plan_step NUMBER, | |||
parent_step NUMBER, | |||
depth NUMBER, | |||
CONSTRAINT pk_sql_plan_details PRIMARY KEY (detail_id), | |||
CONSTRAINT fk_plan_details FOREIGN KEY (plan_id) | |||
REFERENCES sql_plan_history(plan_id) ON DELETE CASCADE | |||
); | |||
-- 인덱스 생성 | |||
CREATE INDEX idx_plan_history_stmt ON sql_plan_history(statement_id); | |||
CREATE INDEX idx_plan_history_hash ON sql_plan_history(plan_hash_value); | |||
CREATE INDEX idx_plan_history_time ON sql_plan_history(execution_time); | |||
CREATE INDEX idx_plan_details_plan ON sql_plan_details(plan_id); | |||
</source> | |||
====실행 계획 저장 프로시저==== | |||
<source lang=sql> | |||
CREATE OR REPLACE PROCEDURE save_sql_plan( | |||
p_sql_text IN CLOB, | |||
p_statement_id IN VARCHAR2 DEFAULT NULL, | |||
p_notes IN VARCHAR2 DEFAULT NULL, | |||
p_plan_id OUT NUMBER | |||
) AS | |||
v_statement_id VARCHAR2(100); | |||
v_plan_hash_value NUMBER; | |||
v_plan_id NUMBER; | |||
v_sql_id VARCHAR2(13); | |||
BEGIN | |||
-- Statement ID 생성 (제공되지 않은 경우) | |||
IF p_statement_id IS NULL THEN | |||
v_statement_id := 'PLAN_' || TO_CHAR(SYSTIMESTAMP, 'YYYYMMDDHH24MISSFF'); | |||
ELSE | |||
v_statement_id := p_statement_id; | |||
END IF; | |||
-- EXPLAIN PLAN 실행 | |||
EXECUTE IMMEDIATE 'EXPLAIN PLAN SET STATEMENT_ID = ''' || v_statement_id || | |||
''' FOR ' || p_sql_text; | |||
-- Plan Hash Value 조회 | |||
SELECT MAX(plan_hash_value) | |||
INTO v_plan_hash_value | |||
FROM plan_table | |||
WHERE statement_id = v_statement_id; | |||
-- 히스토리 테이블에 저장 | |||
INSERT INTO sql_plan_history ( | |||
statement_id, | |||
sql_text, | |||
plan_hash_value, | |||
execution_time, | |||
username, | |||
optimizer_cost, | |||
cardinality, | |||
bytes_size, | |||
cpu_cost, | |||
io_cost, | |||
execution_type, | |||
notes | |||
) | |||
SELECT | |||
v_statement_id, | |||
p_sql_text, | |||
plan_hash_value, | |||
SYSTIMESTAMP, | |||
USER, | |||
MAX(cost), | |||
MAX(cardinality), | |||
MAX(bytes), | |||
MAX(cpu_cost), | |||
MAX(io_cost), | |||
'EXPLAIN', | |||
p_notes | |||
FROM plan_table | |||
WHERE statement_id = v_statement_id | |||
RETURNING plan_id INTO v_plan_id; | |||
-- 상세 정보 저장 | |||
INSERT INTO sql_plan_details ( | |||
plan_id, | |||
operation, | |||
options, | |||
object_name, | |||
object_type, | |||
optimizer, | |||
position, | |||
cost, | |||
cardinality, | |||
bytes_size, | |||
partition_start, | |||
partition_stop, | |||
cpu_cost, | |||
io_cost, | |||
temp_space, | |||
access_predicates, | |||
filter_predicates, | |||
projection, | |||
plan_step, | |||
parent_step, | |||
depth | |||
) | |||
SELECT | |||
v_plan_id, | |||
operation, | |||
options, | |||
object_name, | |||
object_type, | |||
optimizer, | |||
position, | |||
cost, | |||
cardinality, | |||
bytes, | |||
partition_start, | |||
partition_stop, | |||
cpu_cost, | |||
io_cost, | |||
temp_space, | |||
access_predicates, | |||
filter_predicates, | |||
projection, | |||
id, | |||
parent_id, | |||
depth | |||
FROM plan_table | |||
WHERE statement_id = v_statement_id | |||
ORDER BY id; | |||
-- PLAN_TABLE 정리 | |||
DELETE FROM plan_table WHERE statement_id = v_statement_id; | |||
COMMIT; | |||
p_plan_id := v_plan_id; | |||
DBMS_OUTPUT.PUT_LINE('실행 계획이 저장되었습니다. Plan ID: ' || v_plan_id); | |||
DBMS_OUTPUT.PUT_LINE('Statement ID: ' || v_statement_id); | |||
EXCEPTION | |||
WHEN OTHERS THEN | |||
ROLLBACK; | |||
DBMS_OUTPUT.PUT_LINE('오류 발생: ' || SQLERRM); | |||
RAISE; | |||
END save_sql_plan; | |||
/ | |||
</source> | |||
==== 실행 계획 조회 프로시저==== | |||
<source lang=sql> | |||
CREATE OR REPLACE PROCEDURE display_sql_plan( | |||
p_plan_id IN NUMBER | |||
) AS | |||
v_sql_text CLOB; | |||
v_statement_id VARCHAR2(100); | |||
BEGIN | |||
-- SQL 텍스트 조회 | |||
SELECT sql_text, statement_id | |||
INTO v_sql_text, v_statement_id | |||
FROM sql_plan_history | |||
WHERE plan_id = p_plan_id; | |||
DBMS_OUTPUT.PUT_LINE('========================================'); | |||
DBMS_OUTPUT.PUT_LINE('Plan ID: ' || p_plan_id); | |||
DBMS_OUTPUT.PUT_LINE('Statement ID: ' || v_statement_id); | |||
DBMS_OUTPUT.PUT_LINE('========================================'); | |||
DBMS_OUTPUT.PUT_LINE('SQL Text:'); | |||
DBMS_OUTPUT.PUT_LINE(v_sql_text); | |||
DBMS_OUTPUT.PUT_LINE('========================================'); | |||
DBMS_OUTPUT.PUT_LINE(''); | |||
-- 실행 계획 트리 형태로 출력 | |||
FOR rec IN ( | |||
SELECT | |||
LPAD(' ', depth * 2) || operation || ' ' || | |||
NVL(options, '') || ' ' || NVL(object_name, '') AS operation_tree, | |||
cost, | |||
cardinality, | |||
bytes_size, | |||
access_predicates, | |||
filter_predicates | |||
FROM sql_plan_details | |||
WHERE plan_id = p_plan_id | |||
ORDER BY plan_step | |||
) LOOP | |||
DBMS_OUTPUT.PUT_LINE( | |||
RPAD(rec.operation_tree, 60) || | |||
' | Cost: ' || NVL(TO_CHAR(rec.cost), 'N/A') || | |||
' | Rows: ' || NVL(TO_CHAR(rec.cardinality), 'N/A') | |||
); | |||
IF rec.access_predicates IS NOT NULL THEN | |||
DBMS_OUTPUT.PUT_LINE(' Access: ' || rec.access_predicates); | |||
END IF; | |||
IF rec.filter_predicates IS NOT NULL THEN | |||
DBMS_OUTPUT.PUT_LINE(' Filter: ' || rec.filter_predicates); | |||
END IF; | |||
END LOOP; | |||
EXCEPTION | |||
WHEN NO_DATA_FOUND THEN | |||
DBMS_OUTPUT.PUT_LINE('Plan ID ' || p_plan_id || '를 찾을 수 없습니다.'); | |||
WHEN OTHERS THEN | |||
DBMS_OUTPUT.PUT_LINE('오류 발생: ' || SQLERRM); | |||
END display_sql_plan; | |||
/ | |||
</source> | |||
==== 실행 계획 비교 프로시저==== | |||
<source lang=sql> | |||
CREATE OR REPLACE PROCEDURE compare_sql_plans( | |||
p_plan_id1 IN NUMBER, | |||
p_plan_id2 IN NUMBER | |||
) AS | |||
v_cost1 NUMBER; | |||
v_cost2 NUMBER; | |||
v_rows1 NUMBER; | |||
v_rows2 NUMBER; | |||
BEGIN | |||
-- Plan 1 정보 | |||
SELECT optimizer_cost, cardinality | |||
INTO v_cost1, v_rows1 | |||
FROM sql_plan_history | |||
WHERE plan_id = p_plan_id1; | |||
-- Plan 2 정보 | |||
SELECT optimizer_cost, cardinality | |||
INTO v_cost2, v_rows2 | |||
FROM sql_plan_history | |||
WHERE plan_id = p_plan_id2; | |||
DBMS_OUTPUT.PUT_LINE('========================================'); | |||
DBMS_OUTPUT.PUT_LINE('실행 계획 비교'); | |||
DBMS_OUTPUT.PUT_LINE('========================================'); | |||
DBMS_OUTPUT.PUT_LINE('Plan ID 1: ' || p_plan_id1); | |||
DBMS_OUTPUT.PUT_LINE(' Cost: ' || v_cost1 || ', Rows: ' || v_rows1); | |||
DBMS_OUTPUT.PUT_LINE(''); | |||
DBMS_OUTPUT.PUT_LINE('Plan ID 2: ' || p_plan_id2); | |||
DBMS_OUTPUT.PUT_LINE(' Cost: ' || v_cost2 || ', Rows: ' || v_rows2); | |||
DBMS_OUTPUT.PUT_LINE(''); | |||
-- 차이 분석 | |||
IF v_cost1 < v_cost2 THEN | |||
DBMS_OUTPUT.PUT_LINE(' Plan 1이 더 효율적입니다 (Cost 차이: ' || | |||
ROUND((v_cost2 - v_cost1) / v_cost1 * 100, 2) || '%)'); | |||
ELSIF v_cost2 < v_cost1 THEN | |||
DBMS_OUTPUT.PUT_LINE(' Plan 2가 더 효율적입니다 (Cost 차이: ' || | |||
ROUND((v_cost1 - v_cost2) / v_cost2 * 100, 2) || '%)'); | |||
ELSE | |||
DBMS_OUTPUT.PUT_LINE('동일한 Cost입니다'); | |||
END IF; | |||
DBMS_OUTPUT.PUT_LINE('========================================'); | |||
-- 상세 비교 | |||
DBMS_OUTPUT.PUT_LINE(''); | |||
DBMS_OUTPUT.PUT_LINE('상세 차이점:'); | |||
FOR rec IN ( | |||
SELECT | |||
p1.operation AS op1, | |||
p1.object_name AS obj1, | |||
p1.cost AS cost1, | |||
p2.operation AS op2, | |||
p2.object_name AS obj2, | |||
p2.cost AS cost2, | |||
p1.plan_step | |||
FROM sql_plan_details p1 | |||
FULL OUTER JOIN sql_plan_details p2 | |||
ON p1.plan_step = p2.plan_step | |||
AND p2.plan_id = p_plan_id2 | |||
WHERE p1.plan_id = p_plan_id1 | |||
AND (p1.operation != p2.operation | |||
OR NVL(p1.object_name, 'N/A') != NVL(p2.object_name, 'N/A') | |||
OR p1.cost != p2.cost) | |||
ORDER BY p1.plan_step | |||
) LOOP | |||
DBMS_OUTPUT.PUT_LINE('Step ' || rec.plan_step || ':'); | |||
DBMS_OUTPUT.PUT_LINE(' Plan 1: ' || rec.op1 || ' ' || NVL(rec.obj1, '') || | |||
' (Cost: ' || NVL(TO_CHAR(rec.cost1), 'N/A') || ')'); | |||
DBMS_OUTPUT.PUT_LINE(' Plan 2: ' || rec.op2 || ' ' || NVL(rec.obj2, '') || | |||
' (Cost: ' || NVL(TO_CHAR(rec.cost2), 'N/A') || ')'); | |||
END LOOP; | |||
END compare_sql_plans; | |||
/ | |||
</source> | |||
==== 실행 계획 검색 뷰==== | |||
<source lang=sql> | |||
CREATE OR REPLACE VIEW v_sql_plan_summary AS | |||
SELECT | |||
h.plan_id, | |||
h.statement_id, | |||
h.execution_time, | |||
h.username, | |||
h.optimizer_cost, | |||
h.cardinality AS estimated_rows, | |||
h.cpu_cost, | |||
h.io_cost, | |||
SUBSTR(h.sql_text, 1, 100) AS sql_preview, | |||
h.notes, | |||
COUNT(d.detail_id) AS plan_steps | |||
FROM sql_plan_history h | |||
LEFT JOIN sql_plan_details d ON h.plan_id = d.plan_id | |||
GROUP BY | |||
h.plan_id, | |||
h.statement_id, | |||
h.execution_time, | |||
h.username, | |||
h.optimizer_cost, | |||
h.cardinality, | |||
h.cpu_cost, | |||
h.io_cost, | |||
h.sql_text, | |||
h.notes | |||
ORDER BY h.execution_time DESC; | |||
</source> | |||
====사용 예제==== | |||
<source lang=sql> | |||
-- 예제 1: SQL 실행 계획 저장 | |||
DECLARE | |||
v_plan_id NUMBER; | |||
v_sql CLOB := 'SELECT e.employee_id, e.first_name, e.last_name, d.department_name | |||
FROM employees e | |||
JOIN departments d ON e.department_id = d.department_id | |||
WHERE e.salary > 5000 | |||
ORDER BY e.last_name'; | |||
BEGIN | |||
save_sql_plan( | |||
p_sql_text => v_sql, | |||
p_statement_id => 'EMP_DEPT_JOIN_001', | |||
p_notes => '급여 5000 이상 직원 조회', | |||
p_plan_id => v_plan_id | |||
); | |||
DBMS_OUTPUT.PUT_LINE('저장된 Plan ID: ' || v_plan_id); | |||
END; | |||
/ | |||
-- 예제 2: 저장된 실행 계획 조회 | |||
BEGIN | |||
display_sql_plan(p_plan_id => 1); | |||
END; | |||
/ | |||
-- 예제 3: 실행 계획 비교 | |||
BEGIN | |||
compare_sql_plans(p_plan_id1 => 1, p_plan_id2 => 2); | |||
END; | |||
/ | |||
-- 예제 4: 실행 계획 요약 조회 | |||
SELECT * FROM v_sql_plan_summary | |||
WHERE execution_time >= SYSDATE - 7 | |||
ORDER BY optimizer_cost DESC; | |||
</source> | |||
====실행 계획 분석 함수들==== | |||
<source lang=sql> | |||
-- Full Table Scan 발생 여부 확인 | |||
CREATE OR REPLACE FUNCTION has_full_table_scan( | |||
p_plan_id IN NUMBER | |||
) RETURN VARCHAR2 AS | |||
v_count NUMBER; | |||
BEGIN | |||
SELECT COUNT(*) | |||
INTO v_count | |||
FROM sql_plan_details | |||
WHERE plan_id = p_plan_id | |||
AND operation = 'TABLE ACCESS' | |||
AND options = 'FULL'; | |||
IF v_count > 0 THEN | |||
RETURN 'YES (' || v_count || ' occurrences)'; | |||
ELSE | |||
RETURN 'NO'; | |||
END IF; | |||
END; | |||
/ | |||
-- 인덱스 사용 여부 확인 | |||
CREATE OR REPLACE FUNCTION get_indexes_used( | |||
p_plan_id IN NUMBER | |||
) RETURN VARCHAR2 AS | |||
v_indexes VARCHAR2(4000); | |||
BEGIN | |||
SELECT LISTAGG(object_name, ', ') WITHIN GROUP (ORDER BY object_name) | |||
INTO v_indexes | |||
FROM ( | |||
SELECT DISTINCT object_name | |||
FROM sql_plan_details | |||
WHERE plan_id = p_plan_id | |||
AND operation LIKE '%INDEX%' | |||
AND object_name IS NOT NULL | |||
); | |||
RETURN NVL(v_indexes, 'No indexes used'); | |||
END; | |||
/ | |||
-- Cost 기반 성능 등급 | |||
CREATE OR REPLACE FUNCTION get_performance_grade( | |||
p_plan_id IN NUMBER | |||
) RETURN VARCHAR2 AS | |||
v_cost NUMBER; | |||
v_grade VARCHAR2(1); | |||
BEGIN | |||
SELECT optimizer_cost | |||
INTO v_cost | |||
FROM sql_plan_history | |||
WHERE plan_id = p_plan_id; | |||
CASE | |||
WHEN v_cost < 10 THEN v_grade := 'A'; | |||
WHEN v_cost < 100 THEN v_grade := 'B'; | |||
WHEN v_cost < 1000 THEN v_grade := 'C'; | |||
WHEN v_cost < 10000 THEN v_grade := 'D'; | |||
ELSE v_grade := 'F'; | |||
END CASE; | |||
RETURN v_grade || ' (Cost: ' || v_cost || ')'; | |||
END; | |||
/ | |||
</source> | |||
==== 실행 계획 리포트 생성==== | |||
<source lang=sql> | |||
CREATE OR REPLACE PROCEDURE generate_plan_report( | |||
p_plan_id IN NUMBER | |||
) AS | |||
v_sql_text CLOB; | |||
v_cost NUMBER; | |||
v_rows NUMBER; | |||
v_statement_id VARCHAR2(100); | |||
v_execution_time TIMESTAMP; | |||
BEGIN | |||
-- 기본 정보 조회 | |||
SELECT sql_text, optimizer_cost, cardinality, statement_id, execution_time | |||
INTO v_sql_text, v_cost, v_rows, v_statement_id, v_execution_time | |||
FROM sql_plan_history | |||
WHERE plan_id = p_plan_id; | |||
DBMS_OUTPUT.PUT_LINE('╔═══════════════════════════════════════════════════════════════╗'); | |||
DBMS_OUTPUT.PUT_LINE('║ SQL 실행 계획 분석 리포트 ║'); | |||
DBMS_OUTPUT.PUT_LINE('╚═══════════════════════════════════════════════════════════════╝'); | |||
DBMS_OUTPUT.PUT_LINE(''); | |||
DBMS_OUTPUT.PUT_LINE(' 기본 정보'); | |||
DBMS_OUTPUT.PUT_LINE(' Plan ID: ' || p_plan_id); | |||
DBMS_OUTPUT.PUT_LINE(' Statement ID: ' || v_statement_id); | |||
DBMS_OUTPUT.PUT_LINE(' 생성 시간: ' || TO_CHAR(v_execution_time, 'YYYY-MM-DD HH24:MI:SS')); | |||
DBMS_OUTPUT.PUT_LINE(' 성능 등급: ' || get_performance_grade(p_plan_id)); | |||
DBMS_OUTPUT.PUT_LINE(''); | |||
DBMS_OUTPUT.PUT_LINE(' SQL 문장'); | |||
DBMS_OUTPUT.PUT_LINE(v_sql_text); | |||
DBMS_OUTPUT.PUT_LINE(''); | |||
DBMS_OUTPUT.PUT_LINE(' 통계 정보'); | |||
DBMS_OUTPUT.PUT_LINE(' Optimizer Cost: ' || v_cost); | |||
DBMS_OUTPUT.PUT_LINE(' Estimated Rows: ' || v_rows); | |||
DBMS_OUTPUT.PUT_LINE(' Full Table Scan: ' || has_full_table_scan(p_plan_id)); | |||
DBMS_OUTPUT.PUT_LINE(' Indexes Used: ' || get_indexes_used(p_plan_id)); | |||
DBMS_OUTPUT.PUT_LINE(''); | |||
DBMS_OUTPUT.PUT_LINE(' 실행 계획 상세'); | |||
display_sql_plan(p_plan_id); | |||
DBMS_OUTPUT.PUT_LINE(''); | |||
DBMS_OUTPUT.PUT_LINE('═══════════════════════════════════════════════════════════════'); | |||
END generate_plan_report; | |||
/ | |||
</source> | |||
====정리 및 유지보수 프로시저==== | |||
<source lang=sql> | |||
-- 오래된 실행 계획 삭제 | |||
CREATE OR REPLACE PROCEDURE cleanup_old_plans( | |||
p_days_to_keep IN NUMBER DEFAULT 30 | |||
) AS | |||
v_deleted_count NUMBER; | |||
BEGIN | |||
DELETE FROM sql_plan_history | |||
WHERE execution_time < SYSTIMESTAMP - p_days_to_keep | |||
RETURNING COUNT(*) INTO v_deleted_count; | |||
COMMIT; | |||
DBMS_OUTPUT.PUT_LINE(v_deleted_count || '개의 오래된 실행 계획을 삭제했습니다.'); | |||
END cleanup_old_plans; | |||
/ | |||
</source> | |||
====활용 쿼리 모음==== | |||
<source lang=sql> | |||
-- 최근 저장된 실행 계획 TOP 10 | |||
SELECT * FROM ( | |||
SELECT | |||
plan_id, | |||
statement_id, | |||
execution_time, | |||
optimizer_cost, | |||
SUBSTR(sql_text, 1, 50) || '...' AS sql_preview | |||
FROM sql_plan_history | |||
ORDER BY execution_time DESC | |||
) | |||
WHERE ROWNUM <= 10; | |||
-- Cost가 높은 SQL TOP 10 | |||
SELECT * FROM ( | |||
SELECT | |||
plan_id, | |||
optimizer_cost, | |||
cardinality, | |||
SUBSTR(sql_text, 1, 60) AS sql_preview | |||
FROM sql_plan_history | |||
ORDER BY optimizer_cost DESC | |||
) | |||
WHERE ROWNUM <= 10; | |||
-- Full Table Scan이 발생하는 SQL | |||
SELECT DISTINCT | |||
h.plan_id, | |||
h.statement_id, | |||
d.object_name AS table_name, | |||
h.optimizer_cost | |||
FROM sql_plan_history h | |||
JOIN sql_plan_details d ON h.plan_id = d.plan_id | |||
WHERE d.operation = 'TABLE ACCESS' | |||
AND d.options = 'FULL' | |||
ORDER BY h.optimizer_cost DESC; | |||
-- 인덱스별 사용 빈도 | |||
SELECT | |||
object_name AS index_name, | |||
COUNT(*) AS usage_count, | |||
AVG(cost) AS avg_cost | |||
FROM sql_plan_details | |||
WHERE operation LIKE '%INDEX%' | |||
AND object_name IS NOT NULL | |||
GROUP BY object_name | |||
ORDER BY usage_count DESC; | |||
</source> | |||
=== 조치 결과 보고서 === | === 조치 결과 보고서 === | ||
== SQL 플랜 분석 == | == SQL 플랜 분석 == | ||
2025년 9월 24일 (수) 13:45 기준 최신판
SQL 성능 관리 프로그램
프로그램 개요
SQL 인스펙션
Oracle 19c SQL 실행 계획 저장 및 관리 프로그램
실행 계획 저장용 테이블 생성
-- 실행 계획 히스토리 테이블
CREATE TABLE sql_plan_history (
plan_id NUMBER GENERATED ALWAYS AS IDENTITY,
statement_id VARCHAR2(100),
sql_text CLOB,
plan_hash_value NUMBER,
execution_time TIMESTAMP DEFAULT SYSTIMESTAMP,
username VARCHAR2(128) DEFAULT USER,
module VARCHAR2(64),
action VARCHAR2(64),
optimizer_cost NUMBER,
cardinality NUMBER,
bytes_size NUMBER,
cpu_cost NUMBER,
io_cost NUMBER,
elapsed_seconds NUMBER,
rows_processed NUMBER,
execution_type VARCHAR2(20), -- EXPLAIN, ACTUAL
notes VARCHAR2(4000),
CONSTRAINT pk_sql_plan_history PRIMARY KEY (plan_id)
);
-- 실행 계획 상세 정보 테이블
CREATE TABLE sql_plan_details (
detail_id NUMBER GENERATED ALWAYS AS IDENTITY,
plan_id NUMBER,
operation VARCHAR2(128),
options VARCHAR2(128),
object_name VARCHAR2(128),
object_type VARCHAR2(30),
optimizer VARCHAR2(255),
position NUMBER,
cost NUMBER,
cardinality NUMBER,
bytes_size NUMBER,
partition_start VARCHAR2(255),
partition_stop VARCHAR2(255),
cpu_cost NUMBER,
io_cost NUMBER,
temp_space NUMBER,
access_predicates VARCHAR2(4000),
filter_predicates VARCHAR2(4000),
projection VARCHAR2(4000),
plan_step NUMBER,
parent_step NUMBER,
depth NUMBER,
CONSTRAINT pk_sql_plan_details PRIMARY KEY (detail_id),
CONSTRAINT fk_plan_details FOREIGN KEY (plan_id)
REFERENCES sql_plan_history(plan_id) ON DELETE CASCADE
);
-- 인덱스 생성
CREATE INDEX idx_plan_history_stmt ON sql_plan_history(statement_id);
CREATE INDEX idx_plan_history_hash ON sql_plan_history(plan_hash_value);
CREATE INDEX idx_plan_history_time ON sql_plan_history(execution_time);
CREATE INDEX idx_plan_details_plan ON sql_plan_details(plan_id);
실행 계획 저장 프로시저
CREATE OR REPLACE PROCEDURE save_sql_plan(
p_sql_text IN CLOB,
p_statement_id IN VARCHAR2 DEFAULT NULL,
p_notes IN VARCHAR2 DEFAULT NULL,
p_plan_id OUT NUMBER
) AS
v_statement_id VARCHAR2(100);
v_plan_hash_value NUMBER;
v_plan_id NUMBER;
v_sql_id VARCHAR2(13);
BEGIN
-- Statement ID 생성 (제공되지 않은 경우)
IF p_statement_id IS NULL THEN
v_statement_id := 'PLAN_' || TO_CHAR(SYSTIMESTAMP, 'YYYYMMDDHH24MISSFF');
ELSE
v_statement_id := p_statement_id;
END IF;
-- EXPLAIN PLAN 실행
EXECUTE IMMEDIATE 'EXPLAIN PLAN SET STATEMENT_ID = ''' || v_statement_id ||
''' FOR ' || p_sql_text;
-- Plan Hash Value 조회
SELECT MAX(plan_hash_value)
INTO v_plan_hash_value
FROM plan_table
WHERE statement_id = v_statement_id;
-- 히스토리 테이블에 저장
INSERT INTO sql_plan_history (
statement_id,
sql_text,
plan_hash_value,
execution_time,
username,
optimizer_cost,
cardinality,
bytes_size,
cpu_cost,
io_cost,
execution_type,
notes
)
SELECT
v_statement_id,
p_sql_text,
plan_hash_value,
SYSTIMESTAMP,
USER,
MAX(cost),
MAX(cardinality),
MAX(bytes),
MAX(cpu_cost),
MAX(io_cost),
'EXPLAIN',
p_notes
FROM plan_table
WHERE statement_id = v_statement_id
RETURNING plan_id INTO v_plan_id;
-- 상세 정보 저장
INSERT INTO sql_plan_details (
plan_id,
operation,
options,
object_name,
object_type,
optimizer,
position,
cost,
cardinality,
bytes_size,
partition_start,
partition_stop,
cpu_cost,
io_cost,
temp_space,
access_predicates,
filter_predicates,
projection,
plan_step,
parent_step,
depth
)
SELECT
v_plan_id,
operation,
options,
object_name,
object_type,
optimizer,
position,
cost,
cardinality,
bytes,
partition_start,
partition_stop,
cpu_cost,
io_cost,
temp_space,
access_predicates,
filter_predicates,
projection,
id,
parent_id,
depth
FROM plan_table
WHERE statement_id = v_statement_id
ORDER BY id;
-- PLAN_TABLE 정리
DELETE FROM plan_table WHERE statement_id = v_statement_id;
COMMIT;
p_plan_id := v_plan_id;
DBMS_OUTPUT.PUT_LINE('실행 계획이 저장되었습니다. Plan ID: ' || v_plan_id);
DBMS_OUTPUT.PUT_LINE('Statement ID: ' || v_statement_id);
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('오류 발생: ' || SQLERRM);
RAISE;
END save_sql_plan;
/
실행 계획 조회 프로시저
CREATE OR REPLACE PROCEDURE display_sql_plan(
p_plan_id IN NUMBER
) AS
v_sql_text CLOB;
v_statement_id VARCHAR2(100);
BEGIN
-- SQL 텍스트 조회
SELECT sql_text, statement_id
INTO v_sql_text, v_statement_id
FROM sql_plan_history
WHERE plan_id = p_plan_id;
DBMS_OUTPUT.PUT_LINE('========================================');
DBMS_OUTPUT.PUT_LINE('Plan ID: ' || p_plan_id);
DBMS_OUTPUT.PUT_LINE('Statement ID: ' || v_statement_id);
DBMS_OUTPUT.PUT_LINE('========================================');
DBMS_OUTPUT.PUT_LINE('SQL Text:');
DBMS_OUTPUT.PUT_LINE(v_sql_text);
DBMS_OUTPUT.PUT_LINE('========================================');
DBMS_OUTPUT.PUT_LINE('');
-- 실행 계획 트리 형태로 출력
FOR rec IN (
SELECT
LPAD(' ', depth * 2) || operation || ' ' ||
NVL(options, '') || ' ' || NVL(object_name, '') AS operation_tree,
cost,
cardinality,
bytes_size,
access_predicates,
filter_predicates
FROM sql_plan_details
WHERE plan_id = p_plan_id
ORDER BY plan_step
) LOOP
DBMS_OUTPUT.PUT_LINE(
RPAD(rec.operation_tree, 60) ||
' | Cost: ' || NVL(TO_CHAR(rec.cost), 'N/A') ||
' | Rows: ' || NVL(TO_CHAR(rec.cardinality), 'N/A')
);
IF rec.access_predicates IS NOT NULL THEN
DBMS_OUTPUT.PUT_LINE(' Access: ' || rec.access_predicates);
END IF;
IF rec.filter_predicates IS NOT NULL THEN
DBMS_OUTPUT.PUT_LINE(' Filter: ' || rec.filter_predicates);
END IF;
END LOOP;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('Plan ID ' || p_plan_id || '를 찾을 수 없습니다.');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('오류 발생: ' || SQLERRM);
END display_sql_plan;
/
실행 계획 비교 프로시저
CREATE OR REPLACE PROCEDURE compare_sql_plans(
p_plan_id1 IN NUMBER,
p_plan_id2 IN NUMBER
) AS
v_cost1 NUMBER;
v_cost2 NUMBER;
v_rows1 NUMBER;
v_rows2 NUMBER;
BEGIN
-- Plan 1 정보
SELECT optimizer_cost, cardinality
INTO v_cost1, v_rows1
FROM sql_plan_history
WHERE plan_id = p_plan_id1;
-- Plan 2 정보
SELECT optimizer_cost, cardinality
INTO v_cost2, v_rows2
FROM sql_plan_history
WHERE plan_id = p_plan_id2;
DBMS_OUTPUT.PUT_LINE('========================================');
DBMS_OUTPUT.PUT_LINE('실행 계획 비교');
DBMS_OUTPUT.PUT_LINE('========================================');
DBMS_OUTPUT.PUT_LINE('Plan ID 1: ' || p_plan_id1);
DBMS_OUTPUT.PUT_LINE(' Cost: ' || v_cost1 || ', Rows: ' || v_rows1);
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE('Plan ID 2: ' || p_plan_id2);
DBMS_OUTPUT.PUT_LINE(' Cost: ' || v_cost2 || ', Rows: ' || v_rows2);
DBMS_OUTPUT.PUT_LINE('');
-- 차이 분석
IF v_cost1 < v_cost2 THEN
DBMS_OUTPUT.PUT_LINE(' Plan 1이 더 효율적입니다 (Cost 차이: ' ||
ROUND((v_cost2 - v_cost1) / v_cost1 * 100, 2) || '%)');
ELSIF v_cost2 < v_cost1 THEN
DBMS_OUTPUT.PUT_LINE(' Plan 2가 더 효율적입니다 (Cost 차이: ' ||
ROUND((v_cost1 - v_cost2) / v_cost2 * 100, 2) || '%)');
ELSE
DBMS_OUTPUT.PUT_LINE('동일한 Cost입니다');
END IF;
DBMS_OUTPUT.PUT_LINE('========================================');
-- 상세 비교
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE('상세 차이점:');
FOR rec IN (
SELECT
p1.operation AS op1,
p1.object_name AS obj1,
p1.cost AS cost1,
p2.operation AS op2,
p2.object_name AS obj2,
p2.cost AS cost2,
p1.plan_step
FROM sql_plan_details p1
FULL OUTER JOIN sql_plan_details p2
ON p1.plan_step = p2.plan_step
AND p2.plan_id = p_plan_id2
WHERE p1.plan_id = p_plan_id1
AND (p1.operation != p2.operation
OR NVL(p1.object_name, 'N/A') != NVL(p2.object_name, 'N/A')
OR p1.cost != p2.cost)
ORDER BY p1.plan_step
) LOOP
DBMS_OUTPUT.PUT_LINE('Step ' || rec.plan_step || ':');
DBMS_OUTPUT.PUT_LINE(' Plan 1: ' || rec.op1 || ' ' || NVL(rec.obj1, '') ||
' (Cost: ' || NVL(TO_CHAR(rec.cost1), 'N/A') || ')');
DBMS_OUTPUT.PUT_LINE(' Plan 2: ' || rec.op2 || ' ' || NVL(rec.obj2, '') ||
' (Cost: ' || NVL(TO_CHAR(rec.cost2), 'N/A') || ')');
END LOOP;
END compare_sql_plans;
/
실행 계획 검색 뷰
CREATE OR REPLACE VIEW v_sql_plan_summary AS
SELECT
h.plan_id,
h.statement_id,
h.execution_time,
h.username,
h.optimizer_cost,
h.cardinality AS estimated_rows,
h.cpu_cost,
h.io_cost,
SUBSTR(h.sql_text, 1, 100) AS sql_preview,
h.notes,
COUNT(d.detail_id) AS plan_steps
FROM sql_plan_history h
LEFT JOIN sql_plan_details d ON h.plan_id = d.plan_id
GROUP BY
h.plan_id,
h.statement_id,
h.execution_time,
h.username,
h.optimizer_cost,
h.cardinality,
h.cpu_cost,
h.io_cost,
h.sql_text,
h.notes
ORDER BY h.execution_time DESC;
사용 예제
-- 예제 1: SQL 실행 계획 저장
DECLARE
v_plan_id NUMBER;
v_sql CLOB := 'SELECT e.employee_id, e.first_name, e.last_name, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.salary > 5000
ORDER BY e.last_name';
BEGIN
save_sql_plan(
p_sql_text => v_sql,
p_statement_id => 'EMP_DEPT_JOIN_001',
p_notes => '급여 5000 이상 직원 조회',
p_plan_id => v_plan_id
);
DBMS_OUTPUT.PUT_LINE('저장된 Plan ID: ' || v_plan_id);
END;
/
-- 예제 2: 저장된 실행 계획 조회
BEGIN
display_sql_plan(p_plan_id => 1);
END;
/
-- 예제 3: 실행 계획 비교
BEGIN
compare_sql_plans(p_plan_id1 => 1, p_plan_id2 => 2);
END;
/
-- 예제 4: 실행 계획 요약 조회
SELECT * FROM v_sql_plan_summary
WHERE execution_time >= SYSDATE - 7
ORDER BY optimizer_cost DESC;
실행 계획 분석 함수들
-- Full Table Scan 발생 여부 확인
CREATE OR REPLACE FUNCTION has_full_table_scan(
p_plan_id IN NUMBER
) RETURN VARCHAR2 AS
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM sql_plan_details
WHERE plan_id = p_plan_id
AND operation = 'TABLE ACCESS'
AND options = 'FULL';
IF v_count > 0 THEN
RETURN 'YES (' || v_count || ' occurrences)';
ELSE
RETURN 'NO';
END IF;
END;
/
-- 인덱스 사용 여부 확인
CREATE OR REPLACE FUNCTION get_indexes_used(
p_plan_id IN NUMBER
) RETURN VARCHAR2 AS
v_indexes VARCHAR2(4000);
BEGIN
SELECT LISTAGG(object_name, ', ') WITHIN GROUP (ORDER BY object_name)
INTO v_indexes
FROM (
SELECT DISTINCT object_name
FROM sql_plan_details
WHERE plan_id = p_plan_id
AND operation LIKE '%INDEX%'
AND object_name IS NOT NULL
);
RETURN NVL(v_indexes, 'No indexes used');
END;
/
-- Cost 기반 성능 등급
CREATE OR REPLACE FUNCTION get_performance_grade(
p_plan_id IN NUMBER
) RETURN VARCHAR2 AS
v_cost NUMBER;
v_grade VARCHAR2(1);
BEGIN
SELECT optimizer_cost
INTO v_cost
FROM sql_plan_history
WHERE plan_id = p_plan_id;
CASE
WHEN v_cost < 10 THEN v_grade := 'A';
WHEN v_cost < 100 THEN v_grade := 'B';
WHEN v_cost < 1000 THEN v_grade := 'C';
WHEN v_cost < 10000 THEN v_grade := 'D';
ELSE v_grade := 'F';
END CASE;
RETURN v_grade || ' (Cost: ' || v_cost || ')';
END;
/
실행 계획 리포트 생성
CREATE OR REPLACE PROCEDURE generate_plan_report(
p_plan_id IN NUMBER
) AS
v_sql_text CLOB;
v_cost NUMBER;
v_rows NUMBER;
v_statement_id VARCHAR2(100);
v_execution_time TIMESTAMP;
BEGIN
-- 기본 정보 조회
SELECT sql_text, optimizer_cost, cardinality, statement_id, execution_time
INTO v_sql_text, v_cost, v_rows, v_statement_id, v_execution_time
FROM sql_plan_history
WHERE plan_id = p_plan_id;
DBMS_OUTPUT.PUT_LINE('╔═══════════════════════════════════════════════════════════════╗');
DBMS_OUTPUT.PUT_LINE('║ SQL 실행 계획 분석 리포트 ║');
DBMS_OUTPUT.PUT_LINE('╚═══════════════════════════════════════════════════════════════╝');
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE(' 기본 정보');
DBMS_OUTPUT.PUT_LINE(' Plan ID: ' || p_plan_id);
DBMS_OUTPUT.PUT_LINE(' Statement ID: ' || v_statement_id);
DBMS_OUTPUT.PUT_LINE(' 생성 시간: ' || TO_CHAR(v_execution_time, 'YYYY-MM-DD HH24:MI:SS'));
DBMS_OUTPUT.PUT_LINE(' 성능 등급: ' || get_performance_grade(p_plan_id));
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE(' SQL 문장');
DBMS_OUTPUT.PUT_LINE(v_sql_text);
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE(' 통계 정보');
DBMS_OUTPUT.PUT_LINE(' Optimizer Cost: ' || v_cost);
DBMS_OUTPUT.PUT_LINE(' Estimated Rows: ' || v_rows);
DBMS_OUTPUT.PUT_LINE(' Full Table Scan: ' || has_full_table_scan(p_plan_id));
DBMS_OUTPUT.PUT_LINE(' Indexes Used: ' || get_indexes_used(p_plan_id));
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE(' 실행 계획 상세');
display_sql_plan(p_plan_id);
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE('═══════════════════════════════════════════════════════════════');
END generate_plan_report;
/
정리 및 유지보수 프로시저
-- 오래된 실행 계획 삭제
CREATE OR REPLACE PROCEDURE cleanup_old_plans(
p_days_to_keep IN NUMBER DEFAULT 30
) AS
v_deleted_count NUMBER;
BEGIN
DELETE FROM sql_plan_history
WHERE execution_time < SYSTIMESTAMP - p_days_to_keep
RETURNING COUNT(*) INTO v_deleted_count;
COMMIT;
DBMS_OUTPUT.PUT_LINE(v_deleted_count || '개의 오래된 실행 계획을 삭제했습니다.');
END cleanup_old_plans;
/
활용 쿼리 모음
-- 최근 저장된 실행 계획 TOP 10
SELECT * FROM (
SELECT
plan_id,
statement_id,
execution_time,
optimizer_cost,
SUBSTR(sql_text, 1, 50) || '...' AS sql_preview
FROM sql_plan_history
ORDER BY execution_time DESC
)
WHERE ROWNUM <= 10;
-- Cost가 높은 SQL TOP 10
SELECT * FROM (
SELECT
plan_id,
optimizer_cost,
cardinality,
SUBSTR(sql_text, 1, 60) AS sql_preview
FROM sql_plan_history
ORDER BY optimizer_cost DESC
)
WHERE ROWNUM <= 10;
-- Full Table Scan이 발생하는 SQL
SELECT DISTINCT
h.plan_id,
h.statement_id,
d.object_name AS table_name,
h.optimizer_cost
FROM sql_plan_history h
JOIN sql_plan_details d ON h.plan_id = d.plan_id
WHERE d.operation = 'TABLE ACCESS'
AND d.options = 'FULL'
ORDER BY h.optimizer_cost DESC;
-- 인덱스별 사용 빈도
SELECT
object_name AS index_name,
COUNT(*) AS usage_count,
AVG(cost) AS avg_cost
FROM sql_plan_details
WHERE operation LIKE '%INDEX%'
AND object_name IS NOT NULL
GROUP BY object_name
ORDER BY usage_count DESC;