테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

분석·검색: ClickHouse·DuckDB·Elasticsearch

많은 행을 훑어 집계하는 컬럼형 분석 엔진과, 글에서 낱말을 찾는 역색인 검색 엔진이다.

목차 (9)
  1. OLTP와 OLAP: 행으로 둘까, 열로 둘까
  2. 벡터화 실행
  3. ClickHouse: MergeTree와 희소 인덱스
  4. DuckDB: 앱 안에서 도는 분석 엔진
  5. 검색 엔진: 역색인과 분석기
  6. 세그먼트, refresh, merge
  7. 샤드, 레플리카, BM25
  8. Elasticsearch와 OpenSearch
  9. 고르는 법

OLTP와 OLAP: 행으로 둘까, 열로 둘까

쇼핑몰의 주문 처리는 "주문 하나를 넣고, 주문 하나를 꺼낸다"를 초당 수천 번 한다. 이런 일을 OLTP(온라인 트랜잭션 처리)라 하고, 행 하나의 모든 열을 한곳에 두는 행 저장이 맞는다. 반대로 "지난 1년 지역별 매출 합계"는 수억 행을 훑지만 열은 두세 개만 본다. 이런 일이 OLAP(온라인 분석 처리)이고, 열마다 값을 따로 모아 두는 열 저장(columnar)이 맞는다.

행 저장과 열 저장 비교: 같은 표 네 행을 행 저장은 한 행의 id·날짜·지역·금액을 이어 붙여 디스크에 두고, 열 저장은 열마다 값을 이어 붙여 둔다. 금액의 합을 구할 때 행 저장은 모든 블록을 읽어야 하지만 열 저장은 금액 열만 읽는다SELECT sum(금액) FROM 주문행 저장 (OLTP)13/1서울12023/1부산8033/2서울20043/2대구50한 블록 = 한 행. 금액만 필요해도모든 열을 함께 읽는다열 저장 (OLAP)id1234날짜3/13/13/23/2지역서울부산서울대구금액1208020050한 파일(블록) = 한 열금액 열만 읽는다같은 종류 값이 모여 압축도 잘 된다행 하나를 통째로 넣고 꺼내는 일은 반대로 행 저장이 유리하다
같은 표, 다른 배치. 집계 쿼리는 필요한 열만 읽는 열 저장에서 디스크 읽기가 열 개수에 비례해 줄어든다.

열 저장의 이득은 읽는 양만이 아니다. 한 열에는 같은 종류의 값이 이어져 있어 압축이 아주 잘 된다. 날짜처럼 정렬된 값은 차이만, 지역처럼 종류가 적은 값은 사전 번호만 적으면 된다(델타·사전·런 길이 인코딩). 대가는 행 하나를 넣고 고치는 일이 비싸다는 것이다. 한 행이 열 개수만큼의 파일에 흩어져 있기 때문이다. 그래서 분석 엔진은 행을 하나씩이 아니라 수천·수만 행씩 묶어 넣기를 기대하고, 고치기와 지우기는 드물게 일어난다고 가정한다.

벡터화 실행

전통적인 실행기는 연산자마다 "다음 행을 줘"(next) 함수를 행 하나에 한 번씩 부른다(Volcano 모델). 행이 수십억 개면 함수 호출과 분기, 행마다 다른 자료형을 확인하는 비용이 실제 계산보다 커진다. MonetDB/X100 논문(Boncz·Zukowski·Nes, CIDR 2005)이 제안한 벡터화 실행은 한 번에 한 열의 값 수천 개 묶음(벡터)을 넘긴다.

한 행씩 처리와 벡터화 실행 비교: 왼쪽은 읽기·거르기·합계 연산자가 행 하나씩 주고받아 행마다 함수 호출이 생긴다. 오른쪽은 열 값 2048개 묶음을 한 번에 넘기고, 연산자 안의 짧은 반복문이 묶음 전체를 CPU 캐시 안에서 처리한다한 행씩 (튜플 단위)벡터 단위합계 sum(금액)합계 sum(금액)거르기 지역 = 서울거르기 지역 = 서울읽기 (scan)읽기 (scan)행 1개행 1개행마다 next() 호출 · 분기 · 캐시 미스묶음마다 한 번 호출, 안쪽은 짧은 반복문값 2048개
같은 쿼리 계획, 다른 넘김 단위. 묶음으로 넘기면 호출 비용은 묶음 크기만큼 나뉘고, 연산자 안의 짧은 반복문은 CPU 캐시와 SIMD 명령을 그대로 탄다.

묶음 크기는 CPU 캐시에 들어갈 만큼 작고 호출 비용이 묻힐 만큼 크게 고른다. DuckDB는 기본 2048개, ClickHouse는 수만 행짜리 블록을 쓴다. 연산자 안은 "배열의 i번째 값끼리 더한다" 같은 단순한 반복문이 되어 컴파일러가 SIMD로 바꾸기 좋다. 쿼리마다 기계어를 만들어 내는 컴파일 방식도 있지만, 오늘날 열 저장 분석 엔진 대부분은 벡터화를 기본으로 한다.

ClickHouse: MergeTree와 희소 인덱스

ClickHouse는 Yandex가 웹 분석 서비스를 위해 만들어 2016년에 Apache 2.0으로 공개한 열 저장 분석 데이터베이스다. 설계는 VLDB 2024 논문과 구조 개요 문서에 정리되어 있다. 중심은 MergeTree 테이블 엔진이다.

ClickHouse MergeTree 단계 그림: INSERT마다 part가 생기고 배경 병합이 part를 합친다. part 안에서 열 파일은 granule 단위로 나뉘고, 희소 기본 인덱스가 granule마다 첫 키를 갖는다. user_id 42를 찾을 때 이진 탐색으로 granule 하나만 골라 읽는다테이블의 partpart 1part 2part 3 (방금)← INSERT 하나part 1_3 (병합됨)옛 part 셋은 삭제part 하나의 안primary.idxgranule마다 키 하나1g09g123g240g357g480g5user_id.bin8192행8192행8192행8192행8192행8192행ts.bin8192행8192행8192행8192행8192행8192행url.bin8192행8192행8192행8192행8192행8192행키 6개로 약 5만 행을 가리킨다이진 탐색: 40 ≤ 42 < 57 → g3mark
1 / 4

INSERT 묶음 하나가 곧 part 하나다. 행을 정렬 키(ORDER BY) 순서로 정렬하고 열마다 따로 압축해 새 디렉터리로 쓴다. 기존 part는 건드리지 않는다.

MergeTree는 쓰기는 덧붙이기(part), 정리는 배경 병합에 맡기고, 읽기는 희소 인덱스로 건너뛸 수 있는 granule을 최대한 건너뛴다.

테이블을 만들 때 정하는 ORDER BY가 곧 정렬 키이자 기본 키다. 관계형의 기본 키와 달리 유일성을 강제하지 않고, 데이터를 그 순서로 정렬해 두는 역할만 한다. 희소 기본 인덱스 가이드의 설명대로 인덱스는 행마다가 아니라 granule(기본 8192행)마다 키 하나만 갖는다. 수십억 행도 인덱스가 메모리에 들어가는 대신, 한 행을 콕 집어 찾지는 못하고 granule 단위로 읽는다. 그래서 정렬 키의 첫 열은 가장 자주 거르는 조건이어야 하고, 값의 종류가 적은 열을 앞에 두면 정렬된 덩어리가 길어져 압축과 건너뛰기가 모두 좋아진다.

병합(merge)은 단순한 정리를 넘어 기능이 된다. ReplacingMergeTree는 병합할 때 같은 키의 옛 행을 버리고, SummingMergeTree·AggregatingMergeTree는 같은 키의 행을 미리 합쳐 둔다. 다만 병합은 언제 끝날지 모르므로 쿼리 결과가 늘 합쳐진 상태라고 가정하면 안 된다. 복제는 ReplicatedMergeTree가 맡는다. 서버마다 새 part를 만들었다는 기록을 ClickHouse Keeper(Raft 기반 조정 서비스)에 남기면, 다른 복제본이 그 part를 받아 온다.

DuckDB: 앱 안에서 도는 분석 엔진

DuckDB는 분석용 SQLite를 목표로 한다. 공식 소개대로 서버 없이 앱 프로세스 안에서 돌고(in-process), 열 저장과 벡터화 실행을 쓰며, MIT 라이선스다. Python·R·Java·브라우저(WebAssembly)에서 라이브러리처럼 불러 쓴다. 처음 설계는 SIGMOD 2019 데모 논문에 있다.

클라이언트·서버와 in-process 비교: 왼쪽은 앱이 네트워크로 분석 서버에 쿼리를 보내고 결과를 직렬화해 돌려받는다. 오른쪽은 DuckDB가 앱 프로세스 안의 라이브러리로 돌아 Parquet·CSV 파일이나 자기 DB 파일을 직접 읽고, 결과를 앱의 데이터프레임으로 바로 넘긴다클라이언트·서버in-process (DuckDB)앱 (Python 등)분석 서버별도 프로세스네트워크 + 결과 직렬화서버의 저장소설치·운영할 서버가 있고여러 사용자가 함께 쓴다앱 프로세스 하나앱 코드데이터프레임DuckDB라이브러리ParquetCSV.duckdb파일을 직접 읽고, 결과는 메모리로 바로 넘긴다
서버형 분석 DB는 결과를 네트워크로 직렬화해 보내는 비용이 크다. DuckDB는 같은 프로세스 안에서 돌아 파일을 직접 읽고 결과를 메모리로 넘긴다.

자기 DB 파일(테이블을 행 묶음 단위로 나눠 열마다 압축해 두는 단일 파일)도 있지만, Parquet·CSV·JSON 파일을 적재하지 않고 바로 쿼리하는 쓰임이 많다. 노트북에서 수 GB짜리 Parquet를 판다스보다 빠르게 집계하거나, 데이터 파이프라인의 한 단계로 끼워 넣는 식이다. 트랜잭션은 MVCC로 지원하지만 쓰는 프로세스는 하나라는 점에서 SQLite와 닮았다. 여러 사람이 동시에 쓰는 공용 분석 서버가 필요하면 ClickHouse 같은 서버형을, 한 사람·한 프로세스의 분석이면 DuckDB를 고른다.

검색 엔진: 역색인과 분석기

"검색 엔진 빠른"으로 글을 찾는 일은 관계형의 LIKE '%검색%'으로는 안 된다. 모든 행을 훑어야 하고, "engines"와 "engine"을 같은 낱말로 보지도, 얼마나 잘 맞는지 순위를 매기지도 못한다. Elasticsearch와 OpenSearch는 이 일을 Apache Lucene 라이브러리에 맡긴다. Lucene의 중심은 역색인(inverted index)이다. 문서 → 낱말이 아니라 낱말 → 문서 목록으로 뒤집어 저장한다.

역색인 만들기: 문서 셋을 분석기가 소문자로 바꾸고 낱말로 쪼갠 뒤 the·is 같은 불용어를 빼고 engines·engine을 어간 engin으로 줄인다. 그 결과 낱말 사전은 engin이 문서 1과 3, fast가 1과 2, search가 1과 2, speed가 3에 나온다고 적는다. 검색어도 같은 분석기를 거쳐 사전에서 찾는다문서1Fast search engines2The search is fast3Engine speed분석기① 낱말로 쪼개기② 소문자로③ 불용어 빼기the, is④ 어간 추출engines → engin낱말 사전 → 포스팅engin문서 1, 3fast문서 1, 2search문서 1, 2speed문서 3낱말은 정렬돼 있고 포스팅에는빈도·위치도 함께 적는다검색어 "fast engine"도 같은 분석기를 거쳐 fast, engin → 문서 1, 2, 3문서를 훑지 않고 낱말에서 문서로 바로 간다 — 두 낱말이 다 있는 문서 1이 점수가 가장 높다
역색인. 문서와 검색어를 같은 분석기에 통과시켜 같은 낱말 형태로 맞춘 뒤, 낱말 사전에서 포스팅 목록을 바로 꺼낸다.

글을 낱말로 바꾸는 것이 분석기(analyzer)다. 글자 필터, 토크나이저(낱말로 쪼개기), 토큰 필터(소문자화, 불용어 제거, 어간 추출, 동의어)를 차례로 거친다. 언어마다 알맞은 분석기가 다르고, 한국어는 조사가 붙어 띄어쓰기만으로 쪼개면 "검색엔진이"와 "검색엔진을"이 다른 낱말이 되므로 형태소 분석기(Elasticsearch의 nori 등)를 쓴다. 정확히 같은 값으로만 거를 필드(상태 코드, 태그)는 분석하지 않는 keyword 필드로 둔다.

세그먼트, refresh, merge

Lucene 인덱스는 불변 세그먼트의 묶음이다. 세그먼트 하나가 그 자체로 작은 역색인이고, 검색은 모든 세그먼트에 묻고 결과를 합친다. 정렬된 사전과 압축된 포스팅을 고치지 않고 새로 쓰기만 하므로, 구조가 LSM 트리와 닮았다.

Lucene 세그먼트 단계 그림: 색인한 문서는 메모리 버퍼와 translog에 들어가고, refresh가 새 세그먼트를 만들어 검색되게 하며, 고친 문서는 옛 세그먼트에 삭제 표시가 붙는다. flush가 세그먼트를 디스크에 확정하고 translog를 비우며, merge가 세그먼트를 합쳐 삭제된 문서를 없앤다메모리 · 로그인덱싱 버퍼문서 1건비었음translog (디스크)기록 있음비움세그먼트 (검색 대상)세그먼트 ABCD삭제 표시합쳐진 세그먼트 (A+B+C+D, 삭제분 제외)refresh (1초마다) → 검색됨고친 문서: 새 판은 D, B의 옛 판은 삭제 표시디스크에 확정(커밋)된 세그먼트:A, BA, B, C, D (fsync)합친 세그먼트flush: 커밋하고 translog를 비운다merge: 작은 세그먼트를 합치고 삭제 표시된 문서를 버린다
1 / 5

문서를 색인하면 메모리의 인덱싱 버퍼에 들어가고, 같은 요청이 translog에도 적힌다(기본은 요청마다 fsync). 응답은 여기서 나간다. 아직 검색에는 안 잡힌다.

Lucene 인덱스는 불변 세그먼트의 묶음이다. 쓰기는 세그먼트를 새로 만들고, 지우기는 표시만 하며, 정리는 merge가 한다. LSM 트리와 닮은 구조다.

새 문서가 검색되기까지 걸리는 시간은 refresh 주기다. near-real-time 검색 문서대로 기본은 1초마다지만, 최근 30초 동안 검색이 없었던 인덱스는 건너뛴다. 대량 적재 중에는 refresh를 늦추거나 끄면 작은 세그먼트가 덜 생겨 훨씬 빨라진다. 커밋 사이의 내구성은 translog가 지킨다. 기본 설정은 요청마다 translog를 fsync하고 나서 응답한다.

샤드, 레플리카, BM25

Elasticsearch의 인덱스는 샤드로 나뉜다. 샤드 하나가 Lucene 인덱스 하나이고, 문서는 _id(또는 routing 값)의 해시로 primary 샤드 하나에 들어간다. primary 수는 문서가 어느 샤드에 갈지를 정하므로 만든 뒤 바꾸려면 다시 색인(또는 split·shrink)해야 하고, 레플리카 수는 언제든 바꾼다. 쓰기는 primary가 받아 레플리카로 전달하고, 검색은 primary와 레플리카 어느 쪽이든 받는다.

Elasticsearch 샤드 배치와 검색: 인덱스 하나를 primary 샤드 P0·P1·P2로 나누고, 각 샤드의 복제본 R0·R1·R2를 primary와 다른 노드에 둔다. 검색 요청을 받은 조정 노드는 샤드마다 사본 하나(여기서는 P0, P1, R1)에 물어 상위 결과의 id와 점수를 받고, 합쳐서 최종 상위 문서만 다시 가져온다조정 노드 (검색 요청)노드 1P0R2노드 2P1R0노드 3P2R1① query: 샤드마다 상위 10개의 id와 점수만 돌려준다② 합쳐 전체 상위 10개를 고르고 ③ fetch: 그 문서만 가져온다P = primary, R = 복제본. 같은 번호의 P와 R은 늘 다른 노드에 있다
검색 하나가 샤드마다 사본 하나씩에 퍼졌다가 합쳐진다. 샤드가 너무 많으면 이 퍼지고 합치는 비용이 커진다.

순위는 점수가 정한다. 기본 유사도는 BM25다. 문서에 검색 낱말이 자주 나올수록(TF) 점수가 오르지만 일정 횟수 뒤로는 거의 늘지 않고, 흔한 낱말보다 드문 낱말이 맞을 때(IDF) 더 크게 오르며, 긴 문서는 우연히 낱말이 나올 가능성이 크므로 길이로 나눠 깎는다. 공식의 근거는 Robertson·Zaragoza의 정리 논문에 있다. IDF는 기본으로 샤드마다 따로 세므로, 문서가 적은 인덱스에서는 샤드에 따라 점수가 조금씩 달라질 수 있다. 의미가 가까운 글을 찾는 벡터 검색은 시계열·그래프·벡터 노트에서 다룬다.

Elasticsearch와 OpenSearch

Elasticsearch는 처음에 Apache 2.0이었다. 2021년 1월 Elastic은 7.11부터 SSPL과 Elastic License 2.0으로 바꾼다고 발표했고, 같은 해 4월 AWS가 마지막 Apache 판인 7.10.2에서 갈라진 OpenSearch를 발표했다.

Elasticsearch 라이선스 변화: 처음에는 Apache 2.0이었고, 2021년 1월 Elastic이 7.11부터 SSPL과 Elastic License 2.0으로 바꾼다고 발표했다. 2021년 4월 AWS가 마지막 Apache 판인 7.10.2에서 갈라진 OpenSearch를 발표했다. 2024년 8월 Elastic은 AGPLv3를 선택지로 더했고, 2024년 9월 OpenSearch는 리눅스 재단 아래 OpenSearch Software Foundation으로 옮겼다ElasticsearchOpenSearch마지막 Apache 판2010Apache 2.0처음 공개2021.1SSPL / ELv27.11부터2024.8+ AGPLv3셋 중 고름2021.4OpenSearch7.10.2에서, Apache 2.02024.9LF 재단으로OpenSearch 재단바탕의 검색 라이브러리 Lucene은 처음부터 지금까지 Apache 2.0이다
Elasticsearch와 OpenSearch의 갈림길. 둘 다 Lucene 위에 서 있고, 2021년 이후 기능은 따로 발전한다.

2024년 8월 29일 Elastic은 AGPLv3를 세 번째 선택지로 더했다. 기존 SSPL·ELv2를 없앤 것이 아니라 하나를 더한 것이다. 같은 해 9월 AWS는 OpenSearch를 리눅스 재단 아래 OpenSearch Software Foundation으로 옮겼고, OpenSearch는 Apache 2.0을 유지한다. 두 제품은 API의 뿌리가 같지만 갈라진 뒤 생긴 기능과 클라이언트는 서로 호환되지 않을 수 있다. (2026년 10월 기준)

고르는 법

분석·검색 엔진 고르기: 글에서 낱말로 찾고 순위를 매기나? 예면 Elasticsearch 계열. 데이터가 한 기계, 한 프로세스에서 분석되나? 예면 DuckDB. 수십억 행을 여러 사람이 실시간으로 집계하나? 예면 ClickHouse. 모두 아니면 운영 DB의 집계로 충분하다글에서 낱말로 찾고 순위를 매기나?예Elasticsearch 계열아니오데이터가 한 기계, 한 프로세스에서 분석되나?예DuckDB아니오수십억 행을 여러 사람이 실시간으로 집계하나?예ClickHouse아니오운영 DB의 집계로 충분하다
질문이 검색인지 집계인지, 집계라면 얼마나 크고 몇 사람이 쓰는지로 고른다.
기준ClickHouseDuckDBElasticsearch / OpenSearch
형태분산 서버앱에 넣는 라이브러리분산 서버
저장열 저장 part, 희소 인덱스열 저장 단일 파일, 외부 파일 직접 읽기Lucene 세그먼트(역색인 + 열 값)
잘하는 질문수십억 행 집계, 실시간 대시보드, 로그·이벤트 분석한 사람의 데이터 분석, 파이프라인 안의 변환전문 검색과 순위, 자동 완성, 로그 검색
못하는 것잦은 단건 수정·삭제, 트랜잭션여러 프로세스의 동시 쓰기정확한 트랜잭션, 큰 조인, 주 저장소 역할
라이선스Apache 2.0MITAGPLv3·SSPL·ELv2 / Apache 2.0

먼저 질문이 "얼마나"(집계)인지 "어느 것"(검색과 순위)인지 가린다. 집계라면 데이터가 한 사람의 노트북에 들어오는지에 따라 DuckDB와 ClickHouse가 갈린다. 검색이라면 Elasticsearch·OpenSearch를 쓰되, 원본은 관계형이나 문서 데이터베이스에 두고 검색 엔진은 언제든 다시 색인할 수 있는 사본으로 다루는 것이 안전하다. 분석 엔진에 넣을 데이터를 OLTP 데이터베이스에서 옮겨 오는 일은 클라우드 데이터베이스 노트의 데이터 웨어하우스와 이어진다.