Snowflake MigrationClickHouse Workshops
계획 워크시트

워크시트 3: 스키마 변환

TRIPS_RAW와 FACT_TRIPS의 모든 컬럼을 ClickHouse 타입에 매핑하고 일곱 개의 Snowflake 표현식을 변환한다. 모든 답에 즉시 피드백이 제공된다.

예상 소요 시간: 20–25분 참고 자료: Snowflake vs ClickHouse — Section 2 (SQL Dialect Gaps)

개념

타입 매핑과 함수 변환은 마이그레이션에서 가장 기계적인 작업이지만, 부주의하게 하면 가장 오류가 나기 쉬운 작업이기도 하다. Snowflake와 ClickHouse는 서로 다른 의미론을 가진 서로 다른 타입 체계를 쓰며, 잘못된 타입을 쓰면 조용한 정밀도 손실, 과도한 스토리지 사용, 또는 깨진 쿼리 로직이 생긴다.

핵심 원칙:

  1. 정밀도를 명시하라. Snowflake의 TIMESTAMP_NTZ(9)는 나노초 정밀도를 갖는다. ClickHouse의 DateTime은 초 정밀도뿐이므로 버전 컬럼에 쓰지 마라. 밀리초 정밀도(대부분의 실제 요구 사항에 해당)에는 DateTime64(3, 'UTC'), 나노초에는 DateTime64(9, 'UTC')를 사용하라. 이는 정확성 문제다: ReplacingMergeTree 버전 컬럼이 초 정밀도뿐이면 같은 초 안에 도착한 두 업데이트는 비결정적이다 — ClickHouse가 어느 쪽이 더 최신인지 판단할 수 없다.

  2. 가장 작은 올바른 정수 타입을 사용하라. Snowflake의 INTEGER는 NUMBER(38, 0)이다 — 128비트 값으로 저장되는 38자리 고정 정밀도다. ClickHouse에는 고정 폭 정수 타입이 있다: Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64. vendor_id(값 1–3)에 UInt8을 선택하면 Int64에 비해 행마다 7바이트를 절약한다. 5천만 행이면 350MB다.

  3. VARIANT → String. ClickHouse에는 네이티브 JSON 타입이 있지만(v25.3+에서 프로덕션 안정), 이 타입은 테이블 생성 시점에 필드 이름과 구조를 알 수 없는 진정한 동적 스키마를 위해 설계된 것이다. 이 랩의 trip_metadata는 구조가 알려져 있으므로 (driver.rating, app.surge_multiplier 등) 더 나은 접근은 마이그레이션 중에 타입이 있는 컬럼으로 미리 평탄화하거나, String으로 저장하고 쿼리 시점에 JSONExtract*를 쓰는 것이다. 스키마를 정말로 예측할 수 없을 때 JSON 타입을 쓰라: 예를 들어 이벤트마다 필드가 다른 임의의 고객 이벤트 페이로드를 수집하는 경우다. ("타입이 있는 컬럼으로 미리 평탄화"란 ETL 중에 필드를 별도의 최상위 컬럼으로 추출하는 것을 뜻하며 — FACT_TRIPS.driver_rating이 trip_metadata에서 만들어지는 방식 — JSON 블롭 자체를 Tuple로 감싸는 것이 아니다. Tuple 컬럼은 여전히 고정된 하나의 필드 집합에 매이므로, 어떤 트립의 메타데이터가 그 형태와 맞지 않는 순간 깨진다.)

  4. 부동소수점 정밀도. Snowflake의 FLOAT은 ClickHouse에서 Float64로 매핑된다. 정확한 십진 연산이 필요한 금액에는 Decimal(18, 2)을 쓰되, 이 랩에서는 소스와 일치시키는 데 Float64로 충분하다. (이것은 기본값이고, 범위와 무관하게 모든 FLOAT 컬럼이 Float64를 취해야 한다는 규칙이 아니다. 값이 소수점 한 자리로 1.0–5.0 범위인 driver_rating 같은 컬럼은 Float32의 약 7자리 유효 숫자에 여유롭게 들어간다 — 그 컬럼의 선택은 규칙 4가 금액에 대해 보호하려는 정밀도가 아니라 널 허용 여부에 달려 있다.)

  5. LowCardinality() — ClickHouse에만 있는 최적화. 타입을 LowCardinality(String)(또는 LowCardinality(UInt8) 등)으로 감싸면 ClickHouse가 해당 컬럼에 딕셔너리 인코딩을 쓰도록 지시한다 — 값이 반복되는 문자열이 아니라 딕셔너리를 가리키는 정수 참조로 저장된다. 서로 다른 값이 약 1만 개 미만인 문자열 컬럼에서 보통 2–5배의 압축 개선과 더 빠른 GROUP BY를 얻는다. Snowflake에는 대응물이 없다. 자동으로 처리하기 때문이다. 이 랩에서 좋은 후보는 pickup_borough(6개 값), payment_type(6개 값), vehicle_type, vendor_name이다.

연습: TRIPS_RAW의 타입 매핑

NYC_TAXI_DB.RAW.TRIPS_RAW의 각 컬럼을 ClickHouse 타입에 매핑하라. TRIP_ID는 예시로 채워져 있다. VARCHAR(36)에서 마이그레이션할 때는 String이 관용적이다 — 캐스팅이 필요 없고, 모든 문자열 함수를 지원하며, ClickHouse에 네이티브 UUID 타입이 있음에도 삽입 시 UUID 파싱 오버헤드를 피할 수 있다.

연습: FACT_TRIPS의 타입 매핑

FACT_TRIPS에는 dbt 파이프라인이 추가한 계산/파생 컬럼이 더 있다. 대부분의 컬럼은 TRIPS_RAW의 결정을 반복하며, DRIVER_RATING과 UPDATED_AT이 새로 등장한다.

DRIVER_RATING은 자주 NULL이다(평점이 없는 경우). ClickHouse에서 Nullable(Float64)는 널 허용이 아닌 컬럼에 비해 약간의 성능 오버헤드가 있다 — 어떤 행이 널인지 추적하기 위해 데이터와 함께 별도의 비트마스크가 저장된다. 이 컬럼의 선택은 Nullable(Float32)(명시적인 널 의미론)과 -1.0 같은 센티널 값을 쓰는 순수 Float32(더 빠르지만 덜 관용적) 사이의 문제다. 이 랩은 정확성을 위해 Nullable(Float32)를 사용한다.

연습: 함수 변환

각 Snowflake 표현식을 ClickHouse 등가물로 변환하라. 이들은 01-setup-snowflake/queries/의 Q1–Q7에서 그대로 가져온 것이다. 여덟 개 중 세 개 — QUALIFY, MERGE INTO, 그리고 CDC 스트림 읽기 — 는 한 줄 표현식으로 답할 수 없으므로 표 아래의 문제로 다룬다.

회고 문제

위의 표들을 채웠으면 다음 문제를 풀어라. 세 개는 답하는 데 한 줄 이상이 필요했던 연습 3의 변환에서 왔고, 세 개는 원본 워크시트의 "Non-Obvious Translation Decisions"다 — 위에서 TRIP_METADATA, FARE_AMOUNT, PICKUP_LOCATION_ID의 타입을 그렇게 선택한 이유다.

Loading worksheet...

migration-plan.md로 옮기기

타입 결정과 자명하지 않은 변환 노트를 migration-plan.md의 Section 5에 복사하고 다음을 체크하라.

- [ ] Schema translation: completed

이 페이지의 내용

Track your progress?

Optional. We email a link to confirm your address; progress records once you open it.

Please use your work email address, not a personal one.

Progress tracking also requires accepting the current Terms of Service in Privacy settings.

KO