DBMS 아키텍처 개요
어플리케이션이나 데이터베이스 사용자에 의해 전달된 SQL 구문은 DBMS(DataBase Management System)를 통해 다양한 처리를 수행 후 저장 장치에 접근해 데이터를 읽거나 쓴다.
•
쿼리 평가 엔진
쿼리 평가 엔진은 입력받은 SQL 구문을 분석하고, 어떤 순서로 기억장치의 데이터에 접근할지를 결정한다. 이렇게 결정되는 계획을 실행 계획 혹은 실행 플랜이라 부르고, 실행 계획에 기반을 두고 데이터에 접근하는 방법을 접근 메서드(access method)라고 부른다. 즉, 쿼리 평가 엔진은 실행 계획을 세우고 실행을 담당하는 DBMS 핵심 모듈이다.
쿼리(query)
쿼리는 ‘질의’를 뜻하는 영단어로, 좁은 의미로는 SELECT 구문을 나타내는 말이고 큰 의미로는 SQL 구문 전체를 나타낸다.
•
버퍼 매니저
DBMS는 버퍼라는 특별한 용도의 메모리 영역을 확보해두는데, 버퍼 메모리를 관리하는 것이 버퍼 매니저이다.
•
디스크 용량 매니저
데이터베이스는 데이터를 영구적으로 저장하고 많은 데이터를 다루는 소프트웨어이다. 디스크 용량 매니저는 이러한 데이터베이스에서 데이터를 어디에 어떻게 저장할지를 관리하며, 데이터의 읽고 쓰기를 제어한다.
•
트랜잭션 매니저와 락 매니저
수백, 수천 명이 동시에 데이터베이스에 접근해서 사용하기 때문에, DBMS 내부에서 트랜잭션이라는 단위로 데이터베이스를 관리한다. 트랜잭션으로 정합성을 유지하고, 필요한 경우 락을 걸어 다른 사람의 요청을 대기시키는 것이 트랜잭션 매니저와 락 매니저의 역할이다.
•
리커버리 매니저
DBMS 내에서 데이터를 정기적으로 백업하고, 문제가 발생하면 백업 데이터를 통해 복구하는 기능을 수행하는 것이 리커버리 매니저이다.
DBMS와 버퍼
DBMS의 트레이드 오프
이와 같이 일반적으로 기억장치는 기억 비용(기억 코스트)에 따라 1차부터 3차까지 계층으로 분류한다. 기억 비용이란 데이터를 저장하는데 소모되는 비용을 말한다.
하드디스크와 같은 경우에는 저렴하기 때문에 하드디스크 용량을 별로 고민하지 않는다. 이런 측면에서 하드디스크는 기억 비용이 낮고, 같은 비용으로 저장할 수 있는 데이터 용량이 많다(피라미드의 면적이 넓다). 반면 메모리 같은 경우에는 데이터 접근 속도는 빠르지만 기억 비용이 높다(같은 비용저장 할 수 있는 용량이 적다).
따라서 많은 데이터를 영속적으로 저장하고자 한다면 속도를 포기해야하고, 속도를 얻고자한다면 많은 데이터를 영속적으로 저장하기 힘들다는 트레이드 오프가 발생한다.
트레이드 오프(상충 관계)
한 가지를 얻으면 반대 급부를 포기해야한다는 의미 용어로, 다른 측면에서 이득을 얻으면서 집합 또는 디자인의 품질, 양, 속성을 없애거나 잃어버리는 일이 수반되는 상황적 결정을 말한다.
DBMS와 기억장치의 관계
DBMS가 사용하는 기억장치는 하드디스크(HDD)와 메모리 두 가지이다.
•
하드디스크
현재의 대부분 DBMS에서 데이터를 저장하는 매체(저장소)는 하드디스크로, DBMS가 대부분의 시스템에서 범용적으로 사용되는 미들웨어이기 때문에 평균적인 수치를 가지는 2차 기억장치인 하드디스크가 저장 매체로 주로 사용된다.
•
메모리
메모리는 하드디스크에 비해 기억 비용이 굉장히 비싸기 때문에, 데이터베이스 서버에 탑재되는 메모리는 아무리 많아도 100GB를 넘는 경우가 거의 없다. 따라서 DBMS에서는 메모리를 저장소로 사용하는 대신, 버퍼로 활용하여 속도를 향상 시킨다.
메모리는 1차 기억장치로 데이터 접근 속도가 매우 빠르기 때문에, 자주 접근하는 데이터를 메모리에 올려두고 데이터 검색을 빠르게 처리한다.
일반적인 SQL 구문의 실행 시간 대부분은 저장소에 접근하여 데이터를 읽고 쓰는데 사용되기 때문에, 디스크 접근을 줄일 수 있다면 굉장히 큰 폭의 성능 향상이 가능하다. 이와 같이 성능 향상을 목적으로 데이터를 저장하는 메모리를 버퍼(buffer) 또는 캐시(cache)라고 부른다.
버퍼에 데이터를 어떻게, 어느 기간동안 저장해둘 지 관리하는 것이 DBMS의 버퍼 매니저이다. 버퍼 매니저가 어떻게 동작하느냐에 따라 데이터베이스 성능에 많은 영향을 미치게 된다.
메모리 위의 두 가지 버퍼
DBMS가 데이터를 유지하기 위해 사용하는 메모리는 데이터 캐시와 로그 버퍼 두 종류가 있다.
•
데이터 캐시
데이터 캐시는 디스크에 있는 데이터의 일부를 메모리에 유지(캐싱)하여, 캐싱된 데이터를 조회하는 경우 저장소에 접근하지 않고 빠르게 처리하기 위해 사용되는 메모리 영역이다.
•
로그 버퍼
갱신(INSERT, DELETE, UPDATE, MERGE)과 관련된 SQL 구문을 받았을 때, 디스크에 있는 데이터를 바로 변경하는 것이 아니라 로그 버퍼 위에 변경 정보를 저장 후 디스크에 변경을 수행한다. 이런 갱신 처리는 비동기로 이루어진다.
갱신 처리에 로그 버퍼를 두고 처리하는 이유는 저장소는 읽는 것 뿐만 아니라 쓰는 것도 메모리에 비해 상당히 느리기 때문이다. 먼저 메모리에 빠르게 저장하여 사용자가 오랜 시간 대기하지 않고 응답을 받을 수 있게 하고, 이후 내부적으로 저장소에 변경된 사항을 업데이트한다.
메모리의 트레이드 오프
메모리는 비싸기 때문에 가용 용량이 적다는 단점 외에도 휘발성이라는 특징을 가지고 있다. 휘발성은 전원 공급이 끊어지면 메모리의 모든 데이터가 사라지는 성질을 말하고, 이 때문에 메모리에는 데이터의 영속성이 없다.
휘발성의 가장 큰 문제점은 장애가 발생했을 때 메모리에 있던 데이터가 모두 사라져 데이터 정합성을 깨뜨린다는 것이다. 로그 버퍼 위의 데이터를 디스크 위의 로그 파일에 반영하기 전에 이런 장애가 발생한다면 데이터를 잃어버리고 복구도 불가능하다. 이런 상황은 DBMS가 갱신 처리를 비동기로 수행하는 이상 언제든 발생할 수 있다.
이를 회피하기 위해 DBMS는 커밋 시점에 반드시 갱신 정보를 저장소의 로그 파일에 저장함으로써, 장애가 발생해도 정합성을 유지할 수 있게 한다. 커밋(Commit)이란 갱신 처리를 확정하는 것으로, DBMS는 커밋된 데이터를 영속화한다.
다만 커밋 역시 저장소 위의 로그 파일에 접근하는 것이기 때문에, 커밋을 수행할 때는 디스크에 동기 접근이 발생하고 이로 인해 지연이 발생할 수 있다. 디스크에 동기 처리를 한다면 데이터 정합성은 높아지지만 성능이 낮아지고, 반대로 성능을 높이려면 데이터 정합성이 낮아지는 트레이드 오프가 발생한다.
시스템 특성에 따른 트레이드 오프
실제 DBMS의 데이터 캐시와 로그 버퍼를 비교해보면, 데이터 캐시에 비해 로그 버퍼의 초기값이 굉장히 작다는 것을 알 수 있다. 이는 데이터베이스가 기본적으로 검색을 메인으로 처리한다고 가정하기 때문이다.
검색을 처리할 때는 검색 대상 레코드가 수백만에서 수천만 건에 달하는 경우도 많다. 반면 갱신 처리를 할 때는 트랜잭션 규모에 따라 다르지만 많아봤자 트랜잭션 한 건마다 수만 건 정도밖에 되지 않는다. 이러한 이유로 갱신 처리보다 검색 데이터에 캐시를 올려놓는 것이 좋다고 판단되는 것이다.
실제로 많은 DBMS에서 물리 메모리에 여유가 있다면 데이터 캐시를 많이 할당하는 것을 추천하고 있다. 만약 시스템이 검색에 비해 갱신이 많다면 초기 설정을 그대로 사용하면 성능이 제대로 나오지 않을 수 있고, 그런 경우 로그 버퍼의 크기를 늘려주는 튜닝(최적화)를 해야한다.
이렇듯 한정적 메모리로 인해, 검색과 갱신 중 어느 것을 우선 하느냐에 대한 트레이드 오프가 발생한다. 최근의 DBMS는 리소스를 자동으로 조정하는 기능을 포함하고 있고, 이를 이용해 메모리 할당을 스스로 조정하는 DBMS도 있다. 다만 이런 자동 설정에 의지하는 것은 많은 주의를 요하는 일이다.
워킹 메모리
DBMS에는 데이터 캐시와 로그 버퍼 이외에도 워킹 메모리(working memory)라는 메모리 영역을 하나 더 가지고 있다. 워킹 메모리는 정렬 또는 해시 관련 처리에 사용되는 작업용 영역으로, SQL에서 정렬 또는 해시가 필요할 때 사용되고 종료되면 해제되는 임시 영역이다.
만약 워킹 메모리 영역이 다루고자 하는 데이터의 양보다 작아 메모리가 부족하다면, 대부분의 DBMS는 워킹 메모리 대신 저장소를 사용하기 때문에 상당한 성능 저하가 발생할 수 있다. 이러한 동작은 OS에서 스왑(swap)과 같은 동작이다.
하나의 SQL 구문을 처리하는데는 문제가 없을 수 있지만, 여러 개의 SQL 구문을 동시에 실행하면 메모리가 넘치는 경우가 있다. 이런 상황을 재현하는 부하 검사를 실시하지 않으면, 메모리 부족 현상이 발생할 지 판별하기 힘들다.
자바에서 힙 메모리가 부족하면 중단되는 것처럼, DBMS도 메모리가 부족하다면 중단하게 만들 수 있다. 하지만 DBMS는 중요 데이터를 보관하고 처리할 때 처리의 지속성을 보장하려하기 때문에, 느려지더라도 어떻게든 끝까지 처리하려 노력한다.
DBMS와 실행 계획
사용자나 개발자가 데이터베이스에서 의식적으로 사용하는 것은 SQL 레벨까지로, 이후의 과정은 SQL 구문을 읽은 DBMS에서 알아서 처리 후 결과를 제공한다. 일반적인 프로그래밍 언어라면, 어디에 있는 데이터를 어떻게 처리할지 등의 절차를 모두 작성해야한다. DBMS에서는 이런 절차들을 모르더라도 사용 가능하지만, RDB를 더 잘 다루기 위해서와 조금 더 나은 성능을 위해서 알아둘 필요가 있다.
쿼리 평가 엔진
RDB에서 데이터 접근 절차는 쿼리 평가 엔진이라는 모듈에서 결정된다. 쿼리 평가 엔진은 SQL 구문을 처음 읽어들이는 모듈이며, 파서나 옵티마이저 같은 여러 서브 모듈로 구성된다.
•
파서
파서는 이름 그대로 구문 분석(parse)를 하는 서브 모듈로, 사용자로부터 입력받은 SQL 구문이 올바른 구문인지 검사한다. 또한 파서는 이후 DBMS 내부의 후속 처리를 효율적으로 처리할 수 있도록, SQL 구문을 정형적인 형식으로 변환한다.
•
옵티마이저
옵티마이저는 파서에서 검사를 마친 쿼리를 받아 데이터 접근법(실행 계획)을 최적화(optimize)한다. 옵티마이저는 인덱스의 유무와 데이터 분산 또는 편향 정도, DBMS 내부의 매개변수 등의 조건을 고려해, 선택가능한 많은 실행 계획을 작성(플랜 생성)하고 그 비용을 계산(비용 평가)한다.
•
카탈로그 매니저
카탈로그 매니저는 옵티마이저가 실행 계획을 세울 때 카탈로그(통계 정보)를 제공한다. 여기서 카탈로그란 내부 정보들을 모아놓은 테이블로, 테이블이나 인덱스의 통계 정보다 저장되어 있다.
•
플랜 평가
옵티마이저가 SQL 구문에서 여러 실행 계획을 세우면, 그 중 최적의 실행 결과를 선택하는 것이 플랜 평가이다. 여기서의 실행 계획은 DBMS가 실행할 수 있는 형태의 코드가 아닌 문자 그대로의 계획서에 가깝다. 이를 통해 실행 계획을 읽고 수정 방안을 고려할 수 있다.
옵티마이저와 통계 정보
옵티마이저는 명령대로 다 잘 처리하는 만능이 아니기 때문에, 데이터베이스 사용자는 옵티마이저를 잘 사용해야한다. 특히 카탈로그 매니저가 관리하는 통계 정보는 데이터베이스 엔지니어가 항상 신경 써야 한다. 일반적으로 카탈로그에 포함되어 있는 통계 정보는 다음과 같다.
•
각 테이블의 레코드 수
•
각 테이블의 필드 수와 필드의 크기
•
필드의 cardinality(값의 개수)
•
팔드값의 히스토그램(분포 정도)
•
필드 내부의 NULL 개수
•
인덱스 정보
이런 통계 정보들을 통해 옵티마이저가 실행계획을 만든다. 테이블에 데이터 삽입/갱신/제거가 이루어질 때 카탈로그 정보가 제대로 갱신되지 않는다면, 옵티마이저는 과거의 정보들을 들고 잘못된 실행계획을 세우게 된다.
이렇듯 올바른 통계 정보는 SQL에 성능에 있어서 굉장히 중요하다. 테이블의 데이터가 많이 바뀌는 경우 통계 정보도 같이 수정해주어야 하는데, 통계 정보 갱신은 대상 테이블이나 인텍스의 크기와 수에 따라 몇 분에서 몇 십분까지 소요되는 실행비용이 높은 작업이다. 따라서 DBMS가 최적의 플랜을 선택할 수 있게 하기 위해 갱신 시점을 확실하게 검토해야 한다.
실행 계획과 SQL 구문의 성능
실행 계획이 만들어지면 DBMS는 그것을 기반으로 데이터 접근을 수행하는데, 접근하는 테이블에 데이터양이 많거나 복잡한 SQL 구문을 실행하면 반응 지연이 발생한다. 이런 SQL 구문의 지연이 발생했을 때, 제일 먼저 해야할 일은 실행 계획을 살펴보는 것이다. 모든 DBMS는 실행 계획을 확인할 수 있는 기능을 제공한다.
실행 계획 살펴보기
shop_id(점포) | shop_name(점포 이름) | rating(평가) | area(지역) |
0001 | a 상점 | 3 | 서울 |
0002 | b 상점 | 5 | 속초 |
0003 | c 상점 | 1 | 수원 |
위와 같은 점포 테이블이 있는 DB에서의 SQL 구문에 따른 실행 계획을 알아보자.
1.
테이블 풀 스캔(Full Scan)
SELECT * FROM Shops;
SQL
복사
레코드 전체를 검색하는 단순한 SQL 구문의 실행의 실행 계획을 살펴보면 다음과 같다.
PostgreSQL 실행 계획
Oracle 실행 계획
이와 같이 DBMS의 실행 계획에는 공통적으로 조작 대상 객체, 객체에 대한 조작의 종류, 조작 대상이 되는 레코드 수를 포함한다.
•
조작 대상 객체
Postgre의 on 절 뒤나 Oracle의 Name과 같이 조작하는 대상 객체를 나타낸다. 위는 shops라는 테이블 명으로 표기가 되었지만, 인덱스나 파티션, 시퀀스와 같은 SQL 구문으로 조작할 수 있는 객체라면 어떤 것이든 올 수 있다.
•
객체에 대한 조작의 종류
PostgreSQL의 Seq Scan(Sequential Scan, 순차 탐색)이나 Oracle의 Operation 필드처럼 객체에 대해 어떤 조작을 하는지를 나타낸다.
•
조작 대상이 되는 레코드 수
위의 두 DBMS 뿐만 아니라 조작 대상이 되는 레코드 수는 대부분 DBMS에서 rows로 나타낸다. SQL 구문에 따라 얼마만큼의 레코드가 처리되는 지를 나타내고, 이는 SQL 구문의 실행 비용을 파악하는데 중요한 지표가 된다. 이 레코드 수는 카탈로그 매니저의 통계 정보에서 파악한 숫자이기 때문에, 실제 SQL 구문이 실행되는 시점의 테이블 레코드 수와 차이가 있을 수 있다.
2.
인덱스 스캔
SELECT * FROM Shops WHERE shop_id = '00050';
SQL
복사
이와 같은 where 조건이 걸린 SQL 구문의 실행 계획은 다음과 같다.
PostgreSQL 실행 계획
Oracle 실행 계획
PostgreSQL의 Index Scan과 Oracle의 INDEX UNIQUE SCAN을 보면, 두 결과 모두 pk_shops라는 인덱스를 사용해 스캔을 수행한 것을 알 수 있다. 그로 인해 두 결과 모두 Rows가 1로 변경된 것을 볼 수 있다.
인덱스는 기본적으로 Hash와 B-tree를 활용하기 때문에, 모집합의 데이터양이 늘어나더라도 인덱스 처리 비용이 완만하게 증가한다. 위의 예시는 모집합의 데이터가 60개 밖에 없지만, 데이터가 더 늘어나면 처리 속도의 차이가 많이 발생할 것이다.
3.
간단한 테이블 결합
SQL에서 지연이 발생하는 경우는 대부분 결합과 관련된 것으로, 결합을 사용하면 실행 계획이 상당히 복잡해져 옵티마이저가 최적의 실행 계획을 세우기 어렵다.
일반적으로 DBMS는 두 테이블을 결합 시 세가지 종류의 알고리즘을 사용한다.
•
Nested Loop : 한쪽 테이블을 읽으면서 결합 조건에 맞는 레코드를 다른 테이블에서 찾는 방식으로, 이중 반복으로 구현된다.
•
Sort Merge : 결합 키를 통해 레코드를 정렬하고 두 테이블을 순차적으로 결합하는 방법이다. 메모리를 많이 사용하는 병합 정렬 특성상, DBMS의 워킹 메모리를 사용한다.
•
Hash : 결합 키값을 해시값으로 사용하여 매핑하는 방법이다. 해시 테이블을 만들어야하기 때문에, 마찬가지로 워킹 메모리를 사용한다.
reserve_id | shop_id | reserve_name |
1 | 00001 | A |
2 | 00002 | B |
3 | 00003 | C |
이와 같은 추가적인 테이블이 있다고 하면,
SELECT shop_name FROM Shops S
INNER JOIN Reservation R ON S.shop_id = R.shop_id;
SQL
복사
이러한 결합 SQL 구문의 실행 계획은 다음과 같다.
PostgreSQL의 실행 계획
Oracle의 실행 계획
두 DBMS 모두 Nested Loop를 사용한다고 출력되는 것을 볼 수 있다.
일반적으로 실행 계획은 트리구조를 가지는데, 중첩 단계가 높을수록 먼저 실행된다. 위의 결합 SQL 구문의 실행 계획을 살펴보면, 중첩 단계가 높은 순서대로 Index Scan & Seq Scan → Nested Loop 순으로 이루어진다. 이를 통해 결합 전에 테이블 접근이 먼저 수행된다는 것을 알 수 있다. Index Scan과 Seq Scan의 경우 동일한 중첩 단계를 가지고 있으므로, 더 위에 있는 Seq Scan이 먼저 수행되고 이렇게 먼저 접근되는 Reservation 테이블이 구동 테이블(Driving Table)이 된다.