Database

데이터베이스에서 인덱스를 사용하는 이유?

DB에서 검색 성능을 높이기 위해 index을 활용합니다. 특정 컬럼에 대해 index을 생성하게 되면 컬럼에 대해서 정렬된 상태로 저장하기 때문에 효과적으로 값을 찾아내는 것이 가능하다.

하지만, index을 활용하게 되면 추가적인 저장공간을 통해 관리되기 때문에, 인덱스로 지정한 컬럼에 대해 생성, 수정, 삭제, 등이 빈번하게 발생되면 성능상 문제가 발생할 수 있다.

이러한 인덱스를 구성하는 방식에는 대표적으로 2가지 방식이 있는데, B+ Tree, HashTable 방식이 있습니다.

key, value를 한 쌍으로 key을 이용해서 hashing 을 수행해서 hash table에 보관하기 때문에, 평균적으로 O(1)의 시간에 value를 찾을 수 있다.

모든 리프노드가 같은 레벨에 존재하는 B+ Tree의 형태를 활용하여 인덱스를 표현할 수 있습니다. 또한 각각의 리프노드가 양방향 연결리스트로 존재하기 때문에 비교연산자를 활용한 쿼리에 대해서도 인덱스가 가능하다는 특징이 있다.

MySQL의 DB Engine인 InnoDB는 이러한 B+Tree를 기반으로 구성되어 있다.

Clustering Index, Secondary Index

Clustering Index의 경우 테이블 1개당 한개의 인덱스를 구성할 수 있다. primary key 혹은 unique not null 한 컬럼을 대상으로 설계를 할수 있고 테이블 생성과 동시에 자동으로 생성되는 인덱스이다.

반면 secondary index의 경우 테이블에 대해서 여러 개 생성하는 것이 가능하다.

B-Tree, B+Tree

B-Tree, B+Tree 모두 리프노드가 같은 레벨에 있는 트리를 기반으로 활용되는 자료구조 인데, 가장 큰 특징은 Branch Node에 데이터를 저장하느냐에 여부에 따라 달라집니다. B-Tree의 경우 branch node에 key및 value를 모두 저장할 수 있고, B+Tree는 리프노드에만 데이터를 저장하게 된다. 이렇게 하므로써 B+ Tree에 대한 검색 속도는 linear list에 대한 선형 검색속도에서 해결되지만, B-Tree에 대해서는 모든 노드에 대한 검색을 진행하야하는 경우가 발생할 수 있다.

또한, 더많은 포인터 확보를 통해 전체적인 Tree-Height 을 낮추는 효과가 있다.

트랜잭션

일련의 작업을 논리적인 단위로 묶어서 해당 트랜잭션 내에 작업이 모두 수행되거나, 수행 되지 않도록 보장한다.

ACID

트랜잭션 ACID 원칙을 따른는데, Atomicity, Consistency, Isolation, Durability을 의미한다.

Acid(원자성): 하나의 논리적인 단위로 실행되는 트랜잭션은 모두 실행되거나, 모두 실행되지 않도록 보장해야한다.

Consistency(일관성): 트래잭션을 수행할 때에는 반드시 정해진 규칙을 지켜야한다. 트랜잭션을 수행한 이후에 DB의 상태가 유효해야한다.

Isolation(독립성): 트랜잭션 수행 중에는 다른 트랜잭션에 의해 영향을 받지 않아야한다. 공유되는 변수에 대한 수정이 진행될때 다른 트랜잭션에 의해 수정되면 race condition 문제가 발생할 수 있다.

Durability(지속성): 트랜잭션 이후 정상적으로 커밋 된 경우에는 반드시 DB에 저장되어야한다.

Isolation Level

독립성을 지키는 수준에 따라 4가지 단계를 나눠서 이루어진다.

  1. Read Uncommitted: 다른 트랜잭션에서 커밋되지 않은 컬럼에 접근하는 것이 가능하다.
  2. Read Commited: 다른 트랜잭션에서 커밋을 수행한 컬럼에 대한 접근이 가능하다.
  3. Repeatable Read: 반복문을 통해 두 번이상 컬럼에 대해 조회를 진행하더라도 항상 같은 값이 조회되도록 한다.
  4. Serializable: 어떠한 경우에도 트랜잭션에 락을 걸어 다른 트랜잭션이 접근하지 못하도록 한다.

이러한 isolation level은 아래의 동시성 문제에서 발생할 수 있는 경우에 대해 방지하기 위해 활용된다.

Concurrency Control 문제

  1. Dirty Read: 아직 커밋되지 않은 컬럼에 대한 접근을 하는 것이다.
  2. Non-Repeatable Read: 두번 이상 조회를 했을 때 값이 변하는 상황을 의미한다.
  3. Phathom Read: 집합 연산에 대해 반복적으로 조회를 진행했을 때 집합의 크기가 달라지는 경우를 의미한다.

DB 락

트랜잭션의 순차성을 보장하기 위한 방법으로 사용된다.

Lock의 종류로는 공유락, 베타락이 존재하는데, 공유락은 트랜잭션이 데이터를 읽어드릴때 사용되는 락으로 같은 공유락 끼리는 서로 공유가 가능하다.

베타락의 경우, 데이터의 변경을 수행하는 트랜잭션에서 활용하는 락으로 베타락이 해제 되기전까지는 어떠한 접근도 허용하지 않는다.

정규화

테이블 간에 중복을 최소화하고 유연한 상태를 갖추기 위해 테이블을 분리하는 과정을 의미한다. 정규화를 통해 이상현상을 예방한다. 정규화 수준에 따라 1,2,3,BCNF와 같이 단계가 나눠진다.

1차 정규화: 컬럼에 다중값을 모두 제거한다. 2차 정규화: 부분 함수 종속성을 제거한다. 3차 정규화: 이행 함수 종속성을 제거하는 것으로 non prime에 의해 종속적인 non-prime 함수 종속성을 제거한다. BCNF: 항상 모든 속성은 기본키에 의해 종속되어야한다.

보통의 경우 BCNF 이상의 정규화는 수행하지 않는다.

정규화 장/단점

정규화를 통해 이상현상을 제거하고 테이블간의 중복을 최소화할 수 있다. 또한 테이블의 용량을 최소화할 수 있다는 장점이 있지만, 반대로 테이블이 많아지게 되면서 Join 연산이 빈번하게 발생하여 검색 성능이 낮아진다.

역정규화

정규화가 가지는 Join 연산이 많아진다는 문제로, 정규화 수준을 완화한 테이블을 유지한다.

RDBMS vs NoSQL

RDBMS와 NoSQL이 가지는 가장 큰 차이점은 schema, 즉 데이터베이스가 고정된 구조를 가지느냐에 있습니다. RDBMS에 경우 고정된 테이블을 바탕으로 sql와 같은 구조화된 쿼리 언어를 사용한다. RDBMS 방식과 같이 구조화된 설계 방식을 통해 중복을 최소화하여 데이터가 자주 변경되는 경우에도 항상 무결성을 유지할 수 있습니다.

하지만, NoSQL의 경우 정해진 schema를 정의하지 유연하게 데이터를 관리하는 것이 가능하다. 그리하여 사전에 미리 data 형식을 정의하지 않아도 되서 다양한 형태의 데이터를 저장하는 것이 가능하다. 뿐만 아니라 NoSQL은 분산형 데이터베이스를 기반으로 동작되기 때문에 수평적 확장을 통한 수평적인 확장이 가능하다.

이러한 분산형 데이터베이스를 통해 가용성 있는 서비스의 제공이 가능하지만, 반대로 수평적 확장 과정 속에 중복을 어느정도 허용하기 때문에 데이터의 변경이 많이 발생하는 경우 이를 수정하는 데 오래 걸리는 단점이 존재한다.

NoSQL

schema-less 형태로 구조화된 데이터베이스에서 벗어나 다양한 형태의 데이터를 보관하기 위해 나온 데이터베이스이다. 데이터가 크기가 커지고 종류가 다양해지면서 기존의 RDBMS에 저장하기 위해 많은 HW적인 추가비용이 발생하게 된다. 그렇기 때문에 RDBMS에 대해서는 scale-up이 어렵다. 반면 NoSQL은 수평적 확장을 통해 scale-out을 지원하여 많은 양의 데이터를 분산해서 저장하여 데이터에 대한 가용성을 높일 수 있다. 이러한 NoSQL의 종류로 key-Value 방식, document 방식, graph 방식이 있다.

CAP

분산형으로 저장되는 데이터 베이스에 대해서는 CAP 이론이 모두 만족될 수 없다.

Consistency: 모든 노드에 대해 일관성이 있는 결과 조회 가능 Availability: 항상 서비스는 이용가능해야한다는 점 Patition Tolerance: 노드가 고장이 나도 다른 노드를 통해 서비스 대체 가능

네트워크 장애가 발생하지 않는 분산형 데이터베이스는 존재할 수 없기 때문에 CA를 동시에 만족하는 분산형 데이터베이스는 존재할 수 없다.

BASE

보통 분산형 데이테베이스는 아래의 BASE 원칙을 지킨다. Basically Availbalbe: 가용성 Soft-State: 분산 시스템의 구조는 유연한 구조로 되어 있어, 별도의 유저 요청없이도 바뀔 수 있다. Eventually-Consistent: 일관성이 어느 순간 깨질 수 있지만, 언젠가 다시 일관성을 유지한다.

Query Optimizer

SQL에 대한 세부적인 실행 방식을 결정하기 위해 동작한다.

SQL을 정의하게 되면 DBMS 내부에서 Query Optimizer는 여러개 의 실행 계획을 세워서 최적의 성능을 내는 방식을 선택한다. 이때, selectivity를 높이는 방향, join plan, 등을 고려사항으로 둔다.

Delete vs Truncate vs Drop

데이터를 제거하는 SQL 연산이다.

각각의 차이점을 살펴보면

Delete은 특정 조건에 맞는 데이터를 제거할 수 있으며 모든 데이터를 제거하더라도 테이블 용량 자체는 유지된다. 또한 DML 이기 때문에 복구가 가능하다

반면 Truncate는 DDL 기반의 언어이기 때문에 한번 수행하면 다시 복구할 수 없으며 테이블 용량을 모두 초기화한다.

Drop table의 경우 테이블 자체를 삭제하기 위해 사용한다.

Join on vs where

Join 진행시 조인에 대한 조건을 설정할 수 있는데, ON을 통해 조건을 명시하면 Join 연산 전에 조건에 따라 필터링 되며, Where을 통해 명시하게 되면 Join 연산 이후에 조건에 따른 필터링이 수행된다.

MongoDB

MongoDB는 JSON 형태의 데이터가 Document 기반의 NoSQL이다. 가용성과 샤딩, 레플리카셋을 기반으로 분산형 데이터베이스를 지원한다. MongoDB는 ObjectID를 통해 각각의 Document을 구분하는데, ObjectID는 시간을 나타내는 4byte, 임의 난수를 저장하는 5byte, 카운터 3byte로 이루어져있다.

RDBMS와 달리 ACID를 원치 대신에 BASE 원칙에 의거해서 가용성을 우선시한다.

보통의 MongoDB replica set은 3개의 서버로 구성하여 1개의 Primary와 그 외는 Secondary로 사용하여 Secondary에는 Primary에 대한 replication을 저장하게 된다. 이후 Primary가 기능을 하지 못할 경우 secondary간에 투표를 통해 primary가 선출된다.

Redis

Redis은 Key-Value 형태의 NoSQL로 모든 데이터를 메모리에 저장한다는 특징이 있습니다. 문자열, 리스트, 해시, 셋, 등과 같이 다양한 데이터 타입을 지원합니다.

이러한 Redis는 데이터의 지속성을 위해 Disk에 데이터를 백업하여 서버가 내려가도 데이터가 유지되도록한다.

Disk 저장방식으로는 특정 시점의 데이터를 저장하는 snapshot 방식과 생성/수정/삭제와 같은 연산이 로그 형태로 저장되는 AOF방식을 통해 Disk에 백업을 만들수 있다.

SQL의 처리 과정

DB로 전달되는 SQL의 경우 parsing을 거쳐, optimizer에 의해 최적의 실행계획이 선택되어 실행되게 된다. parsing의 과정에서 문법, 구조적인 오류를 검색하게 되고, shared pool 내부의 library cache 영역을 검색하게 되는데, 이 과정에서 이전에 sql을 실행했는지 여부를 확인할 수 있다. 이를 통해 sql이 있는 경우 해당 sql execution plan에 의해 sql이 실행된다. 이렇게 실행되는 방식이 soft-parsing이고, library cache에 sql이 존재하지 않고, optimization, row source generation을 모두 거쳐야하는 경우 hard-parsing이다.

즉, library cache는 sql 처리 과정에서 동작하는 캐시라고 이해하면 된다. soft-parsing을 적극적으로 활용하기 위해, 같은 sql문을 사용하거나, 바인드 변수를 이용해서 최대한 같은 sql문을 활용하도록 하는것이 sql 처리 성능에 도움이 된다.

댓글남기기