Materialized View의 Block Size 제어
📦

Materialized View의 Block Size 제어

ClickHouse 분류
Case Study
Type
Lab
작성자

Ken

들어가며

ClickHouse의 Materialized View(MView)는 실시간 데이터 파이프라인 구축의 핵심 구성 요소입니다. 특히 외부 시스템(MySQL, PostgreSQL 등)과 연동할 때, 데이터 전송 효율성이 전체 시스템 성능을 좌우합니다. 이 글에서는 MView에서 block size 관련 설정을 제어할 수 있는지, 그리고 실제로 어떻게 동작하는지를 실증 테스트를 통해 검증해보았습니다.

  • 들어가며
  • 1. 해결하려는 문제 상황
  • 1.1 전형적인 시나리오: 실시간 데이터 파이프라인
  • 1.2 발생하는 성능 문제들
  • 문제 1: 과도한 네트워크 I/O
  • 문제 2: MySQL 트랜잭션 부하
  • 문제 3: ClickHouse 읽기 성능 저하
  • 1.3 핵심 질문
  • 2. Block Size 관련 주요 파라미터
  • 2.1 파라미터 상세 설명
  • 2.2 파라미터별 동작 방식
  • max_block_size: 읽기 블록 크기
  • min_insert_block_size_rows: 쓰기 블록 크기
  • 2.3 실제 사용 예시
  • 3. Materialized View에서의 적용 검증
  • 3.1 테스트 환경 구성
  • 3.2 MView 생성: 올바른 문법
  • ✅ 정답: TO 방식 (SETTINGS 지원)
  • ❌ 오답: ENGINE 중복 선언
  • 3.3 설정 저장 확인
  • 3.4 실행 시 적용 확인
  • 3.5 실제 Batch INSERT 동작 확인
  • 3.6 Batch INSERT 증거 확인
  • 4. 다른 Table Engine에서의 적용 여부
  • 4.1 지원하는 Engine들
  • 4.2 MySQL Table Engine 특성
  • 4.3 Kafka Engine 제약사항
  • 4.4 S3 / HDFS Engine
  • 5. 실전 권장 설정 및 베스트 프랙티스
  • 5.1 시나리오별 권장 설정
  • 시나리오 1: MySQL로 실시간 집계 전송
  • 시나리오 2: 고빈도 데이터, 낮은 지연 요구사항
  • 시나리오 3: 대용량 배치 처리, 네트워크 대역폭 제한
  • 시나리오 4: S3/HDFS 파일 출력
  • 5.2 주의사항 및 함정
  • 함정 1: GROUP BY와 min_insert_block_size_rows
  • 함정 2: Duplicate Key 에러
  • 함정 3: 메모리 부족
  • 5.3 모니터링 쿼리
  • 1. MView 설정 확인
  • 2. MView 실행 통계
  • 3. query_log에서 batch 크기 확인
  • 4. MySQL 연결 상태
  • 5.4 프로덕션 체크리스트
  • 6. 결론
  • 6.1 핵심 요약
  • 6.2 권장 기본 설정
  • 6.3 최종 조언
  • 6.4 추가 학습 자료

1. 해결하려는 문제 상황

1.1 전형적인 시나리오: 실시간 데이터 파이프라인

많은 기업들이 다음과 같은 아키텍처를 사용합니다:

1.2 발생하는 성능 문제들

문제 1: 과도한 네트워크 I/O

MView가 소량의 데이터를 매번 MySQL로 전송하면 네트워크 왕복 횟수가 급증합니다.

❌ 비효율적인 시나리오:
100만 건 이벤트 → 1건씩 INSERT → 100만 번의 네트워크 요청
각 요청당 평균 5ms 지연 → 총 5,000초 (약 83분!)

✅ 효율적인 시나리오:
100만 건 이벤트 → 10,000건씩 batch INSERT → 100번의 네트워크 요청
각 요청당 평균 50ms 지연 → 총 5초

문제 2: MySQL 트랜잭션 부하

MySQL은 각 INSERT를 트랜잭션으로 처리하므로 작은 batch로 자주 쓰면 트랜잭션 오버헤드가 증가합니다.

-- 이런 패턴이 수천~수만 번 반복되면?
START TRANSACTION;
INSERT INTO table VALUES (1, 'data');
COMMIT;

START TRANSACTION;
INSERT INTO table VALUES (2, 'data');
COMMIT;

영향:

  • Connection pool 고갈
  • Binlog 파일 크기 급증
  • Replication lag 증가
  • Lock contention 심화

문제 3: ClickHouse 읽기 성능 저하

소스 테이블에서 데이터를 읽을 때 block size가 너무 작으면 I/O 효율이 떨어집니다.

Block Size가 작을 때 (1,000):
읽기 요청 1,000번 → 각각 오버헤드 발생 → 느림

Block Size가 적절할 때 (65,536):
읽기 요청 15번 → 오버헤드 최소화 → 빠름

1.3 핵심 질문

"Materialized View 단에서 block size를 제어하여 batch INSERT 크기를 조절할 수 있는가?"

일반적인 SELECT 쿼리에서는 다음과 같이 SETTINGS 절로 block size를 제어할 수 있습니다:

SELECT * FROM large_table
SETTINGS max_block_size = 10000;

하지만 Materialized View의 경우 CREATE 문에서 이런 설정이 가능한지, 그리고 실제로 적용되는지가 불명확했습니다.

2. Block Size 관련 주요 파라미터

2.1 파라미터 상세 설명

파라미터
기본값
역할
영향 범위
max_block_size
65,536
소스 테이블에서 읽을 때 한 번에 처리하는 행 수
SELECT 성능
min_insert_block_size_rows
1,048,576
타겟 테이블에 쓸 때 최소 누적 행 수
INSERT batch 크기
min_insert_block_size_bytes
268,435,456 (256MB)
타겟 테이블에 쓸 때 최소 누적 바이트 수
INSERT batch 크기
max_insert_threads
0 (자동)
INSERT 작업에 사용할 최대 스레드 수
병렬 처리

2.2 파라미터별 동작 방식

max_block_size: 읽기 블록 크기

최적화 포인트:

  • 너무 크면: 메모리 사용량 증가
  • 너무 작으면: I/O 요청 횟수 증가, CPU 오버헤드

min_insert_block_size_rows: 쓰기 블록 크기

중요: min_insert_block_size_rows는 "최소 누적 행 수"입니다. 이 값에 도달해야 실제 INSERT가 발생합니다.

2.3 실제 사용 예시

-- 일반 SELECT 쿼리
SELECT * FROM large_table
SETTINGS max_block_size = 10000;

-- 그렇다면 Materialized View에서는?
CREATE MATERIALIZED VIEW my_mv
TO target_table
AS
SELECT ... FROM source_table
SETTINGS
    max_block_size = ???,
    min_insert_block_size_rows = ???;  -- 이게 가능한가?

3. Materialized View에서의 적용 검증

3.1 테스트 환경 구성

실제 프로덕션과 유사한 환경을 Docker로 구축했습니다:

서비스:
  - Kafka 7.5.0 (메시지 브로커)
  - ClickHouse 25.11.2 (데이터 처리)
  - MySQL 8.0 (타겟 DB)

데이터 플로우:

3.2 MView 생성: 올바른 문법

✅ 정답: TO 방식 (SETTINGS 지원)

❌ 오답: ENGINE 중복 선언

-- TO와 ENGINE을 동시에 사용하면 에러!
CREATE MATERIALIZED VIEW my_mv
TO target_table
ENGINE = MergeTree()  -- ❌ Error: Can't declare both TO and ENGINE
SETTINGS ...

에러 메시지:

DB::Exception: Syntax error: Both 'TO' and 'ENGINE' cannot be specified

3.3 설정 저장 확인

MView를 생성한 후, 설정이 실제로 저장되었는지 확인합니다.

SELECT
    name,
    create_table_query
FROM system.tables
WHERE engine = 'MaterializedView'
  AND name = 'buffer_to_mysql_mv'
FORMAT Vertical;

결과:

검증 완료: SETTINGS가 MView 정의에 영구 저장됩니다!

3.4 실행 시 적용 확인

query_log를 통해 MView 생성 시 설정이 기록되는지 확인합니다.

SELECT
    event_time,
    query_kind,
    Settings['max_block_size'] AS max_block_size,
    Settings['min_insert_block_size_rows'] AS min_insert_block_size_rows
FROM system.query_log
WHERE query LIKE '%CREATE MATERIALIZED VIEW%buffer_to_mysql_mv%'
  AND type = 'QueryFinish'
FORMAT Vertical;

결과:

event_time:                    2025-12-13 10:30:45
query_kind:                    Create
max_block_size:                1000          ✅
min_insert_block_size_rows:    5000          ✅

3.5 실제 Batch INSERT 동작 확인

50,000개의 이벤트를 Kafka로 전송하여 실제로 batch INSERT가 발생하는지 확인했습니다.

# Kafka에 50,000개 이벤트 전송
python3 kafka_producer.py --num-events 50000

# 30초 대기 후 확인

ClickHouse Buffer Table:

SELECT count() FROM events_buffer;
-- 결과: 50,000 rows

MySQL 집계 결과:

SELECT * FROM mysql_aggregated_events;
event_date
query_kind
query_count
total_duration_ms
2025-12-13
SELECT
8,279
20,720,606
2025-12-13
INSERT
8,360
20,908,893
2025-12-13
UPDATE
8,252
20,605,393
2025-12-13
DELETE
8,412
21,023,176
2025-12-13
ALTER
8,320
20,842,037
2025-12-13
CREATE
8,377
21,135,949

총 6개 행 (event_date + query_kind로 GROUP BY)

3.6 Batch INSERT 증거 확인

MView가 MySQL로 데이터를 전송할 때 실제로 batch로 묶어서 보내는지 확인하기 위해, 일부러 duplicate key 에러를 유발했습니다.

에러 메시지 (의도된 에러):

증명 완료: MView가 4개의 행을 한 번의 batch INSERT로 MySQL에 전송했습니다!

4. 다른 Table Engine에서의 적용 여부

4.1 지원하는 Engine들

Table Engine
SETTINGS 지원
비고
MySQL
✅ 지원
네트워크 I/O 고려 필수
PostgreSQL
✅ 지원
MySQL과 동일
JDBC
✅ 지원
범용 JDBC 드라이버
ODBC
✅ 지원
범용 ODBC 드라이버
S3
✅ 지원
파일 크기 최적화
HDFS
✅ 지원
블록 크기 조절 가능
Kafka
⚠️ 제한적
DEFAULT 표현식 불가
MergeTree
✅ 지원
일반적인 경우

4.2 MySQL Table Engine 특성

MySQL Table Engine은 ClickHouse에서 외부 MySQL 데이터베이스로 직접 쓰는 특수한 엔진입니다.

주의사항:

4.3 Kafka Engine 제약사항

Kafka Engine은 특별한 제약이 있습니다.

-- ❌ 이렇게 하면 에러!
CREATE TABLE kafka_events (
    event_time DateTime DEFAULT now(),  -- DEFAULT 불가!
    query_kind String,
    query_duration_ms UInt32
) ENGINE = Kafka(...);

에러 메시지:

DB::Exception: KafkaEngine doesn't support DEFAULT/MATERIALIZED/EPHEMERAL
expressions for columns.

올바른 방법:

-- ✅ DEFAULT 제거
CREATE TABLE kafka_events (
    event_time DateTime,  -- DEFAULT 없이
    query_kind String,
    query_duration_ms UInt32
) ENGINE = Kafka(...);

4.4 S3 / HDFS Engine

S3나 HDFS 같은 파일 기반 엔진에서는 block size가 파일 크기에 영향을 줍니다.

CREATE MATERIALIZED VIEW s3_export_mv
TO s3_table
AS
SELECT * FROM source_table
SETTINGS
    max_block_size = 1000000,  -- 큰 블록으로 파일 크기 증가
    min_insert_block_size_rows = 1000000;

효과:

  • 작은 파일 여러 개 → 메타데이터 오버헤드
  • 큰 파일 몇 개 → 읽기 효율 증가

5. 실전 권장 설정 및 베스트 프랙티스

5.1 시나리오별 권장 설정

시나리오 1: MySQL로 실시간 집계 전송

이유:

  • max_block_size=65536: 소스 읽기는 기본값으로 충분
  • min_insert_block_size_rows=50000: MySQL 네트워크 I/O와 트랜잭션 부하의 균형
  • min_insert_block_size_bytes=10MB: 메모리 사용량 제한

시나리오 2: 고빈도 데이터, 낮은 지연 요구사항

SETTINGS
    max_block_size = 10000,               -- 작은 블록으로 빠른 처리
    min_insert_block_size_rows = 5000,    -- 작은 batch
    min_insert_block_size_bytes = 1048576,  -- 1MB
    max_insert_threads = 4;               -- 병렬 처리

사용 예: 실시간 대시보드, 알림 시스템

시나리오 3: 대용량 배치 처리, 네트워크 대역폭 제한

SETTINGS
    max_block_size = 100000,              -- 큰 블록으로 I/O 최소화
    min_insert_block_size_rows = 500000,  -- 큰 batch
    min_insert_block_size_bytes = 268435456,  -- 256MB
    max_insert_threads = 1;               -- 순차 처리로 부하 분산

사용 예: ETL 작업, 일일 집계

시나리오 4: S3/HDFS 파일 출력

SETTINGS
    max_block_size = 1000000,             -- 매우 큰 블록
    min_insert_block_size_rows = 10000000,  -- 1천만 행씩
    min_insert_block_size_bytes = 1073741824;  -- 1GB

효과: 적은 수의 큰 파일 생성 → 메타데이터 오버헤드 감소

5.2 주의사항 및 함정

함정 1: GROUP BY와 min_insert_block_size_rows

-- 설정: min_insert_block_size_rows = 100000
-- 소스 데이터: 1,000,000 rows

SELECT date, country, count(*)
FROM events
GROUP BY date, country  -- 결과가 50개만 나옴

문제: GROUP BY 결과가 50개만 나오면, 설정이 100,000이어도 50개만 INSERT됩니다.

해결: min_insert_block_size_rows는 "소스에서 누적된 행"이 아니라 "최종 결과 행" 기준입니다.

함정 2: Duplicate Key 에러

MySQL 테이블에 Primary Key가 있고, 중복 데이터가 들어오면 에러가 발생합니다.

-- MySQL 테이블
CREATE TABLE aggregated_events (
    event_date DATE,
    query_kind VARCHAR(50),
    query_count BIGINT,
    PRIMARY KEY (event_date, query_kind)
);

문제: 같은 날짜/종류의 데이터가 다시 들어오면 Duplicate key 에러

해결 방법 1: MySQL에서 ON DUPLICATE KEY UPDATE 사용

-- ClickHouse에서 직접 지원하지 않으므로
-- MySQL 테이블을 다음과 같이 수정
CREATE TABLE aggregated_events (
    ...
    last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                 ON UPDATE CURRENT_TIMESTAMP
);

해결 방법 2: ClickHouse에서 ReplacingMergeTree 사용

-- MySQL Table Engine 대신 ClickHouse 테이블 사용
CREATE TABLE aggregated_events (
    event_date Date,
    query_kind String,
    query_count UInt64,
    total_duration_ms UInt64
) ENGINE = ReplacingMergeTree()
ORDER BY (event_date, query_kind);

-- 주기적으로 MySQL에 UPSERT

함정 3: 메모리 부족

min_insert_block_size_bytes를 너무 크게 설정하면 메모리 부족이 발생할 수 있습니다.

-- ❌ 위험!
SETTINGS min_insert_block_size_bytes = 10737418240;  -- 10GB

권장: 서버 메모리의 10% 이하로 설정

5.3 모니터링 쿼리

1. MView 설정 확인

SELECT
    name,
    database,
    engine,
    substring(create_table_query,
              position(create_table_query, 'SETTINGS'),
              200) AS settings_part
FROM system.tables
WHERE engine = 'MaterializedView'
  AND name LIKE '%_mv'
FORMAT Vertical;

2. MView 실행 통계

SELECT
    view,
    status,
    last_refresh_time,
    last_refresh_result,
    exception
FROM system.view_refreshes
WHERE database = 'default'
ORDER BY last_refresh_time DESC
LIMIT 10;

3. query_log에서 batch 크기 확인

SELECT
    event_time,
    query_duration_ms,
    read_rows,
    written_rows,
    Settings['max_block_size'] AS max_block_size,
    Settings['min_insert_block_size_rows'] AS min_insert_block_size_rows
FROM system.query_log
WHERE query LIKE '%INSERT INTO%mysql_aggregated_events%'
  AND type = 'QueryFinish'
  AND event_time >= now() - INTERVAL 1 HOUR
ORDER BY event_time DESC
LIMIT 20;

4. MySQL 연결 상태

SELECT
    host,
    port,
    database,
    user,
    default_kind,
    pool_size
FROM system.mysql_connections
FORMAT Vertical;

5.4 프로덕션 체크리스트

배포 전 다음 항목들을 확인하세요:

설정 검증:

max_block_size: 소스 테이블 크기에 적합한가?
min_insert_block_size_rows: 타겟 시스템 부하를 고려했는가?
min_insert_block_size_bytes: 메모리 사용량이 적절한가?
max_insert_threads: 병렬 처리가 필요한가?

테이블 설계:

MySQL 테이블에 적절한 인덱스가 있는가?
Primary Key 충돌 처리 방안이 있는가?
파티셔닝이 필요한가?

모니터링:

Kafka consumer lag 모니터링 설정
MView 실행 에러 알림 설정
MySQL 연결 풀 모니터링
네트워크 대역폭 사용량 추적

백업 및 복구:

ClickHouse 백업 전략
MySQL 백업 전략
데이터 불일치 시 복구 프로세스

6. 결론

6.1 핵심 요약

질문
답변
상세
MView에서 block size 제어 가능?
가능
SETTINGS 절을 SELECT 뒤에 추가
설정이 영구 저장되는가?
저장됨
create_table_query에 포함
실제로 batch INSERT되는가?
동작함
에러 로그로 확인 완료
모든 Table Engine에서 동작?
⚠️ 대부분
Kafka Engine은 일부 제약 있음

6.2 권장 기본 설정

대부분의 경우 다음 설정으로 시작하고, 모니터링 후 조정하세요:

CREATE MATERIALIZED VIEW my_mv
TO target_table
AS
SELECT ... FROM source_table
GROUP BY ...
SETTINGS
    max_block_size = 65536,              -- 기본값 유지
    min_insert_block_size_rows = 100000,  -- 10만 행
    min_insert_block_size_bytes = 10485760;  -- 10MB

6.3 최종 조언

DO (해야 할 것):

  • 네트워크 I/O가 있는 경우 반드시 block size 조정
  • 프로덕션 배포 전 부하 테스트 수행
  • query_log로 실제 batch 크기 모니터링
  • MySQL/PostgreSQL의 트랜잭션 특성 이해

DON'T (하지 말아야 할 것):

  • 기본값을 맹목적으로 신뢰
  • GROUP BY 결과 수를 무시하고 설정
  • 메모리 제한 없이 큰 block size 설정
  • TO와 ENGINE을 동시에 사용

6.4 추가 학습 자료

  • ClickHouse Official Documentation: Materialized Views
  • ClickHouse Settings Reference
  • MySQL Table Engine
  • Kafka Engine

작성일: 2025-12-13

테스트 환경: ClickHouse 25.11.2, MySQL 8.0, Kafka 7.5.0

GitHub: 실습 코드 저장소

이 문서의 모든 테스트는 실제 Docker 환경에서 재현 가능하며, 테스트 스크립트와 설정 파일은 저장소에서 확인할 수 있습니다.