테마 선택

📚 지식 노트

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

시계열·그래프·벡터

시간 순 지표, 관계 그 자체, 임베딩의 가까움처럼 특정한 질문에 맞춘 데이터베이스들이다.

목차 (7)
  1. 시계열: Prometheus TSDB
  2. 시계열 압축과 TimescaleDB·InfluxDB
  3. 그래프: Neo4j와 인덱스 없는 인접
  4. 벡터: 임베딩과 IVF
  5. 벡터: HNSW 그래프 탐색
  6. 벡터 DB: pgvector, Qdrant, Milvus
  7. 고르는 법

시계열: Prometheus TSDB

서버의 CPU 사용률, 센서 온도, 주가처럼 시계열 데이터는 성질이 뚜렷하다. 거의 언제나 지금 시각의 값이 끝에 덧붙고, 지난 값은 고치지 않으며, 최근 값을 많이 읽고, 오래된 값은 통째로 버린다. 그리고 "어느 서버의, 어느 지표"를 나타내는 이름표(label)와 시각·값 쌍이 끝없이 이어진다. 시계열 데이터베이스는 이 성질을 그대로 저장 구조에 새긴다.

Prometheus는 지표를 모으는 방식부터 독특하다. 공식 개요대로 대상이 데이터를 보내는(push) 대신, Prometheus가 정한 간격마다 대상의 HTTP 주소를 긁어 간다(pull, scrape). 대상은 지금 값만 내놓으면 되고, Prometheus는 누가 응답하지 않는지 바로 안다. 받은 샘플은 내장 로컬 TSDB에 쌓인다.

Prometheus TSDB 단계 그림: 대상의 /metrics를 가져오고, 샘플을 WAL과 메모리 head block의 chunk에 넣으며, 120개가 찬 chunk는 디스크로 내리고, 2시간 분량은 불변 block으로 쓰고, compaction이 block을 합치고 보존 기간이 지난 block을 지운다대상/metrics/metrics/metricsHTTP로 가져감head block (메모리)시계열 하나의 chunk+ 샘플새 chunkmmap된 지난 chunk들120샘플 = chunk 하나WAL (디스크)디스크의 block (불변)2시간2시간2시간가장 오래된 2시간을 떼어 새 block으로합쳐진 긴 block보존 기간 지난 block: 통째로 삭제block 하나 = 디렉터리 하나chunks/ · index · meta.json
1 / 5

스크레이프: Prometheus가 정한 간격마다 대상의 /metrics 주소를 HTTP로 가져온다(pull). 대상은 지금 값만 내놓으면 되고, 응답이 없으면 그 자체가 "죽었다"는 신호(up = 0)가 된다.

Prometheus의 로컬 저장소. 새 샘플은 메모리와 WAL에, 지난 시간은 불변 block 디렉터리에 둔다. 시간으로 나뉘어 있어 오래된 데이터 지우기가 디렉터리 삭제 한 번이다.

시간으로 나눈 block은 시계열에 꼭 맞는다. 쿼리는 시간 범위에 걸치는 block만 열고, 보존 기간이 지나면 디렉터리 하나를 지우면 끝이다. 레코드마다 지우는 관계형과 비교하면 거의 공짜다. 대신 Prometheus 한 대의 로컬 저장소는 복제가 없다. 오래 보관하거나 여러 대를 모아 보려면 원격 쓰기(remote write)로 Thanos·Mimir 같은 장기 저장소에 보낸다. 조심할 것은 카디널리티다. 이름표 값 조합마다 별도 시계열이 생기므로, 사용자 ID처럼 값이 끝없이 많은 이름표를 달면 시계열 수가 폭발해 메모리가 바닥난다.

시계열 압축과 TimescaleDB·InfluxDB

시계열 샘플은 시각 8바이트와 값 8바이트, 그대로면 16바이트다. Facebook의 Gorilla 논문(VLDB 2015)은 이것을 평균 2바이트 아래로 줄이는 방법을 보였고, Prometheus의 chunk도 같은 생각을 쓴다. 시각은 간격이 거의 일정하므로 "간격의 차이"(delta-of-delta)를 적으면 대부분 0이고, 값은 천천히 변하므로 앞 값과 XOR하면 대부분의 비트가 0이다.

시계열 압축: 시각 1000, 1015, 1030, 1045, 1061의 간격은 15, 15, 15, 16이고 간격의 차이는 0, 0, 1이라 대부분 1비트로 적는다. 값 12.5가 이어지면 앞 값과 XOR이 0이라 1비트, 12.75로 바뀌면 달라진 비트만 적는다시각간격간격의 차이적는 비트100010151030104510611515151600 (1비트)00 (1비트)110+7비트값앞 값과 XOR12.512.50 (1비트)12.50 (1비트)12.75XOR 일부12.750 (1비트)규칙적인 간격, 천천히 변하는 값 → 샘플 하나에 평균 1~2바이트처음 시각과 값만 그대로 64비트로 적는다
Gorilla 방식 압축. 규칙적인 시각과 천천히 변하는 값은 샘플 대부분이 비트 몇 개로 줄어든다.

SQL과 조인이 필요하면 PostgreSQL 위의 시계열이 있다. TimescaleDB는 PostgreSQL 확장이다. 하이퍼테이블을 만들면 겉으로는 표 하나지만, 안에서는 시간 구간마다 chunk라는 일반 표로 자동으로 나뉜다. 쿼리 계획은 시간 조건에 걸리지 않는 chunk를 아예 열지 않고, 오래된 chunk는 지우기도 표 하나 버리기다. 식으면 열 저장(columnstore)으로 바꿔 크게 압축하고 집계를 벡터화한다. 개발사 Timescale은 2025년 6월 회사 이름을 Tiger Data로 바꿨고 확장 이름은 그대로다.

TimescaleDB 하이퍼테이블: 앱은 metrics라는 PostgreSQL 표 하나에 쓰고 묻지만, 안에서는 일주일마다 chunk라는 일반 표로 나뉜다. 가장 최근 chunk는 행 저장으로 쓰기를 받고, 지난 chunk는 열 저장으로 압축되며, 보존 기간이 지난 chunk는 통째로 지운다. 최근 하루를 묻는 쿼리는 마지막 chunk만 본다하이퍼테이블 metrics (보이는 표)chunk9/24–9/30보존 기간 지남chunk10/1–10/7열 저장 압축chunk10/8–10/14열 저장 압축chunk10/15–10/21열 저장 압축chunk10/22–10/28행 저장INSERT는 지금 시각의 chunk로 간다WHERE time > now() − 1일 → 마지막 chunk만 연다 (chunk 제외)chunk는 그냥 PostgreSQL 표다. 인덱스·조인·확장이 그대로 된다
하이퍼테이블은 PostgreSQL 표 하나처럼 보이지만, 안에서는 시간 구간마다 표(chunk)로 나뉘어 쓰는 chunk, 압축된 chunk, 지울 chunk가 따로 관리된다.

InfluxDB는 시계열 전용으로 처음부터 만든 데이터베이스다. 3.0에서 엔진을 Rust로 다시 쓰고 Apache Arrow(메모리 열 형식), DataFusion(쿼리 엔진), Parquet(파일 형식)를 바탕에 깔았다. 오픈 소스판인 InfluxDB 3 Core는 SQL과 InfluxQL로 묻고, 데이터를 로컬 디스크나 오브젝트 스토리지의 Parquet 파일로 둔다. 열 저장 분석의 원리는 분석·검색 노트와 같다.

그래프: Neo4j와 인덱스 없는 인접

"내 친구의 친구 중 같은 회사에 다니는 사람", "이 계좌에서 돈이 세 번 건너 흘러간 곳"처럼 질문이 관계를 여러 번 건너뛰면 그래프 데이터베이스가 빛난다. Neo4j는 속성 그래프(property graph) 모델을 쓴다. 공식 입문 문서의 정의대로 노드(사람, 회사)에는 레이블과 속성이, 관계에는 하나의 타입(KNOWS, WORKS_AT)과 방향과 속성이 붙는다.

조인과 인접 포인터 비교: 왼쪽 관계형은 사람 표와 아는 사이 표를 두고, 친구의 친구를 찾을 때마다 아는 사이 표의 B-트리 인덱스를 내려가 다음 사람을 찾는다. 오른쪽 그래프 데이터베이스는 Ann, Bo, Cy, Di 노드가 KNOWS 관계로 직접 이어져 있어 한 걸음이 포인터 하나를 따라가는 일이다관계형: 걸음마다 조인그래프: 이웃을 직접 가리킴people1 · Ann2 · Bo3 · Cyknows (from, to)1 → 22 → 33 → 4B-트리한 걸음 = 인덱스 탐색 (log n)걸음이 늘면 조인이 겹겹이 쌓인다AnnBoCyDi:KNOWS:KNOWS:KNOWS{name: "Ann"}한 걸음 = 포인터 하나 (전체 크기와 무관)노드와 관계 모두 속성을 가진다
같은 '친구의 친구'를 찾는 두 방식. 관계형은 걸음마다 관계 표의 인덱스를 찾고, 그래프 데이터베이스는 노드에 붙은 관계를 바로 따라간다.

핵심 구현 아이디어는 인덱스 없는 인접(index-free adjacency)이다. 노드가 자기 관계들을 직접 가리키므로, 한 걸음 건너는 비용이 전체 데이터 크기가 아니라 그 노드의 이웃 수에 비례한다. 관계형은 걸음마다 인덱스를 내려가는 O(log n) 조인이 쌓이고, 걸음 수가 정해지지 않은 경로 탐색은 재귀 CTE로 써야 한다. Neo4j의 저장 형식도 이 생각을 따른다. 전통적인 형식은 노드와 관계를 고정 크기 레코드로 두고 관계끼리 연결 리스트처럼 잇고, 새 block 형식은 노드와 그 관계·속성을 한 블록에 모아 포인터 따라가기를 줄이며, 관계가 아주 많은 노드는 B+트리로 따로 둔다.

쿼리 언어는 Cypher다. 그림 그리듯 패턴을 적는다. MATCH (a:Person {name: 'Ann'})-[:KNOWS*2]->(f) RETURN f는 Ann에서 KNOWS를 두 번 건넌 사람을 찾는다. Cypher를 바탕으로 한 그래프 쿼리 표준 GQL이 2024년 ISO 표준이 되었고, Neo4j는 GQL 준수 현황을 따로 정리한다. 그래프가 늘 이기는 것은 아니다. 전체 행을 훑는 집계나 관계가 한두 단계뿐인 질문은 관계형 조인이 충분히 빠르고, 그래프를 여러 기계에 나누면 걸음마다 네트워크를 건너게 되어 강점이 줄어든다.

벡터: 임베딩과 IVF

글, 이미지, 상품을 신경망에 넣으면 수백~수천 개 숫자로 된 벡터, 즉 임베딩이 나온다. 뜻이 비슷한 것끼리 벡터 공간에서 가깝게 놓이도록 학습된 것이다(임베딩 자체는 AI 노트에서 다룬다). "이 질문과 뜻이 가까운 문서"는 곧 "이 벡터와 거리가 가까운 벡터 k개"(k-최근접 이웃)가 되고, 거리로는 코사인 거리, 내적, 유클리드 거리를 쓴다.

정확히 찾으려면 모든 벡터와 거리를 재야 한다. 벡터 1억 개 × 1536차원이면 질문 하나에 곱셈이 천억 번을 넘는다. 그래서 조금 틀려도 되는 근사 최근접 이웃(ANN) 인덱스를 쓴다. 정확도는 재현율(recall), 즉 진짜 최근접 k개 중 몇 개를 찾았는지로 잰다. 가장 단순한 것이 IVF(inverted file)다. 검색 엔진의 역색인처럼, 벡터를 k-means 중심별 목록으로 나눠 두고 가까운 목록만 훑는다.

IVF 인덱스: 점들이 중심이 표시된 네 목록으로 나뉘어 있다. 질의 점과 중심이 가까운 위쪽 두 목록만 훑는다(nprobe = 2). 질의 바로 아래, 탐색하지 않은 목록에 있는 점이 실제로는 가장 가까운 이웃이라 놓친다① 만들 때k-means로 중심(×) 몇 개를 정하고벡터마다 가장 가까운 목록에 넣는다② 찾을 때 (nprobe = 2)질의(+)와 가까운 중심 2개의 목록만하나하나 거리를 잰다(색칠한 칸)③ 놓치는 이웃경계 너머 목록의 점(빨강)은 가장가까워도 안 본다 → nprobe를 늘리면재현율이 오르고 대신 느려진다
IVF는 공간을 목록으로 나눠 일부만 훑는다. 목록 경계 바로 너머의 이웃은 놓칠 수 있고, 몇 목록을 볼지(nprobe)가 속도와 재현율을 맞바꾼다.

IVF는 만들기가 빠르고 메모리를 적게 쓰지만, 데이터가 바뀌면 중심이 낡아 다시 학습해야 한다. 메모리를 더 줄일 때는 벡터를 작은 코드로 압축하는 곱 양자화(PQ)를 함께 쓴다. 이런 기법들은 Meta의 라이브러리 Faiss에 모여 있고, 설계 공간 전체가 Faiss 논문에 정리되어 있다.

벡터: HNSW 그래프 탐색

지금 가장 널리 쓰는 ANN 인덱스는 HNSW(Hierarchical Navigable Small World, Malkov·Yashunin)다. 벡터를 노드로, 가까운 벡터끼리를 간선으로 잇는 그래프를 여러 층으로 쌓는다. 맨 아래층에는 모든 점이 있고, 위층으로 갈수록 무작위로 뽑힌 일부만 남아 간선이 길어진다. 고속도로에서 국도, 골목으로 내려가는 지도와 같다.

HNSW 단계 그림: 점 12개가 세 층에 놓여 있고, 위층일수록 점이 적다. 질의 점(빨간 +)을 찾아 맨 위층의 입구에서 출발해 층마다 더 가까운 이웃으로 옮기고, 더 가까운 이웃이 없으면 같은 점에서 한 층 내려간다. 맨 아래층에서 멈춘 점이 답이다0층012345678910111층023572층07077551010지금잰 이웃질의
1 / 7

맨 위층(2층)의 입구 점 0번에서 출발한다. 위층은 점이 적고 간선이 길어 멀리 빨리 간다.

HNSW 탐색(Malkov·Yashunin). 위층은 점이 드물고 간선이 길어 먼 거리를 몇 걸음에 건너고, 아래층으로 갈수록 촘촘해져 가까운 곳을 다듬는다. 그림은 탐색 폭 ef = 1로 줄였다.

실제 탐색은 맨 아래층에서 가장 가까운 하나만 따라가지 않고 후보 ef개를 함께 들고 넓게 본다. 매개변수는 셋이다. M은 점마다 잇는 이웃 수(클수록 정확하지만 메모리가 늘고), ef_construction은 만들 때 살펴보는 후보 수(클수록 그래프 품질이 좋지만 느리게 만들어지며), ef_search는 찾을 때 후보 수(클수록 재현율이 오르고 느려진다)다. HNSW는 IVF보다 같은 재현율에서 빠르고 데이터를 계속 더해도 되지만, 그래프 전체가 메모리에 있어야 빠르고 만들기가 느리며 지우기가 까다롭다.

벡터 DB: pgvector, Qdrant, Milvus

인덱스 알고리즘은 라이브러리로도 쓸 수 있다. 벡터 데이터베이스가 더하는 것은 데이터베이스의 일이다. 벡터와 함께 메타데이터를 저장하고, 고치고 지우고, 복제하고, 그리고 무엇보다 "조건을 만족하는 것 중에서 가까운 것"을 찾는다. 이 마지막이 생각보다 어렵다.

벡터 검색의 조건 거르기: 재고 있는 상품 가운데 가장 비슷한 10개를 찾을 때, 먼저 비슷한 10개를 뽑고 나서 재고 없는 것을 빼면 3개만 남는다. 탐색 중에 조건을 함께 보면 조건을 만족하는 10개를 채운다"이 상품과 비슷한 것 10개, 단 재고 있는 것만"뽑고 거르기(후필터)가장 가까운 10개 중 재고 있는 것은 3개뿐 → 결과 3개거르며 찾기(조건 인지)그래프를 따라가며 조건에 맞는 것만 세어 10개를 채운다색칠 = 재고 있음, × = 재고 없음. 조건이 까다로울수록 두 방식의 차이가 커진다
근사 인덱스로 먼저 뽑고 나중에 거르면, 조건에 맞는 것이 드물 때 결과가 모자란다. 탐색하는 동안 조건을 함께 봐야 한다.
  • pgvector: PostgreSQL 확장이다. vector 자료형과 HNSW·IVFFlat 인덱스를 더하고, 거리 연산자(<-> 유클리드, <=> 코사인, <#> 음의 내적)로 ORDER BY ... LIMIT k를 쓴다. 벡터가 다른 표와 같은 트랜잭션·같은 백업 안에 있다는 것이 가장 큰 장점이다. 근사 인덱스 뒤에 WHERE를 거는 후필터 문제는 0.8.0부터 결과가 모자라면 인덱스를 더 읽는 반복 스캔(iterative scan)으로 줄인다.
  • Qdrant: Rust로 쓴 벡터 전용 데이터베이스다(Apache 2.0). 페이로드(메타데이터) 인덱스를 먼저 만들면 HNSW에 조건을 고려한 간선을 더해 걸러진 그래프도 끊기지 않게 하고, 조건 거르기의 선택도에 따라 그래프 탐색과 페이로드 인덱스 직접 조회 중 하나를 고른다.
  • Milvus: 수십억 벡터를 겨냥한 분산 벡터 데이터베이스다(LF AI & Data 재단, Apache 2.0). 아키텍처 문서대로 접속·조정·쿼리·데이터 노드를 나누고 저장은 오브젝트 스토리지에 맡겨, 계산과 저장을 따로 늘린다. HNSW, IVF 계열, 디스크 기반 인덱스 등을 골라 쓴다.

고르는 법

시계열·그래프·벡터 엔진 고르기: 시간 순 지표를 모으고 경보를 거나? 예면 Prometheus. 관계를 여러 단계 건너는 질문인가? 예면 Neo4j. 뜻이 비슷한 것을 찾나? 예면 pgvector · Qdrant. 모두 아니면 이미 쓰는 DB나 그 확장(TimescaleDB 등)시간 순 지표를 모으고 경보를 거나?예Prometheus아니오관계를 여러 단계 건너는 질문인가?예Neo4j아니오뜻이 비슷한 것을 찾나?예pgvector · Qdrant아니오이미 쓰는 DB나 그 확장(TimescaleDB 등)
질문의 모양으로 고른다. 어느 것에도 해당하지 않으면 특수 목적 DB가 필요 없다.
질문맞는 엔진저장 구조의 핵심먼저 따져 볼 것
지표가 지금 어떤가, 경보Prometheus시간 block, 압축 chunk, pull이름표 카디널리티, 장기 보관은 따로
시계열 + SQL·조인TimescaleDB시간 chunk로 나뉜 PostgreSQL 표, 열 저장 압축이미 PostgreSQL을 쓰는가
대량 시계열 적재·분석InfluxDB 3, ClickHouseParquet·열 저장쿼리 언어, 오브젝트 스토리지
여러 단계 관계 탐색Neo4j노드가 관계를 직접 가리킴질문이 정말 여러 단계를 건너는가
뜻이 비슷한 것 찾기(소~중 규모)pgvectorPostgreSQL 안의 HNSW·IVFFlat원본 데이터와 같은 DB에 둘 수 있는가
뜻이 비슷한 것 찾기(대규모·복잡한 조건)Qdrant, Milvus조건을 아는 HNSW, 분산 저장운영할 시스템이 하나 더 늘어도 되는가

특수 목적 데이터베이스는 그 질문에서는 압도적이지만, 운영할 시스템이 하나 늘고 원본과의 동기화 문제가 생긴다. 그래서 출발점은 대개 이미 쓰는 데이터베이스의 확장(TimescaleDB, pgvector)이고, 규모나 질문의 모양이 그 한계를 넘을 때 전용 엔진으로 옮긴다. 일반 데이터베이스의 내부 원리는 저장 엔진과 인덱스와 쿼리 실행 노트에 있다.