다중 파일·스키마 진화
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 테이블과 DESCRIBE 결과를 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 column_name, column_type
FROM (DESCRIBE SELECT *
FROM read_parquet('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_name IS NOT NULL AND a.column_type IS DISTINCT FROM e.column_type);parquet_schema().type은 물리 타입이므로 VARCHAR 같은 SQL 타입과 바로 비교하지 않습니다. 위 검사는 DuckDB가 노출하는 논리 스키마를 비교하며, 선택 컬럼이 없는 경우는 허용합니다. 파일마다 실행해야 어느 파일이 계약을 어겼는지 알 수 있습니다. JSON의 논리 타입 보존 여부와 nested field 계약도 별도로 확인합니다.
Manifest 운영
파일 glob은 편하지만 운영에서 같은 결과를 재현하기 어렵습니다. 중요한 batch라면 glob 결과를 manifest table로 고정해 둡니다.
CREATE OR REPLACE TABLE input_manifest AS
SELECT file AS filename
FROM glob('s3://lake/events/dt=2026-05-14/*.parquet');
COPY input_manifest
TO 'manifests/events_dt=2026-05-14.csv' (HEADER, DELIMITER ',');재실행 시에는 같은 manifest를 사용합니다.
SET VARIABLE input_files = (SELECT list(filename ORDER BY filename) FROM input_manifest);
SELECT *
FROM read_parquet(getvariable('input_files'));table function 인자에 서브쿼리를 직접 넣지 않고 SET VARIABLE로 파일 목록을 먼저 확정합니다. 빈 manifest는 실행 전에 실패 처리하고, 파일명이 같아도 내용이 바뀔 수 있으므로 immutable 경로나 object version/checksum을 함께 보존합니다. 변수로 파일 목록 전달.
Drift 대응 기준
| drift | 대응 |
|---|---|
| nullable 컬럼 추가 | registry 업데이트 후 downstream 노출 |
| required 컬럼 누락 | batch fail |
| 타입 확대 | 명시 cast와 회귀 테스트 |
| 타입 축소 | 원본 producer 수정 전까지 quarantine |
| 파일별 컬럼 순서 차이 | union_by_name 허용 |
2.0 읽기 schema를 명시적으로 바꾸기
2.0 문서의 schema map은 field-id 또는 컬럼명을 기준으로 이름·타입·기본값을 지정합니다. 계약 검사와 달리 읽는 결과 자체를 바꾸므로 누락 컬럼을 기본값으로 채우기 전에 producer 오류인지 판정합니다. union_by_name=true와 함께 쓸 수 없습니다.
SELECT * FROM read_parquet('raw/events.parquet', schema = MAP {
'id': {name: 'event_id', type: 'BIGINT', default_value: NULL},
'source': {name: 'source', type: 'VARCHAR', default_value: 'legacy'}
});bound_schema는 나열한 컬럼이 실제로 모두 있어야 하는 완전한 출력 schema입니다. schema·union_by_name과 혼용할 수 없고 hive_partitioning도 명시해야 합니다. Parquet 읽기 옵션.