엘리스의 통합검색 기능을 개선하여 유저가 원하는 결과를 내도록 개선한 경험이다.

https://academy.elice.io 여기서 써볼수있다

유저가 겪는 문제

대부분의 유저는 이름이 "LangChain~"으로 시작하는 상품을 검색하기 위해 보통 "Lang"까지만 검색해본다.

하지만 우리의 통합검색 기능은 "Lang"을 입력했을때 "LangChain~" 상품을 보여주지 않았다.

 

왜 이런 현상이 일어나는 것일까? elasticsearch의 analyzer 동작 원리를 잠깐 살펴보고 가자.

 

elasticsearch analyzer 동작 원리

elasticsearch는 문자열이 들어오면 analyzer를 통해 character filter -> tokenizer -> token filter 순서로 전처리를 한다

 

예를 들어  "<strong>LangChain</strong>으로 나만의 업무" 라는 문장이 있다고 가정해보자

 

먼저 character filter는 토큰화 이전에 문자열을 가공하는 역할을 한다.

character filter는 문자열을 어떻게 정리할 것인지에 따라 HTML 태그 제거, 패턴 기반 치환등 여러 종류가 있다.

만약 HTML 태그 제거 character filter를 사용한다면 "LangChain으로 나만의 업무" 와 같은 문자열로 변환된다.

 

다음으로 tokenizer는 문자열을 토큰화(분리)하는 역할을 한다.

tokenizer는 문장을 어떻게 나눌 것인지에 따라 공백 기반 분할, 패턴 기반 분할 등 여러 종류가 있다

공백 기반 Tokenizer를 사용한다면 ["LangChain으로", "나만의", "업무"] 와 같이 문자열을 분리한다.

 

마지막으로 token filter는 토큰화된 문자들을 다시 가공하는 역할을 한다.

token filter 또한 문자열을 어떻게 정리할 것인지에 따라 대소문자 변환, 중복 제거 등 여러 종류가 있다 

대소문자 변환 token filter를 사용한다면 ["langchain으로", "나만의", "업무"] 와 같이 가공한다.

 

이처럼 analyzer는 원본 텍스트를 검색엔진(elasticsearch)이 이해할 수 있는 형태로 변환하는 역할을 한다.

 

문제 원인

다시 유저가 겪던 문제로 돌아가서 원인을 파악해보았는데, 아래와 같았다

  • 현재 공백 기준으로 토큰화하는 tokenizer를 사용하고 있다 (standard tokenizer)
  • fuzziness 허용 편집거리는 1이다 (오타 1개까지 허용)
    • 즉 "Lang_" 까지 편집거리를 허용해주는데,
      실제 저장된 token인 "LangChain으로"는 편집거리가 훨씬 커서 매칭되지 않는다

해결방법

요구사항에 맞는 적절한 analyzer를 생성하여 해결하였다

{
  "analyzer": {
    "camel_analyzer": {
      "filter": ["lowercase"],
      "char_filter": ["camel_filter"],
      "tokenizer": "nori_mixed"
    }
  },
  "char_filter": {
    "camel_filter": {  // 1
      "pattern": "(?<=\\p{Lower})(?=\\p{Upper})",
      "type": "pattern_replace",
      "replacement": " "
    }
  },
  "tokenizer": {
    "nori_mixed": {  // 2
      "type": "nori_tokenizer",
      "decompound_mode": "mixed"
    }
  }
}
  1. CamelCase character filter 를 적용해 입력을 전처리 한다
    • ["LangChain으로 나만의 업무"] → ["lang chain으로 나만의 업무"]
  2. 전처리 결과에 nori_tokenizer를 적용해 조사를 분리한다
    • ["lang chain으로 나만의 업무"] → ["lang", "chain", "으로", "나", "만", "의", "업무"]

위 analyzer를 index에 적용하고, reindex한 후에 "lang"으로 검색하면,

["lang", "chain", "으로", "나", "만", "의", "업무"]에 lang이 포함되어 있으므로 가중치가 높게 계산될 것이다.

결과

의도한대로 "Lang"으로 검색하면 "LangChain~"이름을 가진 상품이 잘 나온다. 👍

 

 

Ref.

'DB' 카테고리의 다른 글

[PostgreSQL] 운영중인 DB 조용히 갈아끼우기  (0) 2025.07.27
[DB] MongoDB 장점  (0) 2022.06.01
[DB] MongoDB 자료구조  (0) 2022.06.01
[DB] Lock 이해하기  (0) 2022.05.20
[DB] 상황에 맞는 인덱스 사용법  (0) 2022.05.20

 

회사의 모든 데이터베이스를 CNPG로 일원화하고자 하는 정책에 따라, 현재 운영 중인 Azure PostgreSQL 서비스를 자체 관리형 CloudNativePG(CNPG) 클러스터로 마이그레이션해야 하는 상황이 되었다.

 

만약 운영 서비스와 연결되지 않은 데이터베이스였다면 단순히 dump & restore 방식으로 쉽게 해결할 수 있었겠지만, 실제 서비스가 연결되어 있어 다른 접근 전략이 필요했다.

 

팀 내에서도 실시간으로 운영 중인 데이터베이스를 마이그레이션해본 경험이 없었기 때문에 팀원들과 충분히 논의하며 마이그레이션 계획을 수립했다.

 

검토 결과 두 가지 선택지가 있었다.

  • 무중단 마이그레이션 (복제 지연 위험 존재)
  • 다운타임 허용 마이그레이션 (데이터 정합성 보장)

우리 서비스의 특성상 교육 시간대만 피하고 사전 공지를 한다면 짧은 다운타임이 발생해도 비즈니스에 큰 영향이 없었기 때문에, 데이터 정합성을 우선시하는 방향으로 결정했다.

마이그레이션 순서

다운타임 허용 마이그레이션 순서는 아래와 같이 정리했다

  • DB 실시간 복제 실행 → 기존 앱 중단 → 데이터 정합성 확인 → 새로운 앱 배포

마이그레이션 방식 선택

마이그레이션 방식으로는 CDC와 Logical Replication 두 가지를 검토했다.

두 방식 모두 내부적으로 WAL(Write Ahead Logging)을 사용한다는 공통점이 있지만, CDC는 Kafka와 같은 추가 이벤트 스트림 컴포넌트가 필요하다는 차이점이 있다.

  • CDC: Source DB → WAL → Debezium → Kafka → Target(PostgreSQL, Elasticsearch 등)
  • Logical Replication: Source DB → WAL → Target DB

이번 케이스에서는 Debezium, Kafka 등의 추가 컴포넌트 설정이 불필요하고, 동일한 PostgreSQL 간의 단순 마이그레이션이므로 Logical Replication을 선택했다.

도구 선택

Logical Replication 구현을 위해 별도의 서드파티 도구 없이 PostgreSQL 내장 기능만으로 동작하는 pgcopydb(https://github.com/dimitri/pgcopydb)를 선택했다.


테스트 환경 세팅

가장 먼저 개발서버에서 사전 테스트를 진행하기 위해 운영환경과 동일한 환경을 세팅하였다

docker compose로 진행했으며, source/target DB에 대해 postgresql.conf를 적절히 작성하였다

version: '3'
services:
    postgres_source:
        image: postgres:13
        environment:
            - POSTGRES_DB=source-db
            - POSTGRES_USER=hanbin
            - POSTGRES_PASSWORD=thisispassword
        volumes:
            - postgres_13-data:/var/lib/postgresql/data
            - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
        ports:
            - "127.0.0.1:5432:5432"
        shm_size: 1g
        command: [
            "-c", "config_file=/etc/postgresql/postgresql.conf",
         ]
    postgres_target:
        image: postgres:16
        environment:
            - POSTGRES_DB=target-db
            - POSTGRES_USER=hanbin
            - POSTGRES_PASSWORD=thisispassword
        volumes:
            - postgres_16-data:/var/lib/postgresql/data
            - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
        ports:
            - "127.0.0.1:5433:5432"
        shm_size: 1g
        command: [
            "-c", "config_file=/etc/postgresql/postgresql.conf",
        ]
volumes:
    postgres_source-data:
        driver: local
    postgres_target-data:
        driver: local

 


테스트 순서

자세한 순서는 아래와 같다

1. Bastion VM pgcopydb 설치 및 연결 테스트
2. Source DB유저에게 모든 sequence 권한 부여
3. Target DB유저에게 임시로 superuser 권한 부여
4. DB 동기화
5. 복제 상황 모니터링
6. 복제 지연 최소화 대기 및 데이터 검증
7. api, worker, cron pod 제거
8. 복제 지연 없음 확인
9. pgcopydb 프로세스 종료
10. replication slot, origin 삭제
11. target DB유저 superuser 권한 제거
12. api, worker, cron 재배포

순서대로 차근차근 진행해보자

1. Bastion VM pgcopydb 설치 및 연결 테스트
sudo apt update
sudo apt install pgcopydb

mkdir -p pgcopydb-migration
cd pgcopydb-migration

export PGCOPYDB_SOURCE_PGURI="postgres://source-db:thisispassword@127.0.0.1:5432/source-db"
export PGCOPYDB_TARGET_PGURI="postgres://target-db:thisispassword@127.0.0.1:5433/target-db"

pgcopydb ping --source "$PGCOPYDB_SOURCE_PGURI" --target "$PGCOPYDB_TARGET_PGURI"
# 기대 출력
## INFO Successfully could connect to source database
## INFO Successfully could connect to target database
2. Source DB유저에게 모든 sequence 권한 부여
-- source-db 유저에게 모든 sequence 권한 부여
GRANT SELECT, USAGE ON ALL SEQUENCES IN SCHEMA public TO "source-db";
3. Target DB유저에게 임시로 superuser 권한 부여
-- 임시로 superuser 권한 부여
ALTER USER "target-db" WITH SUPERUSER;
4. DB 동기화
nohup pgcopydb clone --follow \
  --source "$PGCOPYDB_SOURCE_PGURI" \
  --target "$PGCOPYDB_TARGET_PGURI" \
  --no-owner \
  --no-acl \
  --table-jobs 1 \
  --index-jobs 1 \
  --verbose \
  --dir . \
  > ./pgcopydb-migration.log 2>&1 &

# 프로세스 ID 저장
echo $! > ./pgcopydb-migration.pid

# 실시간 로그 확인
tail -f ./pgcopydb-migration.log

# 진행 상황 확인
./replication_monitoring.sh
6. 복제 지연 최소화 대기 및 데이터 검증
복제지연 확인
SELECT
    slot_name,
    active,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) as lag_size,
    pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn) as lag_bytes
FROM pg_replication_slots
WHERE slot_name = 'pgcopydb';

-- Result --
 slot_name | active | lag_size | lag_bytes
-----------+--------+----------+-----------
 pgcopydb  | t      | 541 kB   |    553920
(1 row)
데이터 일관성 검증
pgcopydb compare schema --verbose
pgcopydb compare data --verbose

약 541KB 만큼의 복제 지연이 있다는 의미이다.

# 데이터 정합성 확인
pgcopydb compare data

# Output
16:11:44.270 162208 INFO   Running pgcopydb version 0.17-1.pgdg22.04+1 from "/usr/bin/pgcopydb"
16:11:44.357 162208 INFO   Using work dir "/tmp/pgcopydb"
16:11:44.358 162208 INFO   SOURCE: Connecting to "postgres://target-db@127.0.0.1:5432/target-db?keepalives=1&keepalives_idle=10&keepalives_interval=10&keepalives_count=60"
16:11:44.361 162208 INFO   Re-using catalog caches
16:11:44.365 162208 INFO   Starting 4 table compare processes
                    Table Name | ! |                      Source Checksum |                      Target Checksum
-------------------------------+---+--------------------------------------+-------------------------------------
                 public.user |   |  9136aaf4-0f32-cd2b-850f-49270ff7c03 |  9136aaf4-0f32-cd2b-850f-49270ff7c03

만약 두 데이터가 다른 경우 Checksum이 다르고, 두번째 컬럼에 !가 나타난다

7. api, worker, cron pod 제거

사용중인 CD tool에서 직접 내렸다

8. 복제 지연 없음 확인

6번 과정에서 진행한 작업을 반복하여 확인한다

9. replication slot, origin 삭제
-- SOURCE DB에서
SELECT pg_drop_replication_slot('pgcopydb');

-- TARGET DB에서
SELECT pg_replication_origin_drop('pgcopydb');
10. target DB유저 superuser 권한 제거
-- superuser 권한 제거
ALTER USER "target-db" WITH NOSUPERUSER;
11. api, worker, cron 재배포

사용중인 CD tool에서 직접 다시 올렸다

 


실제로 진행할때는 개발환경에서 권한과 postgresql 버전 등등을 정확히 동일하게 세팅 하지 않았어서 운영환경에 반영할때 잡음이 많았다.

좀더 꼼꼼하게 개발환경을 세팅했으면 좋았을텐데 아쉬웠다.

 

팀 내에서 경험이 없던 작업이라 문서화에 신경을 많이 썼는데, 문서화가 제일 피곤했다

 

데이터 정합성과 다운타임을 모두 잡는 해결책이 있을지 궁금해졌다.

'DB' 카테고리의 다른 글

[Elasticsearch] 유저가 원하는 검색결과 보여주기 (analyzer)  (1) 2025.07.31
[DB] MongoDB 장점  (0) 2022.06.01
[DB] MongoDB 자료구조  (0) 2022.06.01
[DB] Lock 이해하기  (0) 2022.05.20
[DB] 상황에 맞는 인덱스 사용법  (0) 2022.05.20

RDBMS에 친숙하다면

  • 정규화가 잘 된 데이터모델의 유용함
  • 트랜잭션의 필요성
  • 견고한 저장 엔진을 통해 얻는 확신

을 잘 알고 있을 것이다.

이런 RDBMS를 벗어나 NoSQL로 옮긴다면 어떤 장점이 있을까?

  1. 확장의 용이성MongoDB의 데이터 모델과 지속성 전략은 높은 읽기/쓰기 효율자동 장애조치를 통한 확장의 용이성을 염두에 두고 만들어졌다.
  2. MongoDB는 웹 애플리케이션과 인터넷 기반을 위해 설계된 데이터베이스 관리 시스템이다.
  3. 직관적인 데이터 모델예를 들자면 아래에 사용자에 대한 정보를 몇 개의 필드로 저장한 도큐먼트가 있다.만약 하나의 사용자가 여러개의 이메일을 가질 수 있다면 어떻게 해야할까?하지만 MongoDB는 아래와 같이 사용하면 된다.예시와 같이 MongoDB는 한명의 사람 정보를 얻기 위해서 하나의 도큐먼트로 표현할 수 있다.
  4. 만약 RDBMS였다면 여러개의 테이블에 나눠져 있는 정보를 조인 연산으로 가득찬 SQL 쿼리를 만들어 표현했어야 할 것이다.
  5. { _id: 10, username: 'hanbin', email : [ 'hanbin8269@gmail.com', 'gksqls0128@naver.com' ] }
  6. RDBMS라면 서로 조인을 하기 위해 이메일 과 사용자 테이블을 각각 만들어야 할 것이다.
  7. { _id: 10, username: 'hanbin', email : 'hanbin8269@gmail.com' }
  8. MongoDB는 데이터의 변경이 발생했을 때 스키마에 맞춰야 하는 걱정 없이 구조화 된 도큐먼트를 저장할 수 있다는 장점이 있는데,
  9. 객체 매핑의 단순화MongoDB의 도큐먼트는 JSON에 기반하는데, JSON은 프로그래밍 언어에서 dictionary나 hash map과 유사하다.
  10. 그렇기 떄문에 프로그래밍 언어에서 정의한 객체가 그대로 저장되므로 객체 매퍼의 복잡성이 사라진다.
  11. 개발자들은 주로 객체지향언어를 사용해 작업을 하기 때문에 객체에 잘 매핑되는 데이터 저장 구조를 원한다.

MongoDB는 도큐먼트 지향적인 데이터베이스다.

내부적으로 MongoDB는 Binary JSON의 형태로 도큐먼트를 저장한다.

 

MYSQL은 데이터를 테이블에 행으로서 저장하는 반면

MongoDB가 도큐먼트의 모음과 같이 데이터를 컬렉션에 도큐먼트로 저장한다.

 

MYSQL와 같은 RDBMS는 한 객체의 데이터를 여러개의 테이블로 나누어 표현하는 정규화를 거치는데, 정규화를 너무 많이 한다면 그에 따른 비용이 들어가게 된다.

이는 곧 데이터를 모으는 작업을 해야 한다는 것이다.

예를 들어서 블로그 글 하나를 보여주기 위해서 post와 comment 테이블에 대한 조인 연산이 필요해 진다.

하지만 도큐먼트 지향적인 데이터 모델(MongoDB)에서는 객체를 자연스럽게 모아놓은 형태로 표현함으로 객체를 전체적으로 작업할 수 있다는 점이 있다.

위에서 post 와 comment 등의 블로그 글 하나를 표현하는 모든 데이터 들이 하나의 데이터베이스 객체에 들어갈 수 있다.

스키마가 없는 모델

도큐먼트에는 미리 정해진 스키마가 없다. RDBMS에서는 데이터 열을 테이블에 저장하지만, 각 테이블은 각각의 컬럼에 대해 허용되는 데이터 타입을 명시한다.

이렇게 스키마가 없다는 것은 아래와 같은 장점이 있다.

첫번째로 데이터베이스가 아닌 애플리케이션이 데이터구조를 정한다는 것이다. 이것은 데이터의 구조가 빈번히 변경되는 개발 초기단계에서 개발 속도를 단축시켜 준다.

두번째로 가변적인 속성을 갖는 데이터를 표현할 수 있다는 것이다. 예를 들어 상품 판매 사이트를 만든다고 했을때, 하나의 상품이 어떤 속성을 갖게 될 지 미리 알 수 없으므로 애플리케이션에서는 이러한 가변적인 속성을 처리할 수 있어야 한다.

이런점에서 MongoDB는 새로운 속성은 도큐먼트에 동적으로 처리할 수 있기 때문에 추후에 필요한 데이터필드가 무엇일지 고민할 필요가 없다.

고정된 스키마를 갖는 데이터베이스에서는 EAV(Entity-Attribute-Value) 패턴을 이용한다.

Entity-Attribute-Value model?

데이터베이스 설계 시 오브젝트의 자유도를 최대한 보장해야 하는 경우 고려 가능한 모델

잠재적인 어트리뷰트는 많지만 실제 사용되는 어트리뷰트 수는 적을 떄 사용되는 데이터 모델이다.

  • Entity는 테이블에서의 엔터티 객체를 나타낸다.
  • Attribute는 객체의 어트리뷰트(Field)이다.
  • Value는 객체의 어트리뷰트의 값이다.

애드혹 쿼리

**애드혹 쿼리(ad hoc query)**란 아래 예제처럼 미리 정의된 쿼리가아닌 아래와 같이 파라미터가 늘 변하게되는 변수로 작용하는 되는 쿼리이다. (**user_id**가 매번 변함)

query = "SELECT * FROM users WHERE id=" + user_id

RDBMS만 사용해 봤다면 애드혹 쿼리를 당연한 것으로 생각하기 쉽지만, 사실 모든 데이터베이스가 이런 동적 질의를 지원하는 것은 아니다.

다른 많은 시스템에서 간단하고 확장성 높은 모델을 위해 풍부한 쿼리 기능을 포기하는 경우가 많은데,

MongoDB의 설계 목표 중 하나는 관계형 데이터베이스상에서 매우 필수적은 쿼리 성능을 유지하는 것이다.

MongoDB에서 쿼리 언어는 아래와 같이 작동한다.

db.posts.find({'tags': 'politics', 'vote_count': {'$gt': 10}})

SQL 쿼리는 엄격하게 정규화 된 모델에 의존하는 반면, MongoDB의 쿼리는 태그가 각 도큐먼트에 포함되어 있다고 가정한다.

여러개의 속성 (tags 검색, vote_count 검색)을 조합하여 질의할 수 있는 능력을 보여주는데, 이것이 애드혹 쿼리의 본질적인 강점이다.

인덱스

MongoDB에서 인덱스는 B-Tree(B-Tree?) 로 구현되어 있다.

관계 데이터베이스에서 기본적으로 제공하는 기능인 인덱스는 범위를 스캔하거나 정렬을 하는 것과 같이 간단한 쿼리에 대해 최적화 되어 있다.

하지만 WiredTiger는 LSM tree(Log-Structed Merge-trees)를 지원한다고 한다.

B-Tree?

이진 트리가 자식 노드가 최대 2개인 노드를 말하는 것이라면 B-Tree는 자식 노드의 개수가 2개 이상인 트리를 말한다.

또한 노드내의 데이터가 1개 이상일 수가 있다. 만약 노드 내 최대 데이터 수가 2개라면 2차 B-Tree, 3개라면 3차 B-Tree라고 말한다.

그리고, 노드의 데이터 수가 N개라면 자식 노드의 개수는 N+1개 이다.

또한, 6보다 작은 값은 왼쪽 서브트리, 6과 17 사이의 값은 중간 서브트리, 17보다 큰 값은 오른쪽 서브트리에 들어가는 구조를 볼 수 있다.

탐색법은 작은 값은 왼쪽, 큰값은 오른쪽에 있기 때문에 root 노드부터 시작해 하향식으로 탐색해 나간다.

내부적으로 MongoDB는 Binary JSON의 형태로 도큐먼트를 저장한다.

BSON(Binary JSON)은 무엇인가?

MYSQL은 데이터를 테이블에 행으로서 저장하는 반면

MongoDB가 도큐먼트의 모음과 같이 데이터를 컬렉션에 도큐먼트로 저장한다.

MYSQL와 같은 RDBMS는 한 객체의 데이터를 여러개의 테이블로 나누어 표현하는 정규화를 거치는데, 정규화를 너무 많이 한다면 그에 따른 비용이 들어가게 된다.

이는 곧 데이터를 모으는 작업을 해야 한다는 것이다.

예를 들어서 블로그 글 하나를 보여주기 위해서 post와 comment 테이블에 대한 조인 연산이 필요해 진다.

하지만 도큐먼트 지향적인 데이터 모델(MongoDB)에서는 객체를 자연스럽게 모아놓은 형태로 표현함으로 객체를 전체적으로 작업할 수 있다는 점이 있다.

위에서 post 와 comment 등의 블로그 글 하나를 표현하는 모든 데이터 들이 하나의 데이터베이스 객체에 들어갈 수 있다.

스키마가 없는 모델

도큐먼트에는 미리 정해진 스키마가 없다. RDBMS에서는 데이터 열을 테이블에 저장하지만, 각 테이블은 각각의 컬럼에 대해 허용되는 데이터 타입을 명시한다.

이렇게 스키마가 없다는 것은 아래와 같은 장점이 있다.

첫번째로 데이터베이스가 아닌 애플리케이션이 데이터구조를 정한다는 것이다. 이것은 데이터의 구조가 빈번히 변경되는 개발 초기단계에서 개발 속도를 단축시켜 준다.

두번째로 가변적인 속성을 갖는 데이터를 표현할 수 있다는 것이다. 예를 들어 상품 판매 사이트를 만든다고 했을때, 하나의 상품이 어떤 속성을 갖게 될 지 미리 알 수 없으므로 애플리케이션에서는 이러한 가변적인 속성을 처리할 수 있어야 한다.

이런점에서 MongoDB는 새로운 속성은 도큐먼트에 동적으로 처리할 수 있기 때문에 추후에 필요한 데이터필드가 무엇일지 고민할 필요가 없다.

고정된 스키마를 갖는 데이터베이스에서는 EAV(Entity-Attribute-Value) 패턴을 이용한다.

Entity-Attribute-Value model?

데이터베이스 설계 시 오브젝트의 자유도를 최대한 보장해야 하는 경우 고려 가능한 모델

잠재적인 어트리뷰트는 많지만 실제 사용되는 어트리뷰트 수는 적을 떄 사용되는 데이터 모델이다.

  • Entity는 테이블에서의 엔터티 객체를 나타낸다.
  • Attribute는 객체의 어트리뷰트(Field)이다.
  • Value는 객체의 어트리뷰트의 값이다.

애드혹 쿼리

**애드혹 쿼리(ad hoc query)**란 아래 예제처럼 미리 정의된 쿼리가아닌 아래와 같이 파라미터가 늘 변하게되는 변수로 작용하는 되는 쿼리이다. (**user_id**가 매번 변함)

query = "SELECT * FROM users WHERE id=" + user_id

RDBMS만 사용해 봤다면 애드혹 쿼리를 당연한 것으로 생각하기 쉽지만, 사실 모든 데이터베이스가 이런 동적 질의를 지원하는 것은 아니다.

다른 많은 시스템에서 간단하고 확장성 높은 모델을 위해 풍부한 쿼리 기능을 포기하는 경우가 많은데,

MongoDB의 설계 목표 중 하나는 관계형 데이터베이스상에서 매우 필수적은 쿼리 성능을 유지하는 것이다.

MongoDB에서 쿼리 언어는 아래와 같이 작동한다.

db.posts.find({'tags': 'politics', 'vote_count': {'$gt': 10}})

SQL 쿼리는 엄격하게 정규화 된 모델에 의존하는 반면, MongoDB의 쿼리는 태그가 각 도큐먼트에 포함되어 있다고 가정한다.

여러개의 속성 (tags 검색, vote_count 검색)을 조합하여 질의할 수 있는 능력을 보여주는데, 이것이 애드혹 쿼리의 본질적인 강점이다.

인덱스

MongoDB에서 인덱스는 B-Tree(B-Tree?) 로 구현되어 있다.

관계 데이터베이스에서 기본적으로 제공하는 기능인 인덱스는 범위를 스캔하거나 정렬을 하는 것과 같이 간단한 쿼리에 대해 최적화 되어 있다.

 

하지만 WiredTiger는 LSM tree(Log-Structed Merge-trees)를 지원한다고 한다.

B-Tree?

이진 트리가 자식 노드가 최대 2개인 노드를 말하는 것이라면 B-Tree는 자식 노드의 개수가 2개 이상인 트리를 말한다.

또한 노드내의 데이터가 1개 이상일 수가 있다. 만약 노드 내 최대 데이터 수가 2개라면 2차 B-Tree, 3개라면 3차 B-Tree라고 말한다.

그리고, 노드의 데이터 수가 N개라면 자식 노드의 개수는 N+1개 이다.

또한, 6보다 작은 값은 왼쪽 서브트리, 6과 17 사이의 값은 중간 서브트리, 17보다 큰 값은 오른쪽 서브트리에 들어가는 구조를 볼 수 있다.

탐색법은 작은 값은 왼쪽, 큰값은 오른쪽에 있기 때문에 root 노드부터 시작해 하향식으로 탐색해 나간다.

PostgreSQL에서는 동시성 제어를 위해 여러가지 모드의 lock을 제공합니다. 이런 lock에도 여러가지 종류가 있고, 명시적으로 사용되는 경우/묵시적으로 사용되는 경우가 있는데, 자세히 알아보도록 합시다.

시작하기 전 3줄요약

  1. Lock이 미치는 범위를 level로 나눈다
  2. Lock 모드별로 충돌하는 관계가 존재한다
  3. Lock은 트랜잭션 종료 시 혹은 롤백시에 풀린다.

lock이 미치는 범위를 Level로 나누는데, Table-Level Lock, Row-Level Lock, Page-Level Lock, Database-Level Lock 까지 다양하게 존재합니다.
해당 글에서는 Table-Level LockRow-Level Lock 에 대해서만 설명하겠습니다.

Table-Level Lock

Table Level Lock은 테이블 수준에 락을 거는 방법입니다. 만약 테이블 내에 100개의 로우가 있다고 하면 하나의 로우에 접근하는 동안 나머지 99개의 로우에 접근 할 수 없기 때문에, 다중 사용자 환경에서는 사용하지 않는 편이 좋습니다. *(보통 테이블 전체 로우의 변경이 있는 DDL 구문과 함께 사용됩니다. (ex) TRUNCATE, ALTER))
*

이런 Table Level Lock은 LOCK 명령어를 이용해서 명시적으로 걸어줄 수도 있지만,
우리가 특정 쿼리문을 사용할 때 마다 암묵적으로 걸리게 됩니다. 어느 상황에 어느 락이 걸리는 지는 해당 문서를 보면 알 수 있습니다.

Row-Level Lock

Row-Level Lock은 row 수준에 락을 거는 방법입니다.

SELECT ~ FOR SHARE과 같은 DML 구문과 함께 가장 자주 사용되는 Lock입니다.


만약 이런 Lock 종류가 하나밖에 없다면, 여러 종류의 트랜잭션에서 lock을 알맞게 사용하기 어려워지는데요,
이런 상황을 처리하기 위해 Lock에는 Lock모드라는 것이 존재합니다.

Lock 모드?

Lock 모드 별로 서로 충돌하는 Lock 모드가 있으며,
충돌한다면 해당 리소스(table, row)에 동시에 접근할 수 없게 됩니다.
ex)

  • ACCESS SHARE락은 ACCESS EXCLUSIVE 락과 충돌한다,
  • ROW SHARE락은 EXCLUSIVE, ACCESS EXCLUSIVE 락과 충돌한다

Table-Level Lock에는ACCESS SHARE, ROW SHARE 등,
Row-Level Lock에는 FOR UPDATE, FOR UPDATE 등 여러가지 Lock 모드가 존재합니다.
이런 모드들의 이름은 일반적으로 사용되는 경우를 나타내지만, 모드마다 기능적으로 다른 점은 없습니다.

각 모드마다 차이점은 오직 "어떤 모드의 lock과 충돌하는가" 입니다.
부연설명하자면 충돌하는 lock 모드의 종류가 다른 것이 차이점이라고 볼 수 있겠습니다.

이런 lock 모드별 충돌되는 경우는 아래의 표를 보고 확인할 수 있습니다.

Table-Level Lock Mode

Row-Level Lock Mode


Ref

Chapter 13. Concurrency Control

인덱스는 데이터베이스 테이블에 대한 검색 성능을 높혀주는 도구이다. 이런 인덱스를 구현하는 알고리즘에는 여러가지가 있는데, 각 상황에 맞게 사용된다.

먼저 인덱스가 무엇인지 알아보고, 어떤 기준으로 상황에 맞는 알고리즘을 고르면 될지 알아보도록 하자

그리고, 이 글은 꽤나 길기때문에 원하는 정보가 있다면 우측의 인덱스를 잘 활용하길 바란다 👉

🤔 인덱스란?

인덱스는 메모리 영역에 존재하며, 지정된 컬럼을 기준으로 생성된 목차를 의미한다. 아래 그림을 예시로 들어보자.

위 인덱스는 지정된 컬럼(company_id)을 기준으로 정렬되어 있고, pointer가 테이블의 row를 가리키고 있다.

만약 인덱스를 거치지 않고 company_id가 18인 값을 찾으러면 테이블은 company_id 로 정렬되어 있지 않기 때문에 모든 row를 탐색해야 할 것이다 (full scan)

이런 인덱스를 정렬하는 알고리즘은 B-tree, Hash, 비트맵 등 여러가지가 있는데, 알고리즘 마다 쓰이는 상황이 다르다. 자세히 알아보자!

B-tree

B-tree 알고리즘은 인덱스에서 가장 일반적으로 사용되는 알고리즘이다.
B tree의 구조는 아래와 같은데,


이 구조의 특징은

  • 하나의 노드가 여러개의 데이터를 가질 수 있으며
  • 하위 노드의 최대 갯수는 상위 노드 데이터 수 + 1 로 결정된다는 점이 있다.

이런 구조 덕분에 검색 속도의 균일성(logN)을 보장할 수 있는데, 그 이유는 다음과 같다.


트리가 만약 왼쪽과 같이 비균형상태라고 쳐보자,
그럼 6이라는 값을 찾기 위해서는 몇개의 노드를 탐색해야 할까? 루트 노드부터 순서대로 6번 탐색해야 할 것이다.

하지만 트리 구조가 오른쪽과 같이 균형이 잡혀있다면 최대 3번의 탐색 안에 무엇이든 원하는 값을 가지게 될 수 있을 것이다.

이런 B-tree 인덱스는 보통 데이터의 값의 종류가 많고 동일한 데이터적은 컬럼에 사용된다.

주의할 점은 OR 오퍼레이터를 사용하는 쿼리문에 비효율적이다. 이를 해결하기 위해 보통 IN 오퍼레이터를 사용한 쿼리문으로 튜닝하곤 한다.

Hash

Hash 인덱스는 범용적으로 쓰이는 인덱스는 아니지만 동등 비교 검색(=)에서 빠른 속도를 보여주는 인덱스이다.


동작 방식은 아래와 같다

  • 지정된 컬럼의 검색하고자 하는 값을 hash function에 입력
  • hash function 출력값으로 해시값을 bucket에서 찾아 데이터 레코드 주소를 알아냄
  • 알아낸 데이터 레코드 주소로 원하는 데이터 레코드에 접근

해시맵을 사용하기 때문에 (hash collision이 없는 경우) 시간 복잡도가 1이라는 장점이 있다.

시간 복잡도가 1이라면 Hash 인덱스가 최고 아닌가? 라는 의문이 들 수도 있다. 물론 동등 비교 검색에서는 빠른 성능을 보여주지만, 범위 검색(>, between) 에서는 N의 시간 복잡도를 가지게 때문에 특수한 상황에서만 쓰이는 것이다.

이런 Hash 인덱스는 아래의 상황에서 쓰인다.

  • 데이터 값의 종류가 많음 (hash collision 방지)
  • InnoDB Adaptive Hash Index(자주 사용되는 값만 해싱하여 버킷에 저장)비트맵비트맵 인덱스는 기존 B-Tree가 가지고 있던 단점을 보완하기 위해 등장한 인덱스다.
    B-Tree는 아래와 같은 단점을 가지고 있었다.
  • 실제 칼럼 값을 인덱스에 저장하고 있다보니 저장 공간 낭비가 심하다
  • NOT 이나 NULL을 사용하거나 복잡한 OR 조건에서 성능이 떨어짐

이런 단점을 비트맵 인덱스는 아래와 같이 해결했다.

  • 칼럼 값을 인덱스에 저장하는 것이 아닌 컴퓨터의 가장 작은 단위인 비트로 값을 저장해 저장 공간 절약
  • bitwise(|, &, ^) 연산으로 복잡한 OR 연산이 빠름


비트맵 탐색을 예시를 들어보자면 아래 조건에 만족하는 row를 찾고 싶다고 가정해보자

  • 여성
  • 미혼
  • 18 - 34세

위 조건과 같을 때 비트맵 값이 101000인 컬럼을 찾으면 되는 것이다.

이런 비트맵 인덱스는 아래의 상황에서 쓰인다.

  • 데이터 값의 종류가 적음(성별, Enum)
  • 쿼리가 OR 연산자를 포함하는 여러개의 WHERE 조건을 가질 때

하지만 비트맵 인덱스는 레코드 하나만 변경되더라도 모든 레코드에 lock이 걸린다는 점 때문에 다른 인덱스 알고리즘에 비해 데이터 갱신비용이 크다는 단점이 있다.
또한, 하나의 비트맵 인덱스만으로는 별로 효과가 없고, 여러 비트맵 인덱스를 동시에 사용할 때 성능 향상에 도움을 준다.

full text search

클러스터링

Ref.

https://jojoldu.tistory.com/243
http://www.gurubee.net/lecture/1109

+ Recent posts