Published on
👁️

Columnar Database 기초 이론

Authors
  • avatar
    Name
    River
    Twitter
101Columnar Database 개념과 등장 배경은?보통
Columnar Database는 전통적인 Row-based DB가 대용량 분석 워크로드에서 보이는 성능 한계를 해결하기 위해 등장했습니다. 특히 의료 업계에서 급증하는 빅데이터와 실시간 분석 요구사항을 충족하기 위한 핵심 기술로, 컬럼별로 데이터를 저장하여 분석 쿼리 성능을 10배~100배 향상시킵니다.
상세 설명

워크로드(Workload)란?

  • 워크로드 (Workload)

    • 데이터베이스에 가해지는 작업의 성격과 패턴을 의미
    • 어떤 종류의 쿼리가 얼마나 자주 실행되는지
    • 데이터 접근 패턴 (랜덤 vs 순차, 읽기 vs 쓰기)
    • 동시 사용자 수와 응답 시간 요구사항

  • 분석 워크로드 (Analytical Workload)

    • 의사결정을 위한 데이터 분석 작업들

    • 특징

      • 읽기 위주 (Write는 거의 없음)
      • 대용량 데이터(수백만~수십억 건) 스캔 필요
      • 집계 연산 (SUM, COUNT, AVG) 중심
      • 복잡한 비즈니스 로직 포함
      • 응답시간보다 정확성과 처리량이 중요
      • 전체 컬럼이 아닌 몇 개 컬럼만 필요
    • 예시

      SELECT hospital_name, COUNT(*) as patient_count, AVG(treatment_cost) as avg_cost
      FROM patients p
      JOIN hospitals h ON p.hospital_id = h.id
      WHERE admission_date >= '2024-01-01'
      GROUP BY hospital_name
      ORDER BY patient_count DESC;
      
      • 월간 매출 리포트, 환자 만족도 분석, 치료 효과 연구

OLTP vs OLAP

  • OLTP (Online Transaction Processing) - 운영 시스템

    • 일반적인 웹 애플리케이션 패턴

    • 예시

      • 환자 등록, 진료 예약, 처방전 발행
      • 결제 처리, 로그인/로그아웃
    • 특징

      • 빠른 INSERT/UPDATE/DELETE 연산 (ms 단위)
      • 소량 데이터 조회 (1-100건)
      • 높은 동시성 (수천명 이상 동시 접속)
      • 실시간 트랜잭션 처리가 핵심
    • 쿼리 예시

      -- 특정 환자 정보 조회 (빠른 응답 필요)
      SELECT * FROM patients WHERE patient_id = 12345;
      
      -- 새 진료 예약 등록
      INSERT INTO appointments (patient_id, doctor_id, date, time) 
      VALUES (12345, 67, '2024-01-15', '14:00');
      
    • MySQL, PostgreSQL 최적화



  • OLAP (Online Analytical Processing) - 분석 시스템

    • 분석, 리포팅 패턴

    • 예시

      • "지난 1년간 당뇨병 환자 통계"
      • "병원별 수익 분석"
      • "약물 효과 분석"
    • 특징

      • 대용량 데이터 집계 (SUM, AVG, COUNT) 연산
      • 복잡한 JOIN과 GROUP BY 쿼리
      • 수백만~수억 건 데이터 스캔
      • 의사결정 지원이 핵심
    • 쿼리 예시

      -- 병원별 월간 수익 분석 (대용량 데이터 처리)
      SELECT h.hospital_name, 
            DATE_TRUNC('month', t.treatment_date) as month,
            COUNT(*) as patient_count,
            SUM(t.treatment_cost) as total_revenue,
            AVG(t.treatment_cost) as avg_cost
      FROM treatments t
      JOIN hospitals h ON t.hospital_id = h.id
      WHERE t.treatment_date >= '2023-01-01'
      GROUP BY h.hospital_name, DATE_TRUNC('month', t.treatment_date)
      ORDER BY month, total_revenue DESC;
      
    • Columnar DB 최적화

Row-based DB의 분석 워크로드 한계점

  • 전통 시스템의 성능 한계

    -- 이런 쿼리가 MySQL에서 30초 이상 걸림
    SELECT hospital_name, COUNT(*) as patient_count, AVG(treatment_cost) as avg_cost
    FROM patients p
    JOIN hospitals h ON p.hospital_id = h.id
    WHERE admission_date >= '2024-01-01'
    GROUP BY hospital_name
    ORDER BY patient_count DESC;
    
    • 대규모 데이터에서 발생하는 문제점들
      • 수백만~수십억 건의 환자 데이터 스캔 필요
      • 집계 함수(COUNT, SUM, AVG)를 많이 사용하는 분석 쿼리
      • 전체 컬럼이 아닌 몇 개 컬럼만 실제로 필요함
      • 불필요한 I/O와 메모리 사용으로 인한 성능 저하

Columnar Database 핵심 아이디어

  • Row Store의 근본적 문제

    환자1: [ID=1, 이름=김철수, 나이=30, 병원=A, 비용=100만원]
    환자2: [ID=2, 이름=이영희, 나이=25, 병원=B, 비용=150만원]
    환자3: [ID=3, 이름=박민수, 나이=35, 병원=A, 비용=120만원]
    
    • 평균 치료비용 계산하려면 모든 환자 데이터를 메모리로 로드
    • Row Store는 불필요한 이름, 나이 데이터까지 함께 읽어야 한다
    • I/O 낭비메모리 비효율 발생

  • Column Store의 해결

    이름 컬럼: [김철수, 이영희, 박민수, ...]
    나이 컬럼: [30, 25, 35, ...]
    병원 컬럼: [A, B, A, ...]
    비용 컬럼: [100만원, 150만원, 120만원, ...]
    
    • 평균 비용 계산 시 비용 컬럼만 읽으면 된다.
    • 필요한 데이터만 로드I/O 90% 절약
    • 연속된 메모리 접근 ⇒ CPU 캐시 효율성 극대화
    • 결과 - 동일한 쿼리가 10배~100배 빠르게 실행 가능

Columnar DB 핵심 기술 특징

  • 압축 알고리즘

    • 같은 컬럼끼리 모여있으니 압축률이 매우 높음
    • 예 : 성별 컬럼 [남, 남, 남, 여, 여] → [남×3, 여×2] 로 압축
    • 저장공간 90% 절약 가능

  • 벡터화 실행 엔진

    • CPU가 한 번에 여러 데이터를 병렬 처리하는 기술
    • 예 : 1000개 숫자를 더할 때, 1개씩 더하지 않고 100개씩 묶어서 더한다.
    • 10배~100배 빠른 계산

언제 Columnar DB를 고려해야 할까?

  • 도입을 검토해볼 상황

    • 기존 MySQL/PostgreSQL에서 분석 쿼리가 너무 느린 경우
    • 수백만 건 이상 데이터에서 집계 연산 빈번
    • 리포팅/대시보드 성능 개선이 필요한 경우
    • 데이터 압축을 통한 저장비용 절약 필요

  • 적합하지 않은 경우

    • OLTP 워크로드 - 빈번한 INSERT/UPDATE/DELETE
    • 소량 데이터 조회 - Row 단위 빠른 조회가 필요한 경우
    • 복잡한 트랜잭션 - ACID 특성이 중요한 시스템

  • Trade-off 고려사항

    • 쓰기 성능 - Row Store 대비 INSERT, UPDATE, DELETE 성능 저하
    • 실시간성 - 배치 처리 방식으로 인한 지연 시간
    • 복잡성 - 기존 RDBMS 대비 높은 학습 곡선

  • 성능 개선 예상치

    • 집계 쿼리 - 대부분 10-100배 개선 (데이터 특성에 따라 차이)
    • 압축률 - 보통 3-10배 압축 (컬럼 데이터 중복도에 따라)
    • 주의 - 실제 성능은 POC 테스트 필수 (환경마다 다름)

주요 Columnar DB 제품들

  • 오픈소스

    • ClickHouse - 러시아 Yandex 개발, 단일 테이블 빠른 집계, SQL 호환성 높음
    • Apache Druid - 실시간 스트림 수집, 시계열 데이터 특화, 롤업 사전 집계

  • 클라우드 서비스

    • Google BigQuery - 페타바이트급 처리, SQL 표준 지원, ML 통합, 사용량 기반 과금
    • Amazon Redshift - PostgreSQL 호환, 기존 BI 툴 연동 쉬움, 예측 가능한 비용
    • Snowflake - 컴퓨트/스토리지 분리, 동시 워크로드 격리, 멀티 클라우드

  • 선택 기준

    • 비용 민감 - ClickHouse (오픈소스)
    • 기존 AWS 환경 - Redshift
    • 대용량 + 서버리스 - BigQuery
    • 멀티 클라우드 - Snowflake

참고

BI 툴 (Business Intelligence)

  • 데이터 시각화/대시보드 만드는 도구들
  • 예시 - Tableau, Power BI, Looker, Grafana
  • "월별 환자 수 차트", "병원별 수익 대시보드”

ML (Machine Learning)

  • 데이터로 패턴 학습해서 예측하는 기술
  • "환자 재입원 위험도 예측", "약물 부작용 예측”

빅데이터 시대의 의료 환경의 분석 요구사항

  • 현대 의료 환경의 데이터 특성

    • EMR (Electronic Medical Records) - 환자별 수년간 누적 데이터
    • IoMT (Internet of Medical Things) - 실시간 센서 데이터 스트림
    • 의료 영상 - DICOM 이미지와 메타데이터 급증
    • 유전체 데이터 - 개인 맞춤 의료를 위한 대용량 시퀀싱 데이터
    • 규모 - 수십만 명 × 수년간 = 억 단위 레코드

  • 실시간 분석이 필요한 의료 시나리오

    • 운영 최적화
      • 응급실 대시보드 - 실시간 환자 현황, 대기시간 분석
      • 병원 운영 최적화 - 병상 가동률, 수술실 이용률 실시간 추적
    • 임상 지원
      • 중환자 모니터링 - 바이탈 사인 이상 패턴 즉시 감지
      • 치료 효과 분석, 약물 상호작용 분석
    • 예측 분석
      • 환자 재입원 위험도, 치료 결과 예측
      • 감염병 확산 추적 - 역학 조사를 위한 접촉자 추적 분석
    • 규제 보고
      • HIPAA, 의료 품질 지표 자동 생성

  • Columnar DB가 의료 분야에 제공하는 이점

    • 즉시성 - 복잡한 의료 분석을 초 단위로 처리
    • 확장성 - 데이터 증가에도 선형적 성능 유지
    • 비용 효율성 - 하드웨어 리소스 최적 활용
    • 의료진 지원 - 데이터 기반 의사결정을 실시간으로 지원
102압축 알고리즘 동작 원리와 사용 방법은?어려움
Columnar Database의 압축 기술은 저장 공간 절약과 쿼리 성능 향상을 동시에 달성하는 핵심 기능입니다. Dictionary Encoding은 반복되는 문자열을 숫자로 변환하고, Delta Encoding은 시계열 데이터의 차이값만 저장하며, Bit-Packing은 실제 값 범위에 맞는 최소 비트를 사용하여 압축합니다. 이러한 기법들을 LZ4나 ZSTD 같은 범용 압축과 조합하면 데이터에서 수 배에서 수십 배까지 압축이 가능합니다. 오늘날 NVMe SSD 환경에서는 디스크 속도보다 압축 해제 속도가 성능을 좌우하므로, 실시간 분석에는 빠른 해제가 가능한 가벼운 압축을, 보관용 데이터에는 강력한 압축을 선택하는 것이 중요할 것 같습니다.
상세 설명

왜 Columnar Database에서 압축이 중요한가?

  • 압축의 3가지 성능 향상 효과

    1. I/O 감소 효과

      원본 데이터: 1GB
      압축 후: 100MB (10배 압축)
      
      • 디스크 읽기 시간 : 1초 ⇒ 0.1초 (90% 단축)

    2. 메모리 효율성

      • 동일한 16GB RAM에
        • 원본 : 1600만 건 로드 가능
        • 압축 : 1억 6000만 건 로드 가능 (10배 증가)

    3. 네트워크 비용 절약 (클러스터 환경)

      • 데이터 전송량 90% 감소


  • Columnar 저장의 압축 우위

    • Row-based 저장

      환자1: [김철수, 35, 당뇨병, 2024-01-01]
      환자2: [이영희, 42, 고혈압, 2024-01-02]
      
      • 다양한 타입 섞임 ⇒ 압축 어려움

    • Column-based 저장
      이름: [김철수, 이영희, 박민수, ...]      -- 문자열끼리 모임
      나이: [35, 42, 28, ...]                 -- 숫자끼리 모임
      진단: [당뇨병, 고혈압, 당뇨병, ...]      -- 반복 패턴 명확
      
      • 같은 타입끼리 모임 ⇒ 압축 유리

Dictionary 압축 (Dictionary Encoding)

  • 동작 원리

    -- 원본 데이터
    진단코드 : [E11.9, E11.9, I10, E11.9, I10, E11.9, I25.1, I10]
    
    -- 1: 고유값으로 Dictionary 생성
    Dictionary : {0: "E11.9", 1: "I10", 2: "I25.1"}
    
    -- 2: 원본 데이터를 숫자 ID로 변환  (Dictionary 이용)
    압축결과: [0, 0, 1, 0, 1, 0, 2, 1]
    
    • Dictionary Encoding반복되는 문자열을 숫자 ID로 대체하는 압축 기법
    • 고유한 값들Dictionary 테이블에 한 번만 저장
    • 실제 데이터는 Dictionary에서 해당 값의 ID(정수)로 변환하여 저장
    • 쿼리 실행 시에는 ID를 다시 원본 문자열로 변환하여 사용자에게 반환

  • 메모리 절약 효과

    • 만약 100만 건의 데이터를 저장할 때

    • Dictionary 압축 전
      - "E11.9" 50만번 × 5바이트 = 2.5MB
      - "I10" 30만번 × 3바이트 = 0.9MB
      - "I25.1" 20만번 × 5바이트 = 1.0MB
      
      • 총합 : 4.4MB

    • Dictionary 압축 후
      - Dictionary : 20개 코드 × 평균 5바이트 = 100바이트
      - 데이터: 100만개 × 4바이트 = 4MB
      
      • Int32 정수 크기 = 4 바이트
      • 총합 : 4MB (약 10% 절약)
      • 중복도가 높을수록 효과 증대
      • Bit-Packing 기법을 함께 사용하면 4 바이트가 아니라 5 비트까지 더욱 줄일 수 있다

  • 적용 사례

    • ICD-10 질병 코드 - E11.9(당뇨병) 같은 코드가 수만 번 반복되므로 유용
    • 약물명 - "아스피린", "메트포르민" 등 제한된 목록 안에서 반복 등장
    • 병원명/의사명 - 동일 기관에서 발생하는 대량 데이터
    • 검사명 - "혈당검사", "혈압측정" 등 표준화된 항목

RLE 압축 (Run-Length Encoding)

  • 동작 원리

    -- 원본 데이터
    성별: [,,,,,,,,]
    
    -- RLE 압축 : 연속된 값을 (값, 개수) 쌍으로 저장
    압축결과: [(, 4), (, 2), (, 3)]
    
    • Run-Length Encoding연속으로 반복되는 동일한 값을 (값, 개수) 쌍으로 압축하는 기법
    • 연속성이 핵심이므로 데이터 정렬 순서가 압축 효과에 큰 영향을 미친다.
    • 같은 값이라도 흩어져 있으면 압축 효과가 없고, 연속으로 배치되어야 효과적
      • 위의 예시를 보면 같은 남이라도 연속되어 있지 않으면 합쳐지지 않는다.
    • Boolean 데이터카테고리형 데이터에서 특히 효과적이다.
      • 예시 - 의료 데이터의 정상/비정상 구분에 유용

  • 데이터 정렬의 중요성

    • 비효율적 정렬 (랜덤 순서)

      ORDER BY patient_id
      [,,,,,]
      
      → RLE: [(,1), (,1), (,1), (,1), (,1), (,1)]
      
      • 정렬이 되어있지 않을 때 ⇒ 압축 효과 없음
      • 정렬에 따라 압축률이 수천 배 차이날 수도 있다.

    • 효율적 정렬

      ORDER BY gender, patient_id
      [,,,,,]
      
      → RLE: [(,3), (,3)]
      
      • 효율적으로 정렬이 되어 있는 경우 ⇒ 50% 압축

  • 적용 사례

    • 병동 정보 - 같은 병동 환자들이 연속 배치된 경우
    • Boolean 플래그 - 정상/비정상, 입원/외래 구분
    • 혈액형 - A, B, AB, O의 제한된 값
    • 검사 결과 - "정상" 범위가 대부분인 검사 데이터

Delta 압축 (Delta Encoding)

  • 동작 원리

    -- 원본 데이터
    혈압: [120, 122, 121, 123, 119, 124, 125]
    
    -- Delta 압축: 이전 값과의 차이만 저장
    기준값: 120
    차이값: [0, +2, -1, +2, -4, +5, +1]
    
    • Delta Encoding이전 값과의 차이만 저장하여 값의 범위를 축소하는 압축 기법
    • 첫 번째 값을 기준값으로 설정하고, 나머지는 이전 값과의 차이만 계산하여 저장한다.
    • 점진적으로 변화하는 데이터에서 효과적이며, 차이값들이 작을수록 더 적은 비트로 표현 가능하다.
      • 4개 비트로 0~15까지 표현 가능한 것처럼 차이값이 작으면 적은 비트로 표현이 가능한 것이다.
    • 시계열 데이터순차적으로 증가하는 ID에서 특히 유용하다.
      • 시계열 데이터의 특성은 보통 점진적으로 변화하고 시간과 연관되어 있기 때문
      • 또는 시간 데이터 자체를 저장할 때도 유용하다.

  • 압축 효과

    • Delta 압축 전
      • 120 ~ 125 범위 ⇒ 7비트 필요

    • Delta 압축 후
      • -4 ~ +5 범위 ⇒ 4비트 필요
      • 7비트 ⇒ 4비트 (약 43% 절약)

  • 적용 사례

    • 바이탈 사인 - 혈압, 심박수, 체온 등 점진적 변화
    • 검사 수치 - 혈당, 콜레스테롤 등 시간에 따른 변화
    • 시간 데이터 - 측정 시간, 입원 날짜 등 순차 데이터
    • 환자 ID - 순차적으로 증가하는 식별자

Bit-Packing

  • 동작 원리

    -- 원본 데이터 (0 ~ 10 범위)
    통증척도: [0, 1, 2, 0, 1, 3, 2, 1, 10, 5]
    
    -- 실제 저장 (비트 패킹)
    원본: [00000000000000000000000000000000, 00000000000000000000000000000001, ...]
    압축: [0000, 0001, 0010, 0000, 0001, 0011, 0010, 0001, 1010, 0101]
    
    • Bit-Packing데이터의 실제 값 범위에 맞는 최소 비트 수로 저장하는 압축 기법
    • 값의 최대 범위를 분석하여 필요한 비트 수를 계산
      • 예시 : 0~10 범위 ⇒ 4비트
    • 원본 데이터 타입(Int32)보다 훨씬 적은 비트로 저장하여 메모리 절약
    • 제한된 범위의 값이나 카테고리형 데이터에서 압축률이 매우 높다.
    • NULL 값 처리에도 활용 가능
      • NULL = 0
      • 데이터 있음 = 1

  • 압축 효과

    • Bit-Packing 압축 전
      • Int32 ⇒ 32비트

    • Bit-Packing 압축 후
      • 0 ~ 10 범위 ⇒ 4비트면 충분
      • 32비트 ⇒ 4비트 (8배 압축)

  • 적용 사례

    • 등급 데이터 - 통증 척도(0-10), 만족도(1-5)
    • Boolean 플래그 - 알레르기 유무, 흡연 여부
    • 카테고리 - 혈액형(4가지), 성별(2가지)
    • NULL 값 처리 - 검사 결과 유무 표시

압축률 vs 성능 트레이드오프 ⭐

압축 기법압축률CPU 오버헤드해제 속도최적 용도
Dictionary높음낮음매우 빠름반복 문자열
RLE높음매우 낮음매우 빠름연속 동일값
Delta중간보통빠름시계열 데이터
Bit-Packing매우 높음낮음매우 빠름제한된 범위 값
LZ4중간높음보통범용 압축
ZSTD높음매우 높음느림보관용 데이터
  • 높은 압축률 ≠ 빠른 쿼리 성능

    • 최신 NVMe SSD 환경에서는 I/O보다 CPU의 압축 해제 속도가 쿼리 성능을 좌우한다.
      • NVMe SSD : 고속 인터페이스를 사용하는 차세대 SSD (3,000-7,000 MB/s)
      • 과거는 디스크가 병목이었지만, 현재는 압축 해제 속도가 병목이다. (CPU 처리가 따라가지 못함)
    • LZ4처럼 압축률은 중간이지만 해제 속도가 매우 빠른 코덱이 실시간 분석에 더 적합하다


  • 워크로드별 압축 전략
    • 실시간 대시보드 (응답 속도 우선)

      • 목표 - 초 단위 응답
      • 전략 - Dictionary + RLE (가벼운 압축)
      • 예시 - 실시간 환자 현황, 응급실 대기시간

    • 월간 분석 리포트 (저장 효율성 우선)

      • 목표 - 정확성과 저장 공간 최적화
      • 전략 - Delta + ZSTD (강력한 압축)
      • 예시 - 월간 환자 통계, 병원 운영 분석

RLE 압축 vs Bit-Packing

  • 카테고리 / Boolean 플래그 데이터 압축 선택
    • “어느 압축 기법이 좋은가”는 상황에 따라 다르다.
    • 10만 건 성별 데이터 (50% 남성, 50% 여성) 있을 때

      • 데이터가 정렬되어 있는 경우

        성별: [,,,,,,,...]
        
        • RLE 압축
          • [(남, 50000), (여, 50000)]2개 쌍만 저장
        • Bit-Packing
          • 10만개 × 1비트 = 12.5KB

      • 데이터가 섞여있는 경우

        성별: [,,,,,,,...]
        
        • RLE 압축
          • [(남,1), (여,1), (남,1), ...]10만개 쌍만 저장
        • Bit-Packing
          • 10만개 × 1비트 = 12.5KB

    • Bit-Packing은 일정한 효율을 보이는 장점이 있다.

압축 기법의 실제 활용

  • LZ4, ZSTD

    • 범용 압축 알고리즘
    • 어떤 데이터든 압축 가능 (텍스트, 이미지, 바이너리 등)
    • 예시
      • 실시간 쿼리 - Dictionary + LZ4 (빠른 해제)
      • 보관용 데이터 - Dictionary + ZSTD (높은 압축률)


  • ClickHouse CODEC 조합 예시

    CREATE TABLE patients (
        patient_id Int32 CODEC(Delta, LZ4),  -- Delta + LZ4 조합
        gender String CODEC(ZSTD),           -- Dictionary + ZSTD 조합
        blood_type Enum8 CODEC(LZ4)          -- Bit-Packing + LZ4 조합
    );
    
    • 실제로 압축 기법은 조합하여 사용된다.

    • diagnosis_code String CODEC(ZSTD)

      • 1단계 : Dictionary Encoding (ClickHouse 자동)

        "당뇨병"0, "고혈압"1, "천식"2
        
        [당뇨병, 고혈압, 당뇨병][0, 1, 0]
        
      • 2단계 : ZSTD 범용 압축

        [0, 1, 0] → 바이너리 압축 데이터
        
    • 이렇게 조합하여 사용하는 이유는 단독 사용일 때보다 시너지로 더 좋은 압축률을 보이기 때문이다.



  • Dictionary + Bit-Packing 조합

    • 1단계 : Dictionary Encoding

      진단코드: ["E11.9", "I10", "I25.1", "E11.9", "I10"]
      → Dictionary: {0: "E11.9", 1: "I10", 2: "I25.1"}
      → ID 배열: [0, 1, 2, 0, 1]
      

    • 2단계 : Bit-Packing

      ID 0 = 00
      ID 1 = 01  
      ID 2 = 10
      → 최종: [00, 01, 10, 00, 01]
      
      • 3개 고유값 ⇒ 2비트면 충분


  • RLE + Bit-Packing 조합

    • 1단계 : RLE Encoding

      성별: [,,,,,,]
      → RLE: [(,3), (,2), (,2)]
      

    • 2단계 : Bit-Packing

      값: 남=0,=1 (1비트)
      개수: 2비트
      최종: [(0,11), (1,10), (0,10)]
      
      • 개수의 고유값은 최대 3 ⇒ 2비트면 충분

ClickHouse CODEC 설정과 기본 사용법 ⭐

  • 기본 조합 패턴

    CREATE TABLE patients (
        patient_id Int32 CODEC(Delta, LZ4),     -- Delta + 범용압축 조합
        diagnosis_code String CODEC(ZSTD),      -- Dictionary + 범용압축 조합  
        gender Enum8('M'=1, 'F'=2)              -- 자동 최적화
    );
    
    • Dictionary EncodingString 컬럼에서 자동으로 적용


  • ClickHouse 자동 최적화 기준

    • RLE는 별도의 독립적인 CODEC이 아니다.

      • ClickHouse에서 RLE범용 압축 알고리즘(LZ4, ZSTD) 내부에서 자동으로 처리

    • String 컬럼
      1. 카디널리티 확인 (고유값 개수)
      2. 카디널리티 낮음 ⇒ Dictionary Encoding
      3. Dictionary ID를 Bit-Packing으로 저장
      4. 추가로 LZ4/ZSTD 범용 압축 적용

    • Enum/Boolean 컬럼
      1. 값 범위 분석
      2. 자동으로 최소 비트 할당
      3. CODEC 지정시 추가 압축 적용 (LZ4/ZSTD)
      4. 이때 ORDER BY 설정으로 정렬되어 저장되면 범용 압축 알고리즘에서 RLE 효과 자동 적용


  • 기본 설계 원칙

    • 읽기 빈도가 높은 컬럼 : 가벼운 압축 (Dictionary, RLE, LZ4)

    • 보관용 데이터 : 강력한 압축 (ZSTD)

    • 시계열 데이터 : Delta 압축 활용

    • 카테고리 데이터 : Enum 타입 자동 최적화

      CREATE TABLE patients (
          gender Enum8('M'=1, 'F'=2),                    -- 자동으로 1비트로 최적화
          blood_type Enum8('A'=1, 'B'=2, 'AB'=3, 'O'=4)  -- 자동으로 2비트로 최적화
      );
      
      • 특수한 경우 - 정렬이 고정된 환경에서 RLE 압축 활용 가능


  • 주의사항

    • CODEC 조합시 순서 중요 : CODEC(Delta, LZ4) (특화 압축 먼저 실시)
    • 과도한 압축은 CPU 오버헤드 증가로 쿼리 성능 저하 가능
    • 압축 설정 변경 시 기존 데이터 재압축 필요 (ALTER TABLE 작업 시간 소요)
103벡터화 실행 엔진과 SIMD는 어떻게 작동하나?어려움
벡터화 실행 엔진은 CPU의 SIMD 명령어를 활용하여 하나의 명령으로 여러 데이터를 동시에 처리하는 기술입니다. 전통적인 Iterator 모델이 레코드를 하나씩 처리하는 반면, 벡터화 엔진은 수백~수천 개의 값을 배치로 처리하여 대규모 데이터 집계 쿼리에서 10배~100배의 성능 향상을 보일 수 있습니다. 단, 복잡한 조건문이나 문자열 처리에서는 벡터화 효과가 제한될 수 있습니다.
상세 설명

기본 개념

  • 스칼라 처리 vs 벡터 처리

    • 스칼라 처리

      a = 10
      b = 20
      result = a + b  -- 한 번에 하나의 덧셈
      
      • 하나의 값에 하나의 연산
    • 벡터 처리

      a_vector = [10, 20, 30, 40]
      b_vector = [5,  15, 25, 35]
      result_vector = a_vector + b_vector  -- 한 번에 4개 덧셈 동시 실행
      
      • 여러 값에 동시에 연산

  • SIMD (Single Instruction, Multiple Data)

    • CPU가 한 번의 명령으로 여러 데이터를 동시에 처리할 수 있게 해주는 기술
    • 마치 공장에서 하나씩 제품을 만드는 대신 컨베이어 벨트로 여러 제품을 동시에 처리하는 것과 같다.
    • 즉, 동일한 연산을 여러 데이터에 동시에 적용하는 것
    • 개발자가 직접 사용하는 것이 아니라, 데이터베이스 엔진이 내부적으로 자동 활용하는 기술

  • Iterator vs Vectorized 처리 방식

    • Iterator
      • 반복자, 데이터를 하나씩 순차적으로 처리하는 방식
    • Vectorized
      • 벡터화, 데이터를 배치(묶음)로 한꺼번에 처리하는 방식

SIMD가 왜 필요한가?

  • 상황

    SELECT AVG(systolic_bp) FROM vital_signs;
    
    • 100만 명 환자의 평균 혈압 계산

  • 전통적인 스칼라 처리의 한계

    sum = 0
    
    for i in range(1000000):
        sum += blood_pressure[i]    -- 100만 번의 개별 덧셈 연산
    
    average = sum / 1000000
    
    • 스칼라 처리 방식 (하나씩 순차 처리)
    • 처리 시간
      • 100만 번의 연산 × CPU 사이클 = 상당한 시간 소요


  • SIMD 병렬 처리

    sum_vector = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]  -- 16개 누적기
    
    for i in range(0, 1000000, 16):
        values = load_16_values(blood_pressure[i:i+16])    -- 16개 값 동시 로드
        sum_vector += values                               -- 16개 덧셈 동시 실행
    
    final_sum = sum(sum_vector)
    average = final_sum / 1000000
    
    • SIMD 벡터 처리 방식 (여러 개 동시 처리)
    • AVX-512
      • 한 번에 16개의 32비트 정수 동시 처리
    • 실제로는 DB 엔진이 내부적으로 SIMD 최적화를 알아서 적용한다.
    • 처리 시간
      • 62,500번의 벡터 연산 (16배 감소)

CPU SIMD 명령어와 병렬 처리 메커니즘

  • SIMD 명령어 종류와 성능

    SIMD 기술레지스터 크기동시 처리 가능 (32비트)성능 향상
    SSE128비트4개4배
    AVX256비트8개8배
    AVX-512512비트16개16배
    • Intel x86/x64 서버 기준
    • 32비트 정수 (int32), 32비트 부동소수점 (float32)
    • 64비트 부동소수점 (double64) ⇒ AVX-512에서 8배 향상


  • 의료 데이터 처리 예시

    1. INSERT 시 Columnar DB 내부에서 분해하여 저장

      INSERT INTO patients VALUES (1, '김철수', 7.2);
      
      Column 1 (ID):    기존 [1,2,3...] 뒤에 → [1,2,3,4...]
      Column 2 (Name):  기존 [...] 뒤에 → [..., '김철수'] 
      Column 3 (HbA1c): 기존 [7.1,6.8...] 뒤에 → [..., 7.2]
      
      • 디스크에 컬럼별로 압축하여 연속 저장한다.

    2. 쿼리 실행 시

      -- 당뇨병 환자 HbA1c 수치 분석
      SELECT
          COUNT(*) as patient_count,
          AVG(hba1c_value) as avg_hba1c,
          MIN(hba1c_value) as min_hba1c,
          MAX(hba1c_value) as max_hba1c
      FROM lab_results
      WHERE test_code = 'HbA1c' AND hba1c_value > 0;
      
      • SIMD 처리 과정
        1. hba1c_value 컬럼을 데이터를메모리로 연속으로 로드

          메모리 주소: 0x1000  0x1004  0x1008  0x100C  0x1010  0x1014...
          데이터:      [7.2]   [6.5]   [8.1]   [5.9]   [7.8]   [6.3]...
                         ↑       ↑       ↑       ↑       ↑       ↑
                       32비트  32비트  32비트  32비트  32비트  32비트
          
        2. AVX-51216개 값씩 동시에 조건 검사 (> 0)

        3. 조건 만족하는 값들을 16개씩 동시에 집계 연산

          (즉, COUNT, SUM, MIN, MAX를 병렬로 계산)

벡터화가 가능한 이유 ⭐

  • Columnar DB의 역할

    • 전체 처리 파이프라인

      디스크 (압축된 상태) → 메모리 (압축 해제) → CPU (SIMD 처리)
           ↓                    ↓                  ↓
      [압축된 데이터][7.2,6.5,8.1...][16개씩 병렬 연산]
      

    • 1단계 : 압축 저장으로 인한 효율성

      *- HbA1c 원본 데이터*
      [7.2, 6.5, 8.1, 5.9, 7.8, 6.3, 7.1, 6.8, ...]
      
      *- Dictionary + Bit-Packing 압축*
      Dictionary: {0: 5.9, 1: 6.3, 2: 6.5, 3: 6.8, 4: 7.1, 5: 7.2, 6: 7.8, 7: 8.1}
      압축된 ID: [5, 2, 7, 0, 6, 1, 4, 3, ...] (3비트 표현)
      
      • Dictionary + Bit-Packing 압축 효과
        • 32비트 ⇒ 3비트 (약 90% 저장 공간 절약)
      • I/O 향상
        • 디스크에서 메모리로 로딩 시간 대폭 단축

    • 2단계 : 메모리에서 압축 해제 후 연속 배치

      압축된 ID: [5, 2, 7, 0, 6, 1, 4, 3, ...]
      
         ↓ 압축 해제
      
      float hba1c_array[] = {7.2, 6.5, 8.1, 5.9, 7.8, 6.3, 7.1, 6.8, ...};
                              ↑ 연속된 메모리 배치
      
      • Dictionary lookup을 통한 압축 해제 ⇒ 원본 값으로 복원
      • 연속된 메모리 배치 (SIMD 최적화 준비 완료)

    • 3단계 : SIMD 벡터 처리

      float hba1c_array[] = {7.2, 6.5, 8.1, 5.9, 7.8, 6.3, 7.1, 6.8, ...};
      
      // AVX-512로 16개씩 동시 처리
      for (int i = 0; i < count; i += 16) {
          __m512 values = _mm512_loadu_ps(&hba1c_array[i]);
          
          // 16개 값을 한 번에 조건 검사 (> 0)
          __mmask16 mask = _mm512_cmp_ps_mask(values, zero_vec, _CMP_GT_OQ);
          
          // 조건 만족하는 값들만 집계 연산
          sum_vec = _mm512_mask_add_ps(sum_vec, mask, sum_vec, values);
          count_vec = _mm512_mask_add_epi32(count_vec, mask, count_vec, one_vec);
          min_vec = _mm512_mask_min_ps(min_vec, mask, min_vec, values);
          max_vec = _mm512_mask_max_ps(max_vec, mask, max_vec, values);
      }
      
      • AVX-512로 16개씩 동시 처리


  • 압축과 SIMD의 시너지 효과

    • 압축 기법의 기여 (디스크 ⇒ 메모리)

      • I/O 병목 해결 - Dictionary/RLE/Delta 압축으로 읽어올 데이터량 감소 (예시는 90%)
      • 메모리 효율성
        • 동일한 RAM에 더 많은 데이터를 저장(로드) 가능 (예시는 10배)
        • 압축이 없다면 16GB RAM에 400만 건의 환자 데이터 로드 가능하지만 압축이 있다면 4000만 건 데이터 로드 가능
      • 연속성 보장
        • 컬럼형 저장으로 동일 타입 데이터의 물리적 연속 배치
        • 즉, 추후 연속된 메모리에 배치하기 위한 전제 조건

    • SIMD의 역할 (메모리 ⇒ CPU)

      • 연산 병목 해결 - 압축 해제된 연속 데이터를 16개씩 동시 처리
      • 병렬 집계 - COUNT, SUM, MIN, MAX 연산을 동시에 수행

    • 핵심

      • 압축 기법이 단순히 저장 공간만 절약하는 것이 아니라, 동일한 타입의 데이터를 연속된 메모리에 효율적으로 배치시켜 SIMD 처리를 가능하게 만드는 핵심 인프라 역할을 수행한다.
      • Columnar 저장동일 타입 연속 저장메모리 연속 배치SIMD 최적화

Vectorized vs Iterator 실행 모델 차이점

  • 두 가지 데이터 처리

    • Iterator 모델 - 각 레코드를 개별적으로 처리
    • Vectorized 모델 - 여러 레코드를 묶어서 배치 처리

  • Iterator 모델 (Row-based Database)

    -- 환자별 월간 평균 혈압 계산
    class IteratorExecutor {
        execute() {
            for each patient {              -- 환자 1명씩 순차 처리
                for each measurement {      -- 측정 기록 1개씩 처리
                    if (date >= '2024-01-01') {
                        sum += systolic_bp;  -- 개별 덧셈
                        count++;
                    }
                }
                result[patient] = sum / count;  -- 개별 나눗셈
            }
        }
    }
    
    • MySQL/PostgreSQL의 처리 방식
    • 특성
      • Tuple-at-a-time (한 번에 1개 레코드 처리)
      • 함수 호출 오버헤드 (레코드마다 함수를 새로 호출)
      • 조건 분기로 인한 CPU 성능 저하
        • CPU가 다음에 뭘 할지 예측하기 어려워서 성능 저하
      • 메모리 랜덤 액세스 (캐시 미스 빈발)


  • Vectorized 모델 (Columnar Database)

    class VectorizedExecutor {
        execute() {
            date_column = load_column("measurement_date");     -- 전체 컬럼 로드
            bp_column = load_column("systolic_bp");
            patient_column = load_column("patient_id");
    
            // 1. 날짜 필터링 (SIMD로 일괄 처리)
            date_mask = simd_compare(date_column, '2024-01-01');  -- 1000개씩 동시 비교
    
            // 2. 필터링된 데이터 추출
            filtered_bp = simd_filter(bp_column, date_mask);
            filtered_patients = simd_filter(patient_column, date_mask);
    
            // 3. 그룹별 집계 (벡터화된 그룹핑)
            result = simd_group_by_avg(filtered_patients, filtered_bp);
        }
    }
    
    • ClickHouse/DuckDB의 처리 방식
    • 특성
      • Column-at-a-time (한 번에 1000개+ 값 처리)
      • 함수 호출 최소화 (배치당 1번 호출)
      • 조건 분기 최소화 (미리 계산된 비트마스크)
        • 조건 검사를 한 번에 끝내고 결과를 저장해둔 비트마스크 생성
      • 순차 메모리 액세스 (캐시 효율성 극대화)
        • 메모리에서 연속된 위치의 데이터를 순서대로 읽는다.

의료 집계 쿼리에서의 벡터화 효과

  1. 수치 범위 필터링

    -- 고혈압 환자 조건부 집계
    SELECT
        age_group,
        COUNT(*) as patient_count,
        AVG(systolic_bp) as avg_systolic
    FROM (
        SELECT
            patient_id,
            systolic_bp,
            CASE
                WHEN age < 40 THEN '40미만'
                WHEN age < 60 THEN '40-60'
                ELSE '60이상'
            END as age_group
        FROM vital_signs
        WHERE systolic_bp >= 140 AND diastolic_bp >= 90  -- SIMD 범위 조건
    )
    GROUP BY age_group;
    
    • 벡터화 효과로 인한 성능 향상 : 10-20배
      1. 혈압 범위 조건 - 16개씩 동시 비교
      2. 연령 그룹 분류 - 16개씩 동시 계산
      3. AVG, COUNT - 16개씩 동시 집계


  2. 시계열 집계 분석

    -- 월별 병원 운영 지표 분석
    SELECT
        toYYYYMM(admission_date) as month,
        hospital_id,
        COUNT(*) as admission_count,
        AVG(length_of_stay) as avg_stay,
        SUM(treatment_cost) as total_revenue,
        quantile(0.5)(treatment_cost) as median_cost,
        quantile(0.95)(treatment_cost) as p95_cost
    FROM admissions
    WHERE admission_date >= '2023-01-01'
    GROUP BY month, hospital_id
    ORDER BY month, hospital_id;
    
    • 벡터화 효과로 인한 성능 향상 : 10-30배
      1. 날짜 변환 - 16개씩 동시 처리
      2. 다중 집계 함수 - 병렬 계산
      3. quantile - 정렬된 데이터에서 SIMD 활용


  3. 조건부 집계

    -- 진료과별 성과 지표 분석
    SELECT
        department,
        countIf(treatment_result = 'SUCCESS') as success_count,
        countIf(treatment_result = 'FAILURE') as failure_count,
        avgIf(treatment_cost, treatment_result = 'SUCCESS') as avg_success_cost,
        avgIf(length_of_stay, readmission_30d = 0) as avg_stay_no_readmit
    FROM treatments t
    JOIN departments d ON t.department_id = d.id
    WHERE treatment_date >= '2024-01-01'
    GROUP BY department;
    
    • 벡터화 효과로 인한 성능 향상 : 5-15배
      1. countIf - 조건과 카운팅을 16개씩 동시 처리
      2. avgIf - 조건부 평균을 벡터화

벡터화 효과가 제한되는 경우

  1. 복잡한 문자열 처리

    -- 환자 이름 정규화 및 검색
    SELECT
        patient_id,
        CONCAT(UPPER(last_name), ', ', UPPER(first_name)) as formatted_name,
        REGEXP_REPLACE(phone, '[^0-9]', '') as clean_phone,
        LEVENSHTEIN(address, '서울특별시') as address_similarity
    FROM patients
    WHERE name LIKE '%김%' OR name LIKE '%이%'
       OR SOUNDEX(name) = SOUNDEX('홍길동');
    
    • 문제점
      • 가변 길이 문자열은 SIMD 처리가 복잡하다
        • SIMD는 고정 크기 필요
      • 정규표현식은 순차 처리 필요
      • 복잡한 문자열 함수는 벡터화가 어렵다
      • 성능 향상 : 1.5-2배 (제한적)


  2. 복잡한 조건 분기

    -- 복잡한 환자 분류 로직
    SELECT
        patient_id,
        CASE
            WHEN age < 18 AND has_guardian = 1 THEN '소아보호자동반'
            WHEN age < 18 AND has_guardian = 0 THEN '소아보호자없음'
            WHEN age >= 65 AND chronic_disease_count >= 3 THEN '고령다질환'
            WHEN age >= 65 AND mobility_score < 5 THEN '고령거동불편'
            WHEN bmi > 30 AND diabetes = 1 AND hypertension = 1 THEN '비만당뇨고혈압'
            WHEN emergency_visits > 5 THEN '응급실다방문'
            ELSE '일반환자'
        END as patient_category,
        -- 추가 복잡한 계산 로직...
    FROM patient_profiles;
    
    • 문제점
      • 많은 조건 분기로 CPU 분기 예측 실패
      • 조건별로 다른 처리 경로
        • 벡터화로 여러 데이터를 동시에 같은 처리할 수 없다.
      • 즉, 조건별로 다르게 처리해야 해서 SIMD 레지스터 활용도 저하
      • 성능 향상 : 2-4배 (일반적인 5-20배보다 낮음)


  3. 중첩 쿼리와 상관 서브쿼리

    -- 환자별 복잡한 통계 계산
    SELECT
        p.patient_id,
        p.age,
        (SELECT COUNT(*)
         FROM admissions a
         WHERE a.patient_id = p.patient_id
           AND a.admission_date >= '2023-01-01') as recent_admissions,
        (SELECT AVG(cost)
         FROM treatments t
         WHERE t.patient_id = p.patient_id) as avg_treatment_cost,
        (SELECT MAX(severity_score)
         FROM diagnoses d
         WHERE d.patient_id = p.patient_id
           AND d.diagnosis_date >= p.last_visit_date) as max_severity
    FROM patients p
    WHERE p.active = 1;
    
    • 문제점
      • 서브쿼리는 환자별로 개별 실행 필요
      • 상관 관계로 인해 배치 처리가 어렵다.
      • 메모리 액세스 패턴이 비효율적
        • 연속된 데이터가 아닌 랜덤 접근으로 캐시 효율성 저하
      • 성능 향상 : 2-5배 (JOIN 부분만 벡터화)
104Columnar SQL 문법과 최적화 기법은?⭐어려움
Columnar SQL 문법과 최적화 기법들은 기존의 RDBMS와 다르게 접근해야 합니다. 물리적 저장 방식의 차이에 의한 JOIN의 비효율성으로 Wide Table 생성이 핵심 요소이며, 이러한 Wide Table 생성 방식은 View, Materialized View, ELT 배치 방식 중에서 선택할 수 있습니다. 여러 최적화 기법 중 파티션 프루닝은 파티셔닝을 통해 불필요한 데이터를 읽지 않아 I/O를 대폭 절약할 수 있습니다. 또한 중요한 최적화 요소인 스킵 인덱스는 MinMax, Set, Bloom Filter 등이 있고 스킵 인덱스로 데이터 블록 단위로 읽을 가치가 있는지 미리 판단하여 효율적으로 스캔을 스킵할 수 있습니다. 여기에 Columnar SQL의 중요한 특징 중 하나인 조건부 집계 함수를 사용하면 단일 스캔만으로 여러 조건을 벡터화 처리하여 성능을 극대화할 수 있고, 윈도우 함수로 각 행마다 관련 행들과의 비교 분석을 하여 시계열 데이터의 이동 평균이나 변화량 계산에 특히 유용합니다. 마지막으로 SQL 최적화 팁으로는 WHERE 절에서 파티션 키를 우선 배치하고, GROUP BY는 낮은 카디널리티부터 적용하는 것이 메모리 효율성을 높이는 핵심입니다.
상세 설명

Columnar DB의 JOIN

  • 데이터 블록 저장 구조

    -- RDBMS
    사용자ID | 이름   | 나이 | 주문ID | 상품명
    1        | 김철수 | 25   | 101    | 노트북
    2        | 이영희 | 30   | 102    | 마우스
    
    -- Columnar DB
    사용자ID: [1, 2, 3, 4, 5...]
    이름: [김철수, 이영희, 박민수...]  
    나이: [25, 30, 28...]
    주문ID: [101, 102, 103...]
    상품명: [노트북, 마우스, 키보드...]
    
    • RDBMS

      데이터 블록 1 : [1,"김철수",25] [2,"이영희",30] [3,"박민수",28]
      데이터 블록 2 : [101,1,"노트북"] [102,1,"마우스"] [103,2,"키보드"]
      
      • 한 행의 모든 컬럼 데이터가 물리적으로 연속된 위치에 저장
      • 데이터 블록
        • 데이터베이스가 관리하는 논리적 단위 (여러 디스크 블록으로 구성)

    • Columnar DB

      데이터 블록 A : users.id [1,2,3,4,5...]
      데이터 블록 B : users.name ["김철수","이영희","박민수"...]
      데이터 블록 C : orders.user_id [1,1,2,3...]
      데이터 블록 D : orders.product ["노트북","마우스","키보드"...]
      
      • 같은 컬럼의 값들끼리 물리적으로 연속된 위치에 저장
      • 각 컬럼별로 다르게 압축되어 저장된다


  • JOIN 메모리 로딩 패턴의 차이

    SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id
    
    • RDBMS JOIN 처리

      • 각 블록에서 필요한 모든 컬럼 데이터가 이미 함께 있다
      • JOIN 처리 : 메모리 내에서 해시 매칭만 하면 된다.
      • 위의 예시에서는 2개 데이터 블록만 메모리에 로드
        1. users 테이블에서 행을 읽으면 ⇒ [1, "김철수", 25] 통째로 메모리에 로드
        2. orders 테이블에서 매칭되는 행 찾기 → [101, 1, "노트북"] 통째로 로드
        3. 이미 행 단위로 조합된 상태라 바로 결과 반환
    • Columnar DB JOIN 처리

      • 각 블록을 읽을 때마다 디스크 I/O 발생
      • JOIN 처리 : 여러 블록의 데이터를 조합해서 새로운 구조 생성
      • 위의 예시에서는 4개 데이터 블록을 각각 메모리에 로드해야 한다.
        1. users.id 컬럼 스캔 ⇒ 블록 A에서 [1,2,3,4...] 읽기
        2. orders.user_id 컬럼 스캔 ⇒ 블록 C에서 [1,1,2,3...] 읽기
        3. 매칭되는 위치 계산 : user_id=1인 레코드들의 위치 파악
        4. 각 컬럼에서 해당 위치의 값 추출
          • users.name 블록 B의 1번째 위치 → "김철수"
          • orders.product 블록 D의 1,2번째 위치 → "노트북", "마우스"
        5. 행 단위로 재조립 : ["김철수", "노트북"], ["김철수", "마우스"]


  • JOIN이 비효율적인 이유

    1. 캐시 지역성 문제
      • RDBMS는 한 행의 모든 데이터가 같은 페이지에 있어서 한 번 읽으면 끝
      • Columnar DB는 여러 컬럼 블록을 각각 읽어야 해서 여러 번 메모리 접근해야 한다.
    2. 압축 해제 오버헤드
      • 각 컬럼이 타입에 따라 다르게 압축된다
      • JOIN시 여러 컬럼의 압축을 동시에 풀어야 한다.
    3. 벡터화 처리 방식과 맞지 않다
      • Columnar DB는 컬럼 전체를 한번에 처리하는데 최적화
      • JOIN은 행별로 매칭하는 작업이라 벡터화 효과 감소
    4. 메모리 사용량 증가
      • 2개 ⇒ 5개 블록 읽기로 I/O 비용 증가
      • 더 많은 블록을 동시에 메모리에 유지해야 한다.

RDBMS SQL vs Columnar SQL 차이점

  • Columnar DB의 속도

    • 왜 같은 SQL인데 Columnar DB에서는 이렇게 빠를까?
      • 표면적으로는 같은 SQL이지만, Columnar Database의 데이터 저장 방식 차이로 인해 차이가 발생한다.
      • 그에 따라, 최적화 전략도 다르게 접근해야 한다
      • 기존의 RDBMS에서 정규화와 JOIN이 핵심이라면, Columnar DBWide Table과 벡터화된 집계가 핵심이다.


  • 기본 접근법의 차이

    • RDBMS 접근법

      • 트랜잭션 일관성과 정규화 중심
      • 개별 레코드 빠른 조회에 최적화
      • JOIN을 통한 데이터 조합

    • Columnar DB 접근법

      • 분석 성능과 집계 연산 중심
      • 대용량 스캔과 벡터화 처리에 최적화
      • Wide Table을 통한 JOIN 최소화


  • SQL 패턴 비교

    • RDBMS 방식 (JOIN 중심)

      SELECT h.hospital_name, COUNT(p.patient_id) as patient_count
      FROM patients p
      JOIN hospitals h ON p.hospital_id = h.id
      JOIN treatments t ON p.patient_id = t.patient_id
      WHERE t.treatment_date >= '2024-01-01'
      GROUP BY h.hospital_name;
      
      • 정규화된 테이블 간의 복잡한 JOIN 필요

    • Columnar 방식 (Wide Table 중심)

      SELECT hospital_name, COUNT(*) as patient_count
      FROM patient_treatments_wide
      WHERE treatment_date >= '2024-01-01'
      GROUP BY hospital_name;
      
      • 미리 비정규화된 Wide Table을 활용한다.

    • 핵심 차이점

      • JOIN 최소화 - 사전에 비정규화된 Wide Table 활용
      • 단순한 WHERE - 복잡한 조인 조건 대신 직접적인 필터링
      • 집계 중심 - COUNT, SUM, AVG가 주요 연산 패턴

Wide Table 생성 방식과 전략

  • Wide Table이 필요한 이유

    • 앞서 언급했듯이 Columnar DB에서는 JOIN 연산이 상대적으로 비효율적이다
    • 따라서 사전에 비정규화된 Wide Table을 만들어 분석 성능 극대화


  • Wide Table 생성 방식

    1. View 생성 방식 (논리적 Wide Table)

      -- View 생성
      CREATE VIEW patient_wide AS
      SELECT
          p.patient_id,
          h.hospital_name,
          t.treatment_cost
      FROM patients p
      INNER JOIN hospitals h ON p.hospital_id = h.id
      INNER JOIN treatments t ON p.patient_id = t.patient_id;
      
      -- 조회할 때마다
      SELECT hospital_name, count() as patient_count
      FROM patient_wide
      WHERE treatment_cost > 100000
      GROUP BY hospital_name;
      
      • View
        • 논리적 테이블로 실제 데이터를 저장하지 않는다.
        • 쿼리 실행 시마다 JOIN과 집계연산을 수행하기 때문에 항상 최신 데이터를 보장한다.
        • 매번 복잡한 연산을 수행하기에 느리다.
      • 장점 : 데이터 일관성, 저장 공간 절약
      • 단점 : 매번 JOIN 연산 발생, 분석 성능 제한


    2. Materialized View 생성 방식 (물리적 Wide Table)

      -- View 생성
      CREATE MATERIALIZED VIEW hospital_stats_mv
      ENGINE = SummingMergeTree()
      ORDER BY hospital_name
      AS SELECT
          hospital_name,
          count() as patient_count,
          sum(treatment_cost) as total_cost
      FROM patient_wide
      GROUP BY hospital_name;
      
      -- 조회할 때마다
      SELECT hospital_name, total_cost
      FROM hospital_stats_mv
      WHERE patient_count > 100
      ORDER BY total_cost DESC;
      
      • Materialized View
        • 물리적 테이블로 데이터를 실제로 디스크에 저장한다.
        • 생성 시점에 미리 계산한 결과를 저장하기 때문에 일반적으로는 주기적 갱신 필요 (REFRESH 명령어)
        • ClickHouse의 경우 자동으로 실시간 갱신 (새 데이터 INSERT시)
        • 단순 SELECT만 수행하기 때문에 빠르다
      • 장점
        • 빠른 분석 성능
        • 실시간 자동 업데이트 (ex. ClickHouse)
      • 단점
        • 저장공간 증가
        • 실시간성 제한 (ClickHouse는 실시간)


    3. ELT 배치 생성 방식

      -- 1. Extract & Load : 원본 데이터를 먼저 ClickHouse에 적재
      INSERT INTO raw_data_table SELECT * FROM external_source;
      
      -- 2. Transform : ClickHouse 내부에서 변환 및 Wide Table 생성
      INSERT INTO patient_summary
      SELECT
          p.patient_id,
          h.hospital_name,
          t.treatment_cost,
          multiIf(
              t.treatment_cost > 500000, 'High',
              t.treatment_cost > 100000, 'Medium',
              'Low'
          ) as cost_level
      FROM raw_patients p
      INNER JOIN raw_hospitals h ON p.hospital_id = h.id  
      INNER JOIN raw_treatments t ON p.patient_id = t.patient_id;
      
      -- 조회할 때
      SELECT cost_level, count() as patient_count
      FROM patient_summary
      GROUP BY cost_level;
      
      • ELT 배치 과정
        1. Extract & Load

          • 외부 DB나 파일에서 원본 데이터 추출하고 ClickHouse에 원본 그대로 적재 (변환 X)
          • raw_patients, raw_hospitals, raw_treatments 테이블에 저장
        2. Transform

          • ClickHouse 내부에서 변환을 수행하고 INSERT INTO … SELECT를 사용하여 미리 생성된 WIDE TABLE로 대용량 데이터 삽입
      • 특징
        • 원본 데이터를 먼저 Columnar DB에 적재 후 내부에서 변환
        • 주기적 배치 작업으로 Wide Table을 생성한다.
        • Columnar DB의 편리한 함수들 활용 가능
      • 장점 : 최대 성능, 복잡한 변환 가능, Columnar DB 내장 함수 사용 가능
      • 단점 : 배치 지연, 운영 복잡도


  • Wide Table 선택 기준

    • 실시간 요구사항이 높을 때

      • View 기반 ⇒ Materialized View 단계적 적용
      • 자주 조회되는 조합만 Materialized View 생성

    • 분석 성능이 최우선일 때

      • ELT 배치로 완전한 Wide Table 생성
      • 시간대별, 부서별 등 다양한 집계 레벨 사전 계산

    • 저장 비용을 고려해야 할 때

      • 핵심 분석만 Materialized View
      • 나머지는 View로 유연성 확보

파티셔닝파티션 프루닝

  • 파티셔닝의 핵심 개념

    • Columnar DB에서 파티셔닝물리적 데이터 분할을 통해 쿼리 성능을 대폭 향상시킬 수 있다.


    • 시간 기반 파티셔닝 (가장 일반적)

      -- ClickHouse 예시
      CREATE TABLE medical_records (
          patient_id Int32,
          record_date Date,
          diagnosis_code String
      )
      PARTITION BY toYYYYMM(record_date)  -- 월별 파티션
      ORDER BY (patient_id, record_date);
      

    • 그 외 파티셔닝

      • 범위 기반 파티셔닝 (나이대별, 매출 구간별)
      • 해시 파티셔닝 - 환자 ID 해시값으로 균등 분할
      • 복합 파티셔닝 - 지역 + 시간 조합


  • 파티션 프루닝 (Partition Pruning)

    • 파티션 프루닝은 불필요한 파티션을 읽지 않는 최적화 기법

    • 동작 원리

      -- 전체 테이블: 12개 월별 파티션
      SELECT * FROM medical_records WHERE record_date = '2024-03-15';
      
      • 파티션 프루닝 동작
        • ✅ 2024-03 파티션만 스캔 (1/12만 읽음)
        • ❌ 나머지 11개 파티션 완전 무시

    • 효과

      • 1년치 데이터 중 1개월만 읽음 ⇒ 12배 I/O 절약

      • 파티션별 병렬 처리 가능

        SELECT department, COUNT(*) 
        FROM records 
        WHERE record_date BETWEEN '2024-01-01' AND '2024-03-31'
        GROUP BY department;
        
        • 각 파티션을 독립적으로 동시 처리
        • Thread 1 : 2024-01 파티션, Thread 2 : 2024-02 파티션, Thread 3 : 2024-03 파티션 처리 후 결과 병합
      • 파티션별 압축률 최적화
        2023년 파티션: 오래된 데이터 → 더 강한 압축 (ZSTD)
        2024년 파티션: 최신 데이터 → 빠른 압축 (LZ4)
        서울 파티션: 다양한 데이터 → Dictionary 압축
        지방 파티션: 반복 데이터 → RLE 압축
        
        • 각 파티션의 데이터 특성에 맞는 압축 방식 적용


  • 파티셔닝 설계 원칙

    1. 쿼리 패턴 기반 설계

      WHERE record_date >= '2024-01-01'     -- 시간 범위
      AND hospital_id = 5                   -- 병원별 필터링
      AND department = 'CARDIOLOGY'         -- 진료과별 분석
      
      -- 적절한 파티셔닝 전략
      PARTITION BY (toYYYYMM(record_date), hospital_id)
      
      • 자주 사용되는 쿼리 패턴에 기반하여 적절한 파티셔닝 전략을 설정한다.

    2. 파티션 크기 고려

      • 과도한 세분화

        -- 일별 파티션
        PARTITION BY record_date  -- 365개 파티션/년
        
        • 메타데이터 오버헤드 - 365개 파티션 정보의 관리 부담
        • 소규모 파일들 - 각 파티션이 너무 작아서 I/O 효율성 저하

      • 너무 큰 단위

        PARTITION BY toYear(record_date)  -- 1개 파티션/년
        
        • 파티션 프루닝 효과 감소 - 월별 쿼리도 1년치 전체를 읽어야 한다.
        • 파티션이 너무 커서 파티션별 병렬 처리의 이점을 못 살린다.

      • 최적점은 보통 월별(toYYYYMM) 파티셔닝

스킵 인덱스 종류 ⭐

  • 스킵 인덱스의 핵심 개념

    • 스킵 인덱스데이터 블록 단위읽을 가치가 있는지 미리 판단하는 기술
    • 목적
      • 불필요한 데이터 블록을 빠르게 스킵하여 쿼리 성능 대폭 향상
    • 원리
      • 각 블록의 요약 정보를 미리 저장하여 조건에 맞지 않는 블록은 읽지 않는다


  • MinMax 인덱스

    • 설정

      CREATE TABLE patients (
          patient_id Int32,
          age Int32,
          hospital_id Int32,
          INDEX idx_age age TYPE minmax GRANULARITY 3
      ) ENGINE = MergeTree()
      ORDER BY patient_id;
      

    • 동작 원리

      1. 각 데이터 블록별로 최솟값과 최댓값 저장

        데이터 블록 1: age 범위 [25, 45]
        데이터 블록 2: age 범위 [60, 85]
        데이터 블록 3: age 범위 [30, 50]
        
      2. 조회 쿼리

        SELECT * FROM patients WHERE age = 70;
        
        • 데이터 블록 1, 3은 스킵
        • 데이터 블록 2만 읽기
    • 특징

      • 범위 조건에 최적화 (>, <, BETWEEN 등)
      • 숫자, 날짜 데이터에 효과적
      • 메모리 사용량 매우 적음 (블록당 2개 값만 저장)


  • Set 인덱스

    • 설정

      CREATE TABLE patients (
          patient_id Int32,
          department String,
          status String,
          INDEX idx_dept department TYPE set(100) GRANULARITY 2
      ) ENGINE = MergeTree()
      ORDER BY patient_id;
      

    • 동작 원리

      1. 각 데이터 블록별로 고유값 집합 저장

        데이터 블록 1: departments = {CARDIOLOGY, NEUROLOGY}
        데이터 블록 2: departments = {ONCOLOGY, PEDIATRICS}
        데이터 블록 3: departments = {CARDIOLOGY, ORTHOPEDICS}
        
      2. 조회 쿼리

        SELECT * FROM patients WHERE department = 'CARDIOLOGY';
        
        • 데이터 블록 2은 스킵
        • 데이터 블록 1, 3만 읽기
    • 특징

      • 정확한 값 매칭에 최적화 (= 조건)

      • 카디널리티가 낮은 컬럼에 효과적 (부서, 상태, 등급 등)

      • IN 조건에도 유용

        (WHERE department IN ('CARDIOLOGY', 'NEUROLOGY'))

      • 메모리 사용량은 고유값 개수에 비례


  • 블룸 필터 (Bloom Filter)

    • 설정

      CREATE TABLE patients (
          patient_id Int32,
          phone String,
          email String,
          INDEX idx_phone phone TYPE bloom_filter(0.01) GRANULARITY 1,
          INDEX idx_email email TYPE bloom_filter() GRANULARITY 1
      ) ENGINE = MergeTree()
      ORDER BY patient_id;
      

    • 동작 원리

      1. 데이터 저장 시 블룸 필터 생성

        • 데이터 블록 1 환자 ID들 : [1001, 1005, 1023, 1045]

        • 각 ID를 여러 해시 함수로 변환

          hash1(1001) = 3, hash2(1001) = 7, hash3(1001) = 12
          hash1(1005) = 5, hash2(1005) = 9, hash3(1005) = 15
          
        • 해당 위치의 비트를 1로 설정

          블룸 필터: [0,0,0,1,0,1,0,1,0,1,0,0,1,0,0,1,...]
          
      2. 조회 쿼리

        SELECT * FROM patients WHERE phone = '010-1234-5678';
        
        • hash1(phone) = 4, hash2(phone) = 11, hash3(phone) = 20
        • 블룸 필터의 4, 11, 20번째 비트 확인
        • 모두 1이면"있을 수도 있음"블록 읽기
        • 하나라도 0이면"확실히 없음"블록 스킵
    • 특징

      • False Negative 없음
        • "없다"고 하면 100% 없음
        • 즉, 빠르게 없다는 것을 확정하고 스킵할 수 있는 것이다.
      • False Positive 가능
        • "있다"고 해도 실제론 없을 수 있음 (보통 1-5%)
        • 해시 함수 개수가 많을수록 False Positive 감소하지만 메모리 사용량 증가
        • 비트맵 크기가 클수록 정확도 향상
        • ClickHouse 설정(bloom_filter(0.01)) ⇒ 1% False Positive Rate
      • 메모리 효율적 (실제 데이터 대신 비트맵만 저장)
      • 카디널리티가 높은 컬럼에 효과적 (사용자 ID, 이메일, 전화번호 등)
      • 문자열 검색에도 유용 (LIKE 조건)

벡터화된 집계 함수, 분위수윈도우 함수

  • 집계 함수와 윈도우 함수

    • 집계 함수 (Aggregate Functions)

      • 여러 행을 하나의 결과로 집약하는 함수
      • GROUP BY와 함께 사용하여 그룹별 집계
      • 예 : COUNT(), SUM(), AVG(), MAX(), MIN()

    • 윈도우 함수 (Window Functions)

      • 각 행에 대해 관련된 행들의 집합을 기준으로 계산하는 함수
      • OVER 절을 사용하여 계산 범위 지정
      • 예 : ROW_NUMBER(), RANK(), LAG(), LEAD()


  • 조건부 집계 함수

    • Columnar DB조건부 집계 함수가장 강력한 기능 중 하나이다.

    • 종류

      • countIf(조건) - 조건을 만족하는 행의 개수
      • sumIf(컬럼, 조건) - 조건을 만족하는 행들의 합계
      • avgIf(컬럼, 조건) - 조건을 만족하는 행들의 평균
      • maxIf(컬럼, 조건) - 조건을 만족하는 행들의 최댓값
      • minIf(컬럼, 조건) - 조건을 만족하는 행들의 최솟값

    • 성능 이점

      SELECT
          hospital_name,
          count() as total_patients,
          countIf(age >= 65) as elderly_patients,
          countIf(gender = 'F') as female_patients,
          avgIf(treatment_cost, insurance_type = 'PREMIUM') as premium_avg_cost,
          sumIf(length_of_stay, admission_type = 'EMERGENCY') as emergency_days
      FROM patient_treatments
      GROUP BY hospital_name;
      
      • 단일 스캔
        • 여러 조건을 한 번에 처리
        • 일반 집계로 여러 번 처리할 것을 단일 스캔으로 모든 조건 한번에 벡터화 처리
      • 메모리 효율
        • 중간 결과 테이블 생성 불필요


  • 분위수와 통계 함수

    • 분위수(Quantile)란?

      • 데이터를 크기 순으로 나열했을 때 특정 위치의 값
      • quantile(0.5) = 중간값(median)
      • quantile(0.95) = 95% 지점 값

    • 데이터 분석에 필수적인 통계 함수들

      SELECT
          department,
          avg(treatment_cost) as avg_cost,
          stddev(treatment_cost) as cost_stddev,
          quantile(0.5)(treatment_cost) as median_cost,
          quantile(0.95)(treatment_cost) as p95_cost,
          quantiles(0.25, 0.5, 0.75)(length_of_stay) as stay_quartiles
      FROM treatments
      GROUP BY department;
      


  • 윈도우 함수

    • 윈도우 함수 목적

      • 각 행마다 다른 행들과 비교/분석하기 위해 사용
      • 집계 함수 : 여러 행 ⇒ 1개 요약 결과
      • 윈도우 함수 : 여러 행 ⇒ 각 행마다 개별 결과

    • 윈도우 함수 구성요소

      • OVER - 윈도우 함수임을 나타내는 키워드
      • PARTITION BY - 어떤 기준으로 그룹을 나눌지
      • ORDER BY - 어떤 순서로 정렬할지
      • ROWS BETWEEN - 몇 개 행까지 포함할지

    • 시계열 분석 패턴

      SELECT
          patient_id,
          measurement_date,
          blood_pressure,
      
          -- 이동 평균 (벡터화 처리)
          avg(blood_pressure) OVER (
              PARTITION BY patient_id
              ORDER BY measurement_date
              ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
          ) as moving_avg_7d,
      
          -- 변화량 계산
          blood_pressure - lag(blood_pressure) OVER (
              PARTITION BY patient_id
              ORDER BY measurement_date
          ) as bp_change
      
      FROM patient_vitals
      ORDER BY patient_id, measurement_date;
      
      

    • Columnar DB에서 윈도우 함수가 빠른 이유

      • PARTITION BY, ORDER BY 시 같은 타입 데이터끼리 정렬
      • LAG, LEAD 등의 오프셋 계산을 벡터화 처리(배치 처리)
      • 윈도우 범위 내 데이터가 메모리에서 가까이 위치

Columnar DB WHERE / GROUP BY 최적화 팁

  1. WHERE 절 최적화

    • 파티션 키 우선 배치
      • WHERE 절에서 먼저 파티션 프루닝이 발생하여 스킵될 수 있도록 해야 한다.

      • 효율적 (파티션 키 먼저)

        WHERE treatment_date >= '2024-01-01'    -- 파티션 키
          AND hospital_id = 5                   -- 인덱스 키
          AND department = 'CARDIOLOGY'         -- 일반 필터
        
      • 비효율적 (파티션 키가 뒤에)

        WHERE department = 'CARDIOLOGY'
          AND hospital_id = 5
          AND treatment_date >= '2024-01-01'
        


  2. GROUP BY 최적화

    • 낮은 카디널리티 부터 GROUP BY 적용하기

      GROUP BY hospital_region,     -- 10개 정도
              hospital_name,       -- 100개 정도
              department_name      -- 50개 정도
      
      • 메모리 사용량 최적화
      • 집계 효율성 향상
105주요 Columnar DB 제품별 특징은 무엇인가?쉬움
주요 Columnar Database 제품들은 각기 차별성 있는 특징을 가지고 있습니다. ClickHouse는 오픈소스로 뛰어난 성능과 비용 효율성을 제공하고, BigQuery는 서버리스 환경에서 페타바이트급 처리가 가능합니다. Redshift는 AWS 생태계와의 완벽한 통합을 제공하는 장점이 있습니다. 의료 데이터 환경에서는 데이터 규모, 실시간성, 보안, 기존 인프라와의 호환성 등을 종합적으로 고려하여 최적의 제품을 선택해야 합니다.
상세 설명

ClickHouse - 고성능 오픈소스 솔루션

  • 핵심 특징

    • 오픈소스 - 무료 사용, 커스터마이징 가능
    • 뛰어난 성능 - 단일 서버에서도 초당 수십억 건 처리
    • SQL 호환성 - 표준 SQL과 높은 호환성
    • 압축률 - 10-100배 압축으로 저장 비용 절약


  • 장단점

    • 장점
      • 비용 효율성 - 라이선스 비용 없음, 하드웨어 최적화
      • 빠른 응답속도 - 복잡한 집계 쿼리도 초 단위 처리
      • 유연한 배포 - 온프레미스, 클라우드, 하이브리드 모두 지원
      • 확장성 - 수평 확장으로 페타바이트급 처리 가능
    • 단점
      • 학습 곡선 - 전문 지식 필요, 운영 복잡도 높음
      • 관리 부담 - 클러스터 관리, 백업, 모니터링 직접 구현
      • 트랜잭션 제한 - ACID 트랜잭션 미지원
      • 생태계 - 상대적으로 작은 툴 생태계


  • 적합한 경우 및 활용 예시

    • 적합한 경우

      • 비용에 민감한 스타트업이나 중소병원
      • 온프레미스 환경이 필요한 경우
      • 개발팀의 기술 역량이 충분한 경우
      • 실시간 분석이 핵심인 시스템

    • 의료 데이터 활용 예시

      • 실시간 환자 바이탈 사인 분석

        CREATE TABLE patient_vitals (
            patient_id Int32,
            measurement_time DateTime,
            heart_rate Int16,
            blood_pressure String,
            temperature Float32
        ) ENGINE = MergeTree()
        ORDER BY (patient_id, measurement_time);
        
      • 병원별 시간당 응급실 현황

        SELECT
            hospital_id,
            toStartOfHour(admission_time) as hour,
            COUNT(*) as patient_count,
            AVG(triage_score) as avg_severity
        FROM emergency_admissions
        WHERE admission_time >= now() - INTERVAL 24 HOUR
        GROUP BY hospital_id, hour
        ORDER BY hospital_id, hour;
        

BigQuery - 서버리스 페타바이트 처리

  • 핵심 특징
    • 서버리스 - 인프라 관리 불필요, 자동 스케일링
    • 페타바이트급 - 자동 확장, 대용량 데이터 처리
    • ML 통합 - BigQuery ML로 데이터에서 바로 머신러닝
    • 실시간 스트리밍 - Pub/Sub 연동으로 실시간 수집


  • 장단점
    • 장점

      • 운영 부담 최소화
        • 서버 관리, 백업, 스케일링 자동화
        • 모니터링, 장애 복구, 성능 최적화, 보안 업데이트
      • 자동 확장 (자동 리소스 할당)
        • 페타바이트급 데이터에서도 일관된 성능
        • 데이터가 늘어도 사용자가 별도 설정 없이 비슷한 응답 시간으로 쿼리 가능
      • GCP 생태계
        • Dataflow, AI Platform과 완벽 연동
      • 표준 SQL - ANSI SQL 표준 완벽 지원
        • 기존 PostgreSQL/MySQL 쿼리를 거의 수정 없이 그대로 사용 가능

    • 단점

      • 비용 예측 어려움 - 쿼리량에 따른 가변 비용
      • 데이터 이동 비용 - 외부 데이터 수집 시 네트워크 비용
      • 실시간 조회 제한 - 스트리밍 인서트 레이턴시 (들어온 데이터를 바로 조회 불가능)
      • 벤더 종속 - Google Cloud에 강한 의존성


  • 적합한 경우 및 활용 예시
    • 적합한 경우

      • 대규모 의료 연구기관이나 제약회사
      • ML/AI 분석이 중요한 프로젝트
      • 운영 리소스(인력, 지식 등)가 제한적인 조직
        • 개발자가 할 일은 SQL 쿼리 작성만 하면 된다.
      • 데이터 크기를 예측하기 어려운 경우
        • 다른 DB는 미리 예측하고 수동 확장 작업 필요
        • BigQuery는 미리 계획을 세울 필요 없이 자동 처리한다.

    • 의료 데이터 활용 예시

      • BigQuery ML을 활용한 환자 재입원 위험 예측

        CREATE OR REPLACE MODEL `medical_db.readmission_model`
        OPTIONS(model_type='logistic_reg') AS
        SELECT
          age,
          length_of_stay,
          num_diagnoses,
          num_medications,
          IF(readmitted_within_30_days = 'YES', 1, 0) as label
        FROM `medical_db.patient_admissions`
        WHERE admission_date < '2024-01-01';
        
      • 예측 실행

        SELECT
          patient_id,
          predicted_label as readmission_risk,
          predicted_label_probs[OFFSET(1)].prob as risk_probability
        FROM ML.PREDICT(MODEL `medical_db.readmission_model`,
          (SELECT patient_id, age, length_of_stay, num_diagnoses, num_medications
          FROM `medical_db.current_patients`));
        
        • ML 전용 함수
          • FROM ML.PREDICT(MODEL medical_db.readmission_model, ...)

Redshift - AWS 생태계 통합

  • 핵심 특징
    • AWS 네이티브 - AWS 서비스와 완벽한 통합
    • PostgreSQL 호환 - PostgreSQL와 쉬운 연동
    • Serverless - 관리형과 서버리스 옵션 제공
    • 예측 가능한 비용 - 시간당 고정 요금제


  • 장단점
    • 장점
      • AWS 생태계 - S3, Lambda, EMR과 완벽 연동
      • 기존 도구 호환 - Tableau, Power BI 등 BI 툴 쉬운 연결
      • 관리 편의성 - AWS 콘솔에서 통합 관리
      • 예측 가능한 비용 - 인스턴스 기반 명확한 요금제
    • 단점
      • AWS 종속 - 멀티 클라우드 전략에 제약
      • 성능 한계 - ClickHouse 대비 상대적으로 느린 처리 속도
      • 복잡한 최적화 - 디스트 키, 정렬 키 설정 필요
      • 동시성 제한 - 많은 사용자 동시 접속 시 성능 저하


  • 적합한 경우 및 활용 예시
    • 적합한 경우

      • 기존 AWS 인프라를 활용 중인 조직
      • PostgreSQL 경험이 있는 개발팀
      • 예측 가능한 비용 구조를 선호하는 경우
      • 기존 BI 도구와의 연동이 중요한 경우

    • 의료 데이터 활용 예시

      • Redshift Spectrum으로 S3 데이터 직접 쿼리

        CREATE EXTERNAL SCHEMA medical_data_lake
        FROM DATA CATALOG
        DATABASE 'medical_db'
        IAM_ROLE 'arn:aws:iam::account:role/RedshiftRole';
        
      • S3의 의료 이미지 메타데이터와 Redshift 환자 데이터 조인

        SELECT
            p.patient_id,
            p.patient_name,
            img.study_date,
            img.modality,
            COUNT(*) as image_count
        FROM patients p
        JOIN medical_data_lake.dicom_metadata img
            ON p.patient_id = img.patient_id
        WHERE img.study_date >= '2024-01-01'
        GROUP BY p.patient_id, p.patient_name, img.study_date, img.modality;
        

제품별 상세 비교

  • 비교표
    항목ClickHouseBigQueryRedshift
    라이선스오픈소스 (무료)사용량 기반 과금시간당 고정 요금
    설치/관리직접 설치/관리완전 관리형관리형 서비스
    쿼리 성능매우 높음높음중간
    압축률10-100배5-20배3-10배
    실시간 처리우수제한적제한적
    ML 통합제한적네이티브 지원기본적
    SQL 호환성높음표준 SQLPostgreSQL
    확장 한계수평 확장자동 확장클러스터 크기 제한

각 제품별 보안/규제 준수

  • HIPAA란?

    • Health Insurance Portability and Accountability Act
    • 환자의 의료정보를 보호하기 위한 규제


  • HIPAA 규제 준수

    • ClickHouse

      • 직접 구현 필요 : HIPAA 준수를 위한 보안 설정 직접 구현

      • 암호화 : 디스크 암호화, 전송 중 암호화 수동 설정

      • 접근 제어 : RBAC(Role-Based Access Control) 직접 구성

      • 감사 로그 : 접근 로그, 쿼리 로그 수동 설정 및 관리

      • 보안 설정 예시

        CREATE USER medical_analyst
        IDENTIFIED BY 'secure_password'
        SETTINGS readonly = 1;
        GRANT SELECT ON medical.patients TO medical_analyst;
        REVOKE ALL ON medical.sensitive_data FROM medical_analyst;
        

    • BigQuery

      • 기본 제공 : Google Cloud의 HIPAA BAA(Business Associate Agreement) 지원
      • 자동 암호화 : 저장 시/전송 중 암료화 기본 제공
      • IAM 통합 : Google Cloud IAM으로 세밀한 권한 관리
      • 감사 로그 : Cloud Audit Logs 자동 생성
      • BigQuery 권한 관리 예시
        • Google Cloud IAM에서 설정
        • BigQuery Data Viewer : 읽기 전용
        • BigQuery Data Editor : 데이터 편집
        • BigQuery Admin : 전체 관리

    • Redshift

      • AWS 규제 준수 : AWS HIPAA 규제 준수 지원
      • VPC 격리 : Virtual Private Cloud 내 배포
      • 암호화 : KMS 키 관리 서비스 연동
      • 네트워크 보안 : VPC 엔드포인트, 보안 그룹 설정


  • 개인정보보호 대응

    • 데이터 익명화 기능
      • ClickHouse

        SELECT substring(MD5(patient_id), 1, 8) as anonymized_id, age_group, diagnosis_code
        FROM patients;
        
        • 수동 익명화 구현 필요

      • BigQuery

        SELECT FARM_FINGERPRINT(CAST(patient_id AS STRING)) as anonymized_id, age_group, diagnosis_code
        FROM `medical.patients`;
        
        • BigQuery DLP API 연동

의료 환경별 최적 제품 선택 기준

  • 중소 병원 (환자 수 < 10만명)

    • 권장 : ClickHouse

      CREATE TABLE daily_patient_stats (
          date Date,
          department String,
          patient_count Int32,
          avg_waiting_time Float32
      ) ENGINE = MergeTree()
      ORDER BY (date, department);
      
      -- 실시간 대시보드 쿼리
      SELECT department,
            COUNT(*) as current_patients,
            AVG(waiting_time) as avg_wait
      FROM patient_queue
      WHERE status = 'waiting'
      GROUP BY department;
      
      • 소규모 환경에 적합한 단순 구성
      • 단일 서버로도 충분한 성능

    • 선택 이유

      • 적은 라이선스 비용으로 시작 가능
      • 단일 서버로도 충분한 성능
      • 필요에 따라 점진적 확장 가능


  • 대형 병원 (환자 수 > 100만명)

    • 권장 : BigQuery 또는 Redshift

      SELECT
          EXTRACT(YEAR FROM admission_date) as year,
          primary_diagnosis,
          COUNT(*) as patient_count,
          AVG(length_of_stay) as avg_los,
          PERCENTILE_CONT(total_cost, 0.5) OVER() as median_cost
      FROM `hospital.admissions`
      WHERE admission_date >= '2020-01-01'
      GROUP BY year, primary_diagnosis
      HAVING patient_count > 1000
      ORDER BY year, patient_count DESC;
      
      • 대용량 환자 데이터 분석

    • 선택 이유

      • 페타바이트급 데이터 처리 가능
      • 인프라 관리 부담 최소화
      • 고급 분석과 ML 기능 활용


  • 연구 기관 (다기관 데이터)

    • 권장 : BigQuery + ClickHouse 하이브리드

      -- BigQuery에서 연구용 데이터셋 생성
      CREATE TABLE `research.patient_cohort` AS
      SELECT patient_id, age, gender, primary_diagnosis,
            outcome_30_day, outcome_90_day
      FROM `hospital_data.admissions`
      WHERE study_enrollment_date BETWEEN '2023-01-01' AND '2024-12-31'
        AND consent_for_research = TRUE;
      
      -- ClickHouse에서 실시간 모니터링
      SELECT toStartOfHour(created_time) as hour,
            COUNT(*) as new_enrollments,
            countIf(primary_diagnosis LIKE '%diabetes%') as diabetes_cases
      FROM research_enrollment
      WHERE created_time >= now() - INTERVAL 24 HOUR
      GROUP BY hour;
      
      • BigQuery : 대규모 연구 데이터 저장/분석
      • ClickHouse : 실시간 대시보드와 빠른 쿼리

제품 도입 시 고려사항

  • 비용 최적화 전략

    • ClickHouse

      • 파티셔닝으로 오래된 데이터 자동 삭제

        ALTER TABLE patient_logs
        DROP PARTITION '2023-01';  -- 오래된 파티션 삭제로 비용 절약
        
      • 압축 설정 최적화

        CREATE TABLE optimized_vitals (
            patient_id Int32 CODEC(Delta),
            timestamp DateTime CODEC(Delta),
            heart_rate Int16 CODEC(T64, LZ4)
        ) ENGINE = MergeTree()
        ORDER BY (patient_id, timestamp);
        

    • BigQuery

      • 파티셔닝과 클러스터링으로 스캔 비용 최소화

        CREATE TABLE `medical.patient_visits`
        PARTITION BY DATE(visit_date)
        CLUSTER BY patient_id, hospital_id
        AS SELECT * FROM source_table;
        
      • 쿼리 비용 미리 확인

        SELECT
          COUNT(*)
        FROM `medical.patient_visits`
        WHERE visit_date = '2024-01-15';
        
        • 이 쿼리는 약 10GB 스캔 예상 ($0.05)


  • 성능 벤치마크 예시

    • 동일한 쿼리로 성능 비교

      -- 1억 건 환자 데이터에서 병원별 월간 통계
      SELECT
          hospital_id,
          DATE_TRUNC('month', admission_date) as month,
          COUNT(*) as admissions,
          AVG(length_of_stay) as avg_los
      FROM patient_admissions
      WHERE admission_date >= '2023-01-01'
      GROUP BY hospital_id, month
      ORDER BY hospital_id, month;