다중 파일·스키마 진화
glob, filename virtual column, union_by_name, 스키마 drift 감지와 파일 manifest 운영 패턴을 다룹니다.
핵심 요약
- glob과 read_parquet 배열로 다중 파일을 읽고, filename virtual column(DuckDB 1.3.0 이후 Parquet 기본)으로 파일 단위 품질 문제를 추적합니다.
- 스키마가 다른 파일은 union_by_name=true로 없는 컬럼을 NULL로 채워 drift 조사에 쓰되, 운영 mart의 암묵적 표준으로 삼지 말고 컬럼 의도를 가려줄 registry를 둡니다.
- expected schema 테이블과 parquet_schema()를 LEFT JOIN해 required 컬럼 누락·타입 불일치를 감지합니다.
- glob은 재현성이 약하므로 중요한 batch는 glob 결과를 manifest table로 고정하고 재실행 시 같은 manifest를 사용합니다.
- drift 대응은 nullable 추가는 registry 업데이트, required 누락은 batch fail, 타입 축소는 producer 수정 전까지 quarantine으로 나눕니다.
현실의 데이터 레이크는 깨끗한 파일 하나로 끝나지 않습니다. 날짜별, tenant별, job별 파일이 뒤섞이고, 과거 파일에는 새 컬럼이 빠져 있거나 타입이 다르기도 합니다. DuckDB의 multi-file read로 이 문제를 빠르게 들여다볼 수는 있지만, 운영 단계에서는 manifest와 schema contract가 따로 필요합니다.
다중 파일 읽기
SELECT *
FROM 'raw/events/**/*.parquet';
SELECT *
FROM read_parquet([
'raw/events/2026-05-12/*.parquet',
'raw/events/2026-05-13/*.parquet'
]);파일 단위로 품질 문제를 추적할 때는 filename virtual column이 중요합니다. DuckDB 1.3.0 이후 Parquet read에서는 별도 설정 없이 바로 쓸 수 있습니다.
SELECT
filename,
count(*) AS rows,
min(event_time) AS min_event_time,
max(event_time) AS max_event_time
FROM read_parquet('raw/events/**/*.parquet')
GROUP BY filename
ORDER BY filename;union_by_name
스키마가 다른 파일을 읽을 때는 union_by_name = true를 사용합니다. 없는 컬럼은 NULL로 채워집니다.
SELECT *
FROM read_parquet('raw/events/*.parquet', union_by_name = true);drift를 조사할 때는 유용하지만, 이 옵션이 운영 mart의 암묵적 표준으로 굳어지면 곤란합니다. 새 컬럼이 의도된 것인지 provider 오류인지 가려줄 registry를 따로 둬야 합니다.
Schema contract 테이블
CREATE OR REPLACE TABLE expected_event_schema AS
SELECT *
FROM (VALUES
('event_id', 'VARCHAR', true),
('event_name', 'VARCHAR', true),
('event_time', 'TIMESTAMP', true),
('user_id', 'VARCHAR', false),
('properties', 'JSON', false)
) AS t(column_name, column_type, required);
WITH actual AS (
SELECT name AS column_name, type AS column_type
FROM parquet_schema('raw/events/2026-05-14/part-000.parquet')
)
SELECT e.*, a.column_type AS actual_type
FROM expected_event_schema e
LEFT JOIN actual a USING (column_name)
WHERE e.required AND a.column_name IS NULL
OR a.column_type IS DISTINCT FROM e.column_type;Manifest 운영
파일 glob은 편하지만 운영에서 같은 결과를 재현하기 어렵습니다. 중요한 batch라면 glob 결과를 manifest table로 고정해 둡니다.
CREATE OR REPLACE TABLE input_manifest AS
SELECT DISTINCT filename
FROM read_parquet('s3://lake/events/dt=2026-05-14/*.parquet');
COPY input_manifest
TO 'manifests/events_dt=2026-05-14.csv' (HEADER, DELIMITER ',');재실행 시에는 같은 manifest를 사용합니다.
SELECT *
FROM read_parquet((SELECT list(filename) FROM input_manifest));Drift 대응 기준
| drift | 대응 |
|---|---|
| nullable 컬럼 추가 | registry 업데이트 후 downstream 노출 |
| required 컬럼 누락 | batch fail |
| 타입 확대 | 명시 cast와 회귀 테스트 |
| 타입 축소 | 원본 producer 수정 전까지 quarantine |
| 파일별 컬럼 순서 차이 | union_by_name 허용 |