[Database] 14, 15, 16 스키마, 인덱스, SQL 튜닝
- 튜닝은 스키마튜닝 -> 인덱스튜닝 -> 쿼리튜닝 순으로 진행한다.
- DBA는 이 3가지의 튜닝을 진행하며 성능을 관리한다.
스키마 튜닝
- 디자인 상에서 정규화를 많이하면 할수록, redundancy는 감소하지만, join으로인해 workload가 증가한다.
- 기본적으로
제 3 정규화
를 한다.workload가 적다면
, 물리적 데이터베이스 크기를 줄이기위해BCNF로 정규화한다.
workload가 많다면
,- 필드를 추가해 역정규화를 (denormalize)한다.
- 혹은 수평 분할을 고려한다.
예시
Contracts(C,S,J,D,P,Q,V)
F = {JP -> C, SD->P}, PK = C, CK = JP
- 위의 예시는 부분종속과 이전종속이 존재하지 않으므로, 제3정규형이다.
- 후보키 (JP, C)의 진부분집합이 다른 nonprime attribute를 결정하지 않으며,
- nonprime attribute가 다른 nonprime attribute를 결정하지 않는다.
- 하지만, 함수적 종속관계에서 SD는 후보키가 아니므로, BCNF는 아니다.
- 위 스키마를 BCNF로 만들기 위해선 SDP를 따로 분해해 스키마를 만들어야한다.
BCNF로 정규화하기
- BCNF로 만들기
SDP, CSJDQV
위 두 relation모두 BCNF이다.
lossless decomposition을 만족
하지만,dependency preserving 하지는 않다.
- 왜냐하면,
JP->C
의 함수적 종속이 사라졌기 때문이다.
- 왜냐하면,
- 함수적 종속성 보존을 위해서는 별도의 ASSERTION을 정의해줘야한다.
- 스키마 1개 추가
- 따라서,
dependency preserving
하기 위해서는JPC
라는 스키마가 1개 추가되어야한다.{SDP}, {CSJDQV}, {CJP}
- 스키마가 1개 추가됨에따라, JOIN 성능에 영향을 주기때문에,
JP에 Index
를 걸어주면된다.
SPQ->V
종속성이 추가되었을때CSJDPQV
=>SPQ->V
+CSJDPQ
CSJDPQ
=>SD=>P
+CSJDQ
- 따라서,
SDP
,CSJDQ
,SPQV
모두 BCNF가 된다. - 단, 이 경우에도 CJP도 더해줘야 dependency preserving이 가능하다.
역정규화를 해야할때
SDP, CSJDQV
-
CP=>Q
와 같은 질의가 중요하다면 위 스키마를 사용하는게 아닌 3차정규형 상태 그대로를 사용하는 것이 좋다. -
V와 B를 비교
하는 질의가 중요하다면,CSJDPQVB
로 B를 스키마에 추가하는 것이 좋다.- 물론, 이렇게 된다면 nonprime attribute D가 B를 결정하므로, 2차정규형으로 역정규화된 것이다.
BCNF 분할하기
- 위 예시에서 아래 스키마는 각각 BCNF이다.
{SDP, CSJDQV}
- 아래와 같은 쿼리가 중요하다고 가정하자.
- S가 가진 C를 가져와라.
- D가 참여하는 C를 가져와라.
- 현재 모든 스키마가 BCNF이지만 위 쿼리는 JOIN이 필요하지 않다.
- 하지만 성능을 더 올릴 수 있는데, 메모리 I/O를 줄이는 방식으로 가능하다.
- CS, CD, CJQV로 분할하여 쿼리 속도를 높일 수 있다.
- 하지만, CS->V를 가져오는 쿼리는 당연하게도 느려지게된다.
- 위에서 봤듯, 스키마 튜닝은 성능 향상에 지대한 영향을 주지만, 그만큼 어렵다.
- 튜닝은 SQL튜닝-> 인덱스튜닝->스키마튜닝 의 순서로 진행하며, 성능 향상의 정도는 이 순서의 반대이다.
Horizontal Decompositions
- 수평분할은 하나의 relation을 여러개의 relation으로 잘라내는 것이다.
스키마 분할은 수직분할이며, 속성을 칼럼단위로 projection
으로 자른 것이다.수평분할은 selection으로 잘라낸 것
이다.- 이러한 수평분할을 빅데이터에서는
sharding
이라고한다.
- 이러한 수평분할을 빅데이터에서는
예시
-
쿼리에서 특정 속성값이 10000 이상인 record를 뽑아낸다고 가정하자.
-
위 쿼리를 빠르게하는 방법은 2가지가 있다.
- val 필드에 index를 건다.
- 10000개를 기준으로 수평분할을 한다.
- 수평분할해놓고, 모든 데이터가 필요하다면,
VIEW
를 정의한다.
- 수평분할해놓고, 모든 데이터가 필요하다면,
쿼리, 뷰 튜닝
- 특정 쿼리가 느리다면, 인덱스를 재생성한다.
- 인덱스를 다시 만들어도 느리다면, DBMS OPTIMIZER의 약점을 확인해야한다
Query Optimizer
쿼리가 처리되는 절차
- SQL 쿼리가 입력된다.
Quary parser
에 전달된다.- 파서는 이를 AST(추상구문트리)로 변환한다.
Query Rewrite
에 전달된다.- 재작성기에서는 정규형식으로 쿼리를 변환한다.
Query Optimizer
에 전달된다.- 추상구문트리를 효율적인 계획으로 변환한다.
- Plan generator와 Cost Estimator로 구성되어있다.
- Cost Estimator는 시스템 카탈로그에서 다른 테이블에 대한 스키마를 가져와 쿼리 계획을 어떻게 짜는 것이 효율적인가에 대해 파악한다.
Query Executor
- 효율적으로 생성된 쿼리에 대해 실행한다.
구성요소
- Query parser
- 권한 확인
- 쿼리 파서에 전달되는 쿼리는 사용자의 권한을 체크하고, 쿼리를 수행할 권한이 있는 사용자인지 확인하는 절차를 가진다.
- Parse tree 생성
- AST(추상구문트리)를 생성한다.
- Query rewriter
- 추상구문트리를 캐노니컬 폼으로 변환한다.
- 뷰를 가져와서 다른 테이블에 조인하는 쿼리를 4개의 테이블 쿼리로 평면화해 일부 경우에 뷰를 참조한다.
- 일부 쿼리를 join으로 변환할 수 있기에, subquery들을 더 적은 수의 쿼리 블록으로 변환한다.
- Query Optimizer
비용을 기반
으로한다.- 비용을 계산할때,
한 번에 1개의 쿼리블록만 계산
한다. - 이때, 쿼리 블록이란 아래의 묶음이다.
- Select, Project, Join
- Group by / Aggregate FNC
- Order By
- 비용을 계산할때,
- 시스템 카탈로그에서
쿼리블록마다
최소비용으로 찾는 통계를 사용한다. - 항상 올바르게 최적화하는 것은 아니다.
- 쿼리블록들은 관계대수로 변환된다.
- 아래와 같은 쿼리가 있다고 가정하자.
-
위 쿼리 블록은 아래와 같은 관계대수로 변환되며,
-
관계대수는 아래와 같은 트리형태로 다시 변환된다.
약점
nested subquery 지양
하기- 반드시 써야하는 경우를 제외하곤, 1개의 쿼리블록 내에 모두 표현하는 것이 좋다.
- 쿼리블록을 기준으로 최적화되기 때문이다.
Distinct
줄이기- distinct는 기본적으로 쿼리 실행전에 sorting하기때문에 느려진다.
Group By
줄이기- Group By 또한 정렬하기때문에 느려진다.
Null값
포함한 selection 줄이기연산 selection 줄이기
OR
절 줄이기임시테이블 지양
하기
정리
- DB 튜닝 == 개념적 스키마의 변경
- 성능상에서 괄목할만한 변화를 가져올 수 있다.
- workload에 따라 BCNF아래의 3NF 혹은 더 아래의 정규화를 수행한다.
- 역정규화를 통해 분할된 테이블을 합친다.
- BCNF형 스키마를 수직-수평분할을 통해 더 분할 할 수 있다.
- 무결성 검증을 위해 항상 종속성은 보존되어야한다.
- 성능상에서 괄목할만한 변화를 가져올 수 있다.
- 인덱스 튜닝
- 인덱스는 항상 삭제 - 생성 - 재생성으로 성능적으로 잘 튜닝되어야한다.
- DBMS는 항상 left-deep plan만을 고려한다.
튜닝 의사결정
Workload
의사결정
Query
- 가장 중요한 쿼리가 무엇이고, 얼마나 자주 일어나는가?
- 어떤 relation에 접근하는가
- 어떤 속성값을 받아오는가
- selection, join에 사용되는 속성이 무엇이고, selectivity가 어느정도인가?
- 가장 중요한 업데이트가 무엇이고, 얼마나 자주 일어나는가?
- selection, join에 사용되는 속성이 무엇이고, selectivity가 어느정도인가?
- INSERT/DELETE/UPDATE 중 무엇이고 어떤 속성이 영향받는지?
- 그리고 이 쿼리와 업데이트의 이상적인 성능은 어느정도인가?
- 가장 중요한 쿼리가 무엇이고, 얼마나 자주 일어나는가?
Index
- 어떤 인덱스를 만들어야하지?
- 어떤 relation에 index를 만들 지,
- 어떤 필드가 search key가 되고, 여러 index를 만들어야하는지?
- 각각의 인덱스를 어떤 종류로 만들어야하는지?
- Clustered
- Hash/Tree
- Dynamic/Static
Schema
- 개념적 스키마를 변경해야하는지?
- 다른 정규화된 스키마를 사용할지?
- 역정규화를 할지?
- 수평분할/중복/뷰 를 통해 처리할지?
인덱스 튜닝
- 위 2개의 튜닝은 스키마 튜닝과 SQL 튜닝이다.
- 성능 향상의 정도는 스키마 튜닝이 RDBMS의 근본적인 원리를 조작하는 것이기때문에 가장 향상의 정도가 높고,
- 그 다음이 인덱스 튜닝이 성능 향상에 많은 도움이 된다.
인덱스란
- 사전적 정의는 색인, DB상에서 데이터를 빠르게
검색
하기 위한 데이터베이스의 일부분을 의미한다. SELECT
절에서WHERE
절에 인덱스가 적용된 칼럼이 사용된다면, DBMS는 인덱스를 사용해 데이터를 검색하게된다.- 항상
정렬된 상태를 유지
하며, 10%의 추가적인 용량이 필요하다.
DB가 데이터를 찾아오는 알고리즘
Full Table Scan
- 페이지의 모든 값을 순회한다.
- Full Table Scan은 아래와 같은 경우에 사용한다.
- 현재 쿼리가 적용가능한 인덱스가 없는 경우
- 인덱스의 처리 범위가 너무 큰 경우
- 크기가 작은 테이블에 액세스하는 경우
- B+Tree
-
- 보통 우리가 아는 BST(Balanced Search Tree)에서
자식 노드가 2개 이상
이고트리의 높이가 항상 같은
자료구조이다. - 데이터 베이스 인덱스의 기본적인 구조이다.
- 보통 우리가 아는 BST(Balanced Search Tree)에서
-
- B+ Tree에서 루트노드와 브랜치노드는 자식노드의 정보를 가지고 있다.
- 리프 노드는 클러스터링인가 논클러스터링인가에 따라 가지고있는 데이터가 다르다.
- 클러스터링 인덱스의 경우, 리프노드에 실제 데이터 페이지가 담겨있으며,
- 논클러스터링 인덱스는 리프노드에 데이터의 주소 페이지가 담겨있다.
클러스터링 인덱스
CREATE TABLE test (
id int PRIMARY KEY,
name char(20),
email char(20) unique
);
- 위의 쿼리에서 인덱스는 총 2개가 생성된다.
- PRIMARY KEY인 id에는
클러스터링 인덱스
- UNIQUE NULL 제약이 걸린 email은
논클러스터링 인덱스
가 생성된다.
- DBMS는
PRIMARY KEY 또는 UNIQUE+NOT NULL 제약이 걸린 칼럼을 클러스터링 인덱스로 생성
하는데,- 우선순위는 물론 PRIMARY KEY에 있다.
- 실제 데이터 자체가 정렬되며
- 테이블당 1개가 존재한다.
- 위에서 언급했듯, 리프 페이지 자체가 데이터를 담고있다.
논클러스터링 인덱스
ALTER TABLE test ADD CONSTRAINT unq_name UNIQUE(name);
--중복을 허용하지 않는 인덱스
CREATE UNIQUE INDEX unq_idx_name ON test(name);
--중복을 허용하는 인덱스
CREATE INDEX idx_name ON test(name);
- UNIQUE + NULL 제약은 논클러스터링 인덱스를 생성한다.
-
또한, CREATE INDEX 명령을 통해 생성하는 인덱스도 논클러스터링 인덱스이다.
- 위에서 언급했듯, 논클러스터링 인덱스의 리프노드는 데이터를 직접 가지고있는 것이 아닌, 데이터가 담겨진 페이지의 주소를 가진다.
- 따라서 별도의 인덱스 페이지가 필요하다.
- 여기에 offset 값으로 원래 값이 페이지의 주소로부터 몇 byte뒤에 있는지를 표현한다.
- 또한 테이블당 여러개 존재할 수 있다.
Hashing Index
- 큰 크기의 데이터를 저장할때, 데이터를 가져오기위해 모든 인덱스를 찾는 것은 비효율적이다.
- 따라서 해싱을 통해 데이터가 디스크의 어느위치에 정확하게 존재하는지 계산할 수 있다.
- 해시함수를 통해 데이터가 저장된 주소를 알아낼 수 있다.
- 해싱에는 2가지 방법이 있다.
- 정적 해싱
- 정적 해싱은 해시함수를 통해 나오는
데이터 주소가 항상 동일
하다는 것이다. - 정적 해싱을 통해 데이터 주소에 담기는
데이터의 양은 항상 동일
하다.
- 정적 해싱은 해시함수를 통해 나오는
- 동적 해싱
- 정적 해싱과 다르게, 데이터 주소에 담기는 데이터의 양이 동적이라는 의미.
- 빠르게 데이터가 커졌다 작아졌다하는 데이터베이스에 적합하다.
인덱스 튜닝
- 인덱스를 만들때 고려해야하는 사항
- 가장 중요한 쿼리를 고려한다.
- 현재 인덱스들을 사용하는 최고의 계획을 고려한다.
- 추가적인 인덱스 생성을 통해 만들 수 있는 계획을 찾아본다.
- 인덱스 만들기 전에는 update에 가해지는 workload에 대해 고려해야한다.
인덱스를 만들면 search는 빠르지만, update가 느려지며, disk 공간도 더 많이 차지한다.
- 인덱스 Selection
- 인덱스를 선택할때에는 INDEX가 생성된 KEY에 대한 WHERE 절에서
- KEY가 EXACT MATCH(=)로 선택된다면
hash index
를 사용한다. - KEY가 RANGE MATCH(>, <)로 선택된다면
tree index
를 사용한다.
- KEY가 EXACT MATCH(=)로 선택된다면
- equality, range 쿼리에서 clustering이 유용한 이이유는 duplicate때문이다.
- 가장 많은 쿼리에 도움을 줄 수 있는 인덱스를 선택해야한다.
- relation마다 1개의 clustred index를 가질 수 있기 때문.
- 인덱스를 선택할때에는 INDEX가 생성된 KEY에 대한 WHERE 절에서
WHERE clause에서의 MultipleCondition
- 범위 선택 연산이 포함되면, range ordering에 맞추기위해
속성의 순서가 중요
- 인덱스들은 중요한 쿼리들에
index-only strategy
를 제공할 수 있다. - index-only starategy에서 clustering은 중요하지 않다.
- 범위 선택 연산이 포함되면, range ordering에 맞추기위해
Join 조건
- inner join에서 hash index는 매우 효율적이다.
- inner tuple을 받기위해서는 반드시 clustered 되어야한다.
쿼리최적화 예시
쿼리 최적화 과정
SELECT LNAME
FROM EMPLOYEE, WORKS_ON, PROJECT
WHERE PNAME = 'AQUARIUS'
AND PNUMBER = PNO
AND ESSN = SSN
AND BDATE > DEC-31-1957;
- 위의 예시를 보면 3개의 relation을 join하는 것을 알 수 있다.
- join clause로는
PNUMBER = PNO
,ESSN = SSN
이 있다. - 나머지 2개의 where clause 는 relation의 특정 데이터를 selection한다.
-
쿼리 OPTIMIZER에게 전달되는 AST는 다음과 같다.
- 이 canonical tree는 3개의 relation을
CARTESIAN PRODUCT
하므로 매우 비효율적이다. - 위 트리는 where절에 맞게 아래와 같은 표준 트리로 변환된다.
- 하지만 이 트리도 실행되지 않는다.
- 쿼리 옵티마이저가 효율적 실행이 가능한 트리로 변환한다.
Pname='Aquarius'
라는 조건으로 1개의 tuple만이 선택될 것이므로, 쿼리 옵티마이저는 이 쿼리를 트리의 가장 왼쪽으로 이동한다.- 기본적으로 쿼리 옵티마이저는 트리의 왼쪽부분부터 확인해나가기때문에, 이 쿼리를 왼쪽으로 옮기면 cartesian product 시 생기는 튜플의 개수가 적어져 실행속도가 빨라지게된다.
- 이제 쿼리 옵티마이저는 Cartesian product를 join과 select 연산을 사용해 치환하게된다.
- 여기서 더 최적화를 할 수 있는데, join 조건에서 사용되는 attribute만을 project하도록 하는 것이다.
- 필요한 attribute만을 읽어도록해 입출력 연산을 최소화할 수 있다.
최적화 속 인덱스 사용
1번예시
SELECT ename
FROM Department, Employee
WHERE dname = 'Toy'
AND Employee.dno = Department.dno;
- 위의 쿼리는 쿼리 옵티마이저가 아래와같이 바꾼다.
- 만약에
Department.dno
에 인덱스가 생성되어있다고 생각해보자.- 이 인덱스는 연산하면서 사용될까?
- 정답은 NO이다.
Department에서 유일한 tuple이 dname조건에 의해 선택되기때문이다.
- 다만,
Employee.dno
는 사용된다.
2번예시
SELECT E.ename, D.mgr
FROM Employee E, Department D
WHERE D.dname = 'sales'
AND E.dno = D.dno
AND E.age = 25;
- 위 쿼리 역시 아래와 같이 계획된다.
- D.dname과 E.age에 걸린 인덱스를 통해 빠른 연산이 가능하다.
- 만약 E.age에 인덱스가 걸려있다면, E.dno 인덱스를 만들 필요는 없다.
댓글남기기