Memory Table Engine: 언제, 왜, 어떻게 사용하는가들어가며
⛸️

Memory Table Engine: 언제, 왜, 어떻게 사용하는가들어가며

ClickHouse 분류
Core Architecture
Type
Introduction
작성자

Ken

ClickHouse는 다양한 Table Engine을 제공하며, 각 엔진은 특정 사용 사례에 최적화되어 있습니다. 그중에서 Memory Table Engine은 이름 그대로 데이터를 RAM에 저장하는 가장 단순하면서도 강력한 성능을 자랑하는 엔진입니다. 하지만 "언제 Memory 엔진을 사용해야 하는가?"라는 질문에 명확하게 답하기 어려운 경우가 많습니다.

이 글에서는 Memory Table Engine의 필요성과 활용 사례, 그리고 ClickHouse Cloud 환경에서의 제약사항과 대안까지 상세히 다루어봅니다.

  • 1. Memory Table Engine이란?
  • 1.1 핵심 특징
  • 1.2 기본 사용법
  • 2. 왜 Memory Table Engine이 필요한가?
  • 2.1 MergeTree vs Memory: 성능 트레이드오프
  • 2.2 주요 사용 사례
  • 3. 고급 기능: Circular Buffer
  • 3.1 설정 파라미터
  • 3.2 Circular Buffer 활용 예시
  • 4. ClickHouse Cloud에서의 제약사항
  • 4.1 핵심 제약: 데이터 복제 불가
  • 4.2 발생하는 문제
  • 4.3 해결 방법
  • 5. Memory 테이블 vs 다른 접근 방식
  • 5.1 Memory vs Temporary Table
  • 5.2 Memory vs Dictionary
  • 5.3 Memory vs Buffer Table
  • 6. 권장 사용 시나리오
  • 6.1 사용을 권장하는 경우
  • 6.2 사용을 권장하지 않는 경우
  • 7. 모니터링 및 운영 팁
  • 7.1 Memory 테이블 모니터링
  • 7.2 메모리 사용량 추적
  • 7.3 설정 변경
  • 결론
  • 참고 자료

1. Memory Table Engine이란?

Memory Table Engine은 데이터를 압축 없이 RAM에 그대로 저장하는 테이블 엔진입니다. 디스크 I/O가 전혀 발생하지 않기 때문에 읽기와 쓰기 모두에서 최고의 성능을 제공합니다.

1.1 핵심 특징

Memory Table Engine의 가장 중요한 특성들은 다음과 같습니다.

첫째, 비압축 저장 방식입니다. 데이터가 수신된 형태 그대로 메모리에 저장되므로, 읽을 때 압축 해제나 역직렬화 과정이 필요 없습니다. 이로 인해 단순 쿼리에서 초당 10GB 이상의 처리량을 달성할 수 있습니다.

둘째, 동기화된 동시 접근입니다. 읽기와 쓰기 작업이 서로를 차단하지 않으며, Lock 시간이 매우 짧습니다. 이는 고빈도 읽기/쓰기가 혼합된 워크로드에서 유리합니다.

셋째, 인덱스 미지원입니다. Memory 테이블은 인덱스를 지원하지 않습니다. 대신 읽기가 병렬로 수행되어 전체 스캔이 빠르게 처리됩니다. 이는 소규모 데이터셋에서 매우 효율적이지만, 대용량 데이터에서는 비효율적일 수 있습니다.

넷째, 휘발성입니다. 서버가 재시작되면 테이블의 모든 데이터가 사라지고 빈 상태가 됩니다. 이 특성은 Memory 엔진의 가장 큰 제약이자, 특정 사용 사례에서는 오히려 장점이 됩니다.

1.2 기본 사용법

Memory 테이블 생성은 매우 간단합니다:

-- 기본 Memory 테이블 생성
CREATE TABLE lookup_table (
    id UInt32,
    name String,
    value Float64
) ENGINE = Memory;

-- 데이터 삽입
INSERT INTO lookup_table VALUES
    (1, 'config_a', 0.95),
    (2, 'config_b', 1.23),
    (3, 'config_c', 0.87);

-- 조회
SELECT * FROM lookup_table WHERE id = 2;

2. 왜 Memory Table Engine이 필요한가?

ClickHouse의 강력한 MergeTree 계열 엔진이 있음에도 Memory 엔진이 필요한 이유는 무엇일까요?

2.1 MergeTree vs Memory: 성능 트레이드오프

MergeTree 계열 엔진은 대용량 데이터를 효율적으로 저장하고 쿼리하기 위해 설계되었습니다. 데이터를 압축하고, 인덱싱하며, 디스크에 저장합니다. 이 모든 과정은 대규모 분석 워크로드에서 최적이지만, 소량의 데이터를 빈번하게 읽고 쓰는 경우에는 오버헤드가 됩니다.

Memory 엔진은 이러한 오버헤드를 완전히 제거합니다. 압축/해제 없음, 디스크 I/O 없음, 인덱스 관리 없음. 오직 메모리에서 직접 읽고 쓰기만 합니다.

2.2 주요 사용 사례

첫 번째 사례: GLOBAL IN 연산자를 위한 임시 테이블

ClickHouse는 분산 쿼리에서 GLOBAL IN 연산을 처리할 때 내부적으로 Memory 테이블을 사용합니다. 서브쿼리 결과를 Memory 테이블에 저장한 후, 이를 모든 원격 서버로 전송하여 JOIN이나 필터링에 활용합니다.

-- GLOBAL IN 사용 예시
-- ClickHouse는 내부적으로 서브쿼리 결과를 Memory 테이블에 저장
SELECT *
FROM distributed_table
WHERE user_id GLOBAL IN (
    SELECT user_id
    FROM user_segments
    WHERE segment = 'premium'
);

이 과정에서 Memory 테이블의 빠른 읽기 성능이 분산 쿼리의 효율성을 높입니다.

두 번째 사례: 외부 데이터를 활용한 쿼리 처리

ClickHouse의 External Data 기능은 쿼리 실행 시 외부에서 데이터를 전달받아 임시 테이블로 사용할 수 있게 합니다. 이때 생성되는 임시 테이블이 바로 Memory 엔진 기반입니다.

# 커맨드라인에서 외부 데이터와 함께 쿼리 실행
cat user_ids.csv | clickhouse-client --query="
    SELECT *
    FROM main_table
    WHERE user_id IN external_table" \
    --external --name=external_table \
    --structure='user_id UInt64' \
    --format=CSV

세 번째 사례: 테스트 및 개발 환경

기능 테스트나 단위 테스트에서 빠른 데이터 생성과 삭제가 필요할 때 Memory 테이블은 이상적입니다. 서버 재시작 시 자동으로 데이터가 정리되므로 별도의 클린업 로직이 필요 없습니다.

-- 테스트용 Mock 데이터 테이블
CREATE TABLE test_mock_users (
    id UInt32,
    name String,
    score Float64
) ENGINE = Memory;

-- 테스트 데이터 로드
INSERT INTO test_mock_users
SELECT number, concat('user_', toString(number)), rand() / 1000000000
FROM numbers(10000);

-- 테스트 실행...
-- 서버 재시작 시 자동 정리

네 번째 사례: Lookup Table 및 참조 데이터

자주 조회되지만 거의 변경되지 않는 소규모 참조 데이터(설정값, 코드 매핑 등)를 Memory 테이블에 저장하면 매우 빠른 조회가 가능합니다.

-- 국가 코드 매핑 테이블
CREATE TABLE country_codes (
    code String,
    name String,
    region String
) ENGINE = Memory;

INSERT INTO country_codes VALUES
    ('KR', 'South Korea', 'Asia'),
    ('US', 'United States', 'North America'),
    ('DE', 'Germany', 'Europe');

3. 고급 기능: Circular Buffer

ClickHouse 23.3 이후 버전에서는 Memory 테이블에 크기 제한을 설정하여 Circular Buffer처럼 동작하게 만들 수 있습니다. 새로운 데이터가 삽입될 때 가장 오래된 데이터가 자동으로 삭제됩니다.

3.1 설정 파라미터

CREATE TABLE recent_events (
    event_id UInt64,
    event_time DateTime DEFAULT now(),
    event_data String
) ENGINE = Memory
SETTINGS
    min_rows_to_keep = 1000,    -- 최소 유지할 행 수
    max_rows_to_keep = 5000;    -- 최대 유지할 행 수

사용 가능한 설정은 다음과 같습니다:

설정
설명
기본값
min_bytes_to_keep
유지할 최소 바이트
0
max_bytes_to_keep
유지할 최대 바이트 (초과 시 오래된 데이터 삭제)
0
min_rows_to_keep
유지할 최소 행 수
0
max_rows_to_keep
유지할 최대 행 수 (초과 시 오래된 데이터 삭제)
0
compress
메모리 내 데이터 압축 여부
false

3.2 Circular Buffer 활용 예시

실시간 모니터링에서 최근 N개의 이벤트만 유지하고 싶을 때 유용합니다:

-- 최근 10,000개 메트릭만 유지하는 버퍼
CREATE TABLE metrics_buffer (
    timestamp DateTime,
    metric_name String,
    value Float64
) ENGINE = Memory
SETTINGS
    min_rows_to_keep = 8000,
    max_rows_to_keep = 10000;

-- 데이터 삽입 (10,000개 초과 시 자동 정리)
INSERT INTO metrics_buffer
SELECT now(), 'cpu_usage', rand() / 1000000000
FROM numbers(100);

이 기능은 최근 데이터만 필요한 실시간 대시보드나, 메모리 사용량을 제한해야 하는 환경에서 특히 유용합니다.

4. ClickHouse Cloud에서의 제약사항

ClickHouse Cloud 환경에서 Memory Table Engine을 사용할 때는 중요한 제약사항을 이해해야 합니다.

4.1 핵심 제약: 데이터 복제 불가

ClickHouse Cloud에서 Memory 테이블의 데이터는 노드 간에 복제되지 않습니다. 이는 의도된 설계입니다.

ClickHouse Cloud는 SharedMergeTree 엔진 기반의 Stateless 아키텍처를 채택하고 있습니다. 모든 데이터는 Object Storage(S3 등)에 저장되고, Compute 노드는 데이터 없이 쿼리만 처리합니다. 그러나 Memory 테이블은 이 아키텍처에 맞지 않습니다. Memory에 저장된 데이터는 해당 노드의 RAM에만 존재하며, 다른 노드로 복제되거나 공유되지 않습니다.

4.2 발생하는 문제

다음과 같은 상황이 발생할 수 있습니다:

-- 노드 A에서 데이터 삽입
INSERT INTO memory_table VALUES (1, 'test');

-- 다음 쿼리가 노드 B로 라우팅되면...
SELECT * FROM memory_table;  -- 결과: 0 rows (데이터 없음!)

HTTP 인터페이스나 Load Balancer를 통한 연결에서는 각 요청이 다른 노드로 라우팅될 수 있어, Memory 테이블의 데이터가 일관되게 보이지 않을 수 있습니다.

4.3 해결 방법

ClickHouse Cloud에서 Memory 테이블을 사용해야 한다면 다음 방법을 고려하세요:

방법 1: 동일 세션 유지

모든 작업을 단일 세션 내에서 수행합니다:

-- 세션 기반 작업
SET session_id = 'my_consistent_session';
INSERT INTO memory_table VALUES (1, 'test');
SELECT * FROM memory_table;  -- 같은 노드에서 실행

방법 2: Sticky Connection 사용

TCP 또는 Native 인터페이스를 사용하는 클라이언트(예: clickhouse-client)를 사용하면 연결이 특정 노드에 고정됩니다:

# clickhouse-client는 sticky connection 지원
clickhouse-client --host=your-cloud-instance.clickhouse.cloud \
    --secure --password=xxx

방법 3: 대안 엔진 사용

영구 저장이 필요하다면 Memory 대신 다른 엔진을 고려하세요:

대안 엔진
특징
적합한 경우
SharedMergeTree
Cloud 기본 엔진, 완전한 복제 지원
일반적인 사용
Buffer
Memory + 주기적 디스크 플러시
배치 쓰기 최적화
Dictionary
메모리 캐싱 + 소스 동기화
Lookup 데이터

5. Memory 테이블 vs 다른 접근 방식

Memory 테이블 사용을 고려할 때, 대안과의 비교가 중요합니다.

5.1 Memory vs Temporary Table

Temporary Table은 세션 종료 시 자동 삭제되는 테이블로, 기본적으로 Memory 엔진을 사용합니다:

-- Temporary Table (Memory 엔진 기반)
CREATE TEMPORARY TABLE temp_results (
    id UInt32,
    value Float64
);

-- 세션 종료 시 자동 삭제

두 방식의 차이점은 다음과 같습니다:

특성
Memory Table
Temporary Table
생명주기
서버 재시작까지
세션 종료까지
가시성
모든 세션
생성한 세션만
데이터베이스 지정
가능
불가
분산 쿼리 전송
가능
자동 전송

5.2 Memory vs Dictionary

소규모 참조 데이터라면 Dictionary가 더 적합할 수 있습니다:

-- Dictionary 방식
CREATE DICTIONARY country_dict (
    code String,
    name String
)
PRIMARY KEY code
SOURCE(CLICKHOUSE(TABLE 'country_source'))
LAYOUT(FLAT())
LIFETIME(MIN 300 MAX 360);

-- 사용
SELECT dictGet('country_dict', 'name', 'KR');

Dictionary는 자동 새로고침, 분산 환경 지원, 최적화된 메모리 레이아웃 등의 장점이 있습니다.

5.3 Memory vs Buffer Table

Buffer 테이블은 Memory와 MergeTree의 중간 지점입니다:

-- Buffer 테이블: 메모리 버퍼 + 주기적 플러시
CREATE TABLE events_buffer AS events
ENGINE = Buffer(
    default, events,  -- 대상 테이블
    16,               -- 버퍼 수
    10, 100,          -- min/max 시간(초)
    10000, 1000000,   -- min/max 행 수
    10000000, 100000000  -- min/max 바이트
);

실시간 삽입과 영구 저장이 모두 필요한 경우 Buffer가 좋은 선택입니다.

6. 권장 사용 시나리오

Memory Table Engine 사용을 권장하는 경우와 그렇지 않은 경우를 정리합니다.

6.1 사용을 권장하는 경우

다음 상황에서는 Memory 테이블이 최적의 선택입니다:

첫째, 소규모 참조/lookup 데이터: 수천에서 수백만 행 이하의 자주 조회되는 데이터. 둘째, 테스트 및 개발 환경: 빠른 데이터 생성/삭제가 필요한 경우. 셋째, 쿼리 중간 결과 저장: 복잡한 쿼리 파이프라인의 임시 저장소. 넷째, 실시간 버퍼 (Circular Buffer): 최근 N개의 레코드만 유지하는 경우.

6.2 사용을 권장하지 않는 경우

다음 상황에서는 다른 엔진을 고려하세요:

첫째, 대용량 데이터: 1억 행 이상의 데이터는 메모리 부담이 큼. 둘째, 영구 저장 필요: 서버 재시작 후에도 데이터 유지가 필요한 경우. 셋째, ClickHouse Cloud 멀티노드: 노드 간 데이터 일관성이 필요한 경우. 넷째, 복잡한 쿼리 패턴: 인덱스가 필요한 선택적 쿼리.

7. 모니터링 및 운영 팁

7.1 Memory 테이블 모니터링

-- Memory 테이블 현황 확인
SELECT
    database,
    name,
    engine_full,
    total_rows,
    formatReadableSize(total_bytes) as size
FROM system.tables
WHERE engine = 'Memory'
ORDER BY total_bytes DESC;

7.2 메모리 사용량 추적

-- 테이블별 메모리 사용량
SELECT
    database,
    table,
    formatReadableSize(sum(primary_key_bytes_in_memory)) as pk_memory,
    formatReadableSize(sum(data_compressed_bytes)) as data_size
FROM system.parts
GROUP BY database, table
ORDER BY sum(primary_key_bytes_in_memory) DESC
LIMIT 20;

7.3 설정 변경

-- 기존 Memory 테이블 설정 변경
ALTER TABLE my_memory_table
MODIFY SETTING
    min_rows_to_keep = 5000,
    max_rows_to_keep = 10000;

결론

Memory Table Engine은 ClickHouse의 다양한 테이블 엔진 중에서 가장 단순하면서도 특정 사용 사례에서 가장 강력한 성능을 제공하는 엔진입니다.

핵심 포인트를 정리하면 다음과 같습니다:

  1. 초고속 성능: 압축, 디스크 I/O, 인덱싱 오버헤드 없이 순수 메모리 성능 제공
  2. 휘발성 설계: 서버 재시작 시 데이터 손실 - 이는 제약이자 특정 상황에서는 장점
  3. Circular Buffer: 최신 버전에서 크기 제한 설정으로 자동 데이터 관리 가능
  4. Cloud 제약: ClickHouse Cloud에서는 노드 간 복제가 불가하므로 Sticky Connection 필요
  5. 적합한 사용처: 소규모 참조 데이터, 테스트 환경, 쿼리 중간 결과, GLOBAL IN 처리

Memory 테이블은 "올바른 도구를 올바른 곳에" 사용해야 하는 전형적인 예입니다. 사용 시나리오를 명확히 이해하고 적용한다면, ClickHouse 워크로드의 성능을 한층 끌어올릴 수 있습니다.

참고 자료

  • ClickHouse Memory Table Engine 공식 문서
  • ClickHouse Cloud Stateless Architecture
  • External Data for Query Processing