본문으로 건너뛰기
clickhouse검색
SQL과 쿼리

딕셔너리 (외부 키 조회를 메모리에서, 레이아웃, 갱신, dictGet)

차원 테이블 조인을 메모리 키-값 조회로 바꾸는 딕셔너리의 만드는 법, 레이아웃(flat, hashed, cache, range_hashed, ip_trie) 선택, 갱신 주기와 운영 시 주의점을 정리합니다.

딕셔너리dictGetrange_hashedip_trie조인 대체캐시2026-10 기준, 약 11분

딕셔너리가 무엇인가

딕셔너리는 외부 소스(ClickHouse 테이블, PostgreSQL, MySQL, HTTP, 파일 등)의 데이터를 서버 메모리에 키-값 구조로 올려 두고 dictGet('이름', '속성', 키)로 조회하는 객체입니다. 국가 코드로 국가 이름, 상품 ID로 카테고리, IP로 지역을 붙이는 작업을 JOIN 없이 함수 호출 한 번으로 끝냅니다.

  • 조회는 해시 조회 한 번이므로 수천만 행에 속성을 붙여도 빠릅니다.
  • 소스가 바뀌면 LIFETIME 주기에 맞춰 백그라운드로 다시 적재합니다.
  • 각 서버(복제본)가 자기 메모리에 따로 들고 있습니다. 메모리 크기를 노드 수만큼 곱해 계산합니다.

조인과의 비교는 JOIN 전략에 있습니다. 짧게 말하면, 오른쪽 테이블이 수천만 행 안쪽이고 자주 조인된다면 딕셔너리가 거의 항상 낫습니다.

만들기

CREATE DICTIONARY dim_country
(
    country_id UInt16,
    name       String,
    continent  String DEFAULT 'Unknown',
    population UInt64
)
PRIMARY KEY country_id
SOURCE(CLICKHOUSE(TABLE 'dim_country_src' DB 'ref'))
LAYOUT(FLAT())
LIFETIME(MIN 300 MAX 600);

SELECT
    dictGet('dim_country', 'name', toUInt64(country_id)) AS country,
    dictGetOrDefault('dim_country', 'continent', toUInt64(country_id), 'N/A') AS continent,
    count() AS c
FROM web_logs
WHERE event_date = today()
GROUP BY country, continent
ORDER BY c DESC;
  • PRIMARY KEY는 UInt64 하나(단순 키) 또는 여러 컬럼(복합 키, complex_key_* 레이아웃)입니다. 단순 키 레이아웃에 넘기는 키는 UInt64로 변환해야 합니다.
  • SOURCE는 ClickHouse 테이블이 가장 흔하고, POSTGRESQL, MYSQL, HTTP, FILE, EXECUTABLE, REDIS, MONGODB도 있습니다. 접속 정보는 네임드 컬렉션으로 빼 두면 DDL에 비밀번호가 남지 않습니다.
  • LIFETIME(MIN 300 MAX 600)은 300~600초 사이 임의 시점에 갱신합니다. 모든 노드가 동시에 소스를 두드리지 않게 범위를 둡니다. LIFETIME(0)은 갱신하지 않음.
  • 속성에 DEFAULT, EXPRESSION, HIERARCHICAL, INJECTIVE, IS_OBJECT_ID 같은 수식어를 붙일 수 있습니다. INJECTIVE는 키와 값이 일대일이라는 선언으로 GROUP BY 최적화에 쓰입니다.

조회 함수

함수하는 일
dictGet(d, attr, key)값. 없는 키는 속성의 DEFAULT
dictGetOrDefault(d, attr, key, def)없는 키에 지정한 기본값
dictGetOrNull(d, attr, key)없는 키는 NULL
dictGet(d, ('a', 'b'), key)여러 속성을 튜플로 한 번에
dictHas(d, key)키 존재 여부
dictGetString, dictGetUInt64 등타입 고정 버전
dictGetHierarchy, dictIsIn, dictGetChildren, dictGetDescendants계층(부모 ID) 딕셔너리 탐색
dictGetAllregexp_tree 레이아웃에서 맞는 모든 값

팁 복합 키는 dictGet('d', 'attr', (tenant_id, product_id))처럼 튜플로 넘깁니다. 키 타입이 선언과 다르면 오류가 나므로 toUInt64, toString으로 맞춥니다.

레이아웃 고르기

레이아웃키특징쓸 때
FLATUInt64, 작은 범위배열 인덱스 조회, 가장 빠름. 키 최대값까지 배열을 잡음 (max_array_size 기본 50만)연속된 작은 ID (국가, 카테고리)
HASHEDUInt64해시 맵일반적인 ID
SPARSE_HASHEDUInt64메모리 적게, 조금 느림키가 수천만 개
HASHED_ARRAYUInt64속성을 배열로 묶어 메모리 효율 좋음속성이 많은 큰 딕셔너리
COMPLEX_KEY_HASHED복합튜플 키 해시(tenant, id) 같은 키
RANGE_HASHEDUInt64 + 기간키마다 유효 기간(RANGE(MIN start MAX end))환율, 요금표, 버전별 설정
CACHE, COMPLEX_KEY_CACHEUInt64, 복합요청된 키만 소스에서 가져와 캐시소스가 너무 커서 전부 못 올릴 때
SSD_CACHEUInt64캐시를 SSD에캐시도 메모리에 안 들어갈 때
DIRECT, COMPLEX_KEY_DIRECT적재 없이 요청마다 소스 조회소스가 빠르고 데이터가 매우 자주 바뀔 때
IP_TRIE문자열 CIDRIP 주소를 CIDR 범위로 찾기GeoIP, 사내 대역 분류
REGEXP_TREE문자열정규식 트리로 매칭User-Agent 파싱
POLYGON좌표점이 어느 다각형 안인지행정 구역

처음에는 HASHED(단순 키)나 COMPLEX_KEY_HASHED로 시작하고, 메모리가 아쉬우면 HASHED_ARRAY나 SPARSE_HASHED, 소스가 너무 크면 CACHE로 바꿉니다.

주의 CACHE 레이아웃은 캐시 미스가 많으면 소스를 계속 두드려 오히려 느려지고, 소스 DB에 부하를 줍니다. 히트율은 system.dictionaries의 hit_rate로 봅니다. 대부분의 분석 워크로드는 전체 적재형이 단순하고 빠릅니다.

range_hashed: 기간이 있는 값

CREATE DICTIONARY fx_rate
(
    currency   String,
    valid_from Date,
    valid_to   Date,
    rate       Float64
)
PRIMARY KEY currency
SOURCE(CLICKHOUSE(TABLE 'fx_rate_src' DB 'ref'))
LAYOUT(COMPLEX_KEY_RANGE_HASHED())
RANGE(MIN valid_from MAX valid_to)
LIFETIME(3600);

SELECT
    order_id,
    amount_usd * dictGet('fx_rate', 'rate', tuple(currency), toDate(ordered_at)) AS amount_krw
FROM orders
WHERE ordered_at >= today() - 7;

범위가 겹칠 때 어느 값을 쓸지는 RANGE_LOOKUP_STRATEGY 설정(min 또는 max)으로 정합니다. ASOF JOIN보다 메모리와 속도 모두 유리합니다.

ip_trie: IP 대역 분류

CREATE DICTIONARY ip_region
(
    prefix String,
    region String,
    is_internal UInt8
)
PRIMARY KEY prefix
SOURCE(CLICKHOUSE(TABLE 'ip_region_src' DB 'ref'))
LAYOUT(IP_TRIE())
LIFETIME(86400);

SELECT dictGet('ip_region', 'region', tuple(IPv4StringToNum(client_ip))) AS region, count()
FROM web_logs
GROUP BY region;

IPv4와 IPv6을 함께 다루려면 키를 IPv6으로 통일합니다(IPv4ToIPv6).

갱신과 운영

  • system.dictionaries에서 상태(status), 적재 시각, 메모리(bytes_allocated), 원소 수, 마지막 오류(last_exception)를 봅니다. 모니터링 대시보드에 올려 두는 것이 좋습니다. 모니터링
  • 즉시 갱신은 SYSTEM RELOAD DICTIONARY dim_country, 전체는 SYSTEM RELOAD DICTIONARIES.
  • 소스가 바뀌지 않았으면 적재를 건너뛰게 하려면 SOURCE(... INVALIDATE_QUERY 'SELECT max(updated_at) FROM ...')를 둡니다. 결과가 전과 같으면 다시 읽지 않습니다.
  • 증분 갱신은 UPDATE_FIELD updated_at로 바뀐 행만 가져올 수 있습니다(해시 계열 레이아웃).
  • 기본은 지연 적재입니다. 첫 조회 때 올라가므로 첫 쿼리가 느립니다. 서버 설정 dictionaries_lazy_load = false로 시작 시 모두 적재하거나, 배포 후 SYSTEM RELOAD를 한 번 호출합니다.
  • 적재에 실패하면 이전 데이터를 계속 쓰고 last_exception에 이유가 남습니다. 완전히 비어 있는 상태로 실패하면 조회가 예외를 던지므로, 배포 직후에는 꼭 확인합니다.
  • 복제본마다 따로 적재하므로 노드가 10대면 소스를 10번 읽습니다. 소스가 외부 DB라면 LIFETIME을 넉넉히 두거나 ClickHouse 테이블로 한 번 복사한 뒤 그 테이블을 소스로 삼습니다.
  • ClickHouse Cloud에서는 DDL로 만든 딕셔너리가 모든 복제본에 자동으로 퍼지고, 외부 소스는 네임드 컬렉션과 네트워크 허용 설정이 필요합니다. 네트워크와 보안

딕셔너리 JOIN 문법

join_algorithm에 direct가 포함되어 있으면(기본값) 딕셔너리를 오른쪽에 둔 JOIN은 자동으로 조회로 바뀌어 해시 테이블을 만들지 않습니다. BI 도구가 JOIN 문법을 쓰더라도 성능을 챙길 수 있습니다.

SELECT l.country_id, d.name, count()
FROM web_logs AS l
LEFT JOIN dim_country AS d ON l.country_id = d.country_id
GROUP BY l.country_id, d.name;

딕셔너리는 SELECT * FROM dim_country로 테이블처럼 읽을 수도 있습니다(dictionary('dim_country') 함수도 같음).

머티리얼라이즈드 뷰에서 미리 붙이기

조회 시점마다 dictGet을 호출하는 대신, INSERT 시점에 MV 안에서 dictGet으로 속성을 붙여 결과 테이블에 저장하면 조회가 더 단순해집니다. 단, 딕셔너리가 나중에 바뀌어도 과거 행은 그대로입니다. "이벤트 당시의 값"이 필요하면 이 방식이 오히려 맞습니다. 머티리얼라이즈드 뷰

체크리스트

  • 키 타입을 UInt64 또는 튜플로 맞췄나.
  • 레이아웃이 데이터 크기와 키 모양에 맞나 (연속 작은 ID는 FLAT, 기간은 RANGE_HASHED, IP는 IP_TRIE).
  • LIFETIME에 MIN과 MAX를 두어 노드가 동시에 소스를 치지 않게 했나.
  • system.dictionaries의 status와 last_exception을 모니터링하나.
  • 메모리를 노드 수만큼 곱해 계산했나.
  • 지연 적재 때문에 첫 쿼리가 느린 것을 감안했나.

공식 문서

이 문서에서