결합 표의 계약
90분 안팎
학습 목표
결합 SQL과 키 제약을 저장하고 조인 전후 행 수·합계·미매칭 수를 검사합니다.
개념
결합 표도 다음 단계의 입력입니다
SQL 한 번의 성공보다 다른 사람이 같은 결과를 만들 수 있는지가 중요합니다. queries/join.sql에는 결합 규칙을, join_pipeline.py에는 적재 제약과 파일 출력을 둡니다. 앞 단계 pipeline.py와 daily.sql은 보존합니다. python3 pipeline.py로 교통 산출물을 재생성한 뒤 python3 join_pipeline.py로 결합합니다. 테스트는 작은 입력을 바꿔 조회 결과와 감사 기록을 확인합니다.
joined.csv의 열은 region,date,total_vehicles,valid_observations,weather_region,rain_mm,station_rows,valid_stations,join_status입니다. 한 행은 교통 지역·날짜입니다. 통행량 단위는 대/일, 평균 강수량은 mm이며 나머지 수량은 행 수입니다. 매칭되지 않은 오른쪽 수량은 NULL입니다. 날씨 그룹이 있으나 모든 강수량이 결측이면 station_rows는 양수이고 valid_stations는 0입니다.
세 종류의 누락을 분리합니다
mapping_missing은 교통 지역의 대응 코드가 없는 상태입니다. weather_missing은 대응 코드는 있지만 해당 날짜 날씨 그룹이 없는 상태입니다. rain_missing은 날씨 그룹은 있으나 유효 강수량이 없는 상태입니다. CASE는 이 순서로 검사해 한 교통 행에 상태 하나를 부여합니다. matched는 날씨값이 있다는 뜻이며 교통량도 존재한다는 뜻은 아닙니다. A의 9월 3일 교통 결측은 그대로 남깁니다.
join-audit.json에는 before_rows,after_rows,before_total,after_total과 세 상태별 건수를 기록합니다. 기본 표본은 행 수6, 합계390이며 weather_missing1, rain_missing1입니다. 대응표를 B 없이 실행한 fixture에서는 mapping_missing3이 되어도 교통 행은 6개입니다. 전체 교통량이 NULL인 입력의 SUM은 NULL이며 빈 입력도 합계NULL로 기록합니다. 비어 있는 합계를 측정된 0으로 오해하지 않습니다.
제약과 검사는 다른 실패를 잡습니다
적재 표의 기본키는 daily의 지역·날짜, 대응표의 교통 지역, 날씨의 지역·날짜·관측소입니다. 키 열에는 NOT NULL을 명시하고 코드 빈 문자열은 Python에서 거부합니다. 숫자의 음수는 CHECK로 막고 날짜 형식은 Python에서 검사합니다. SQLite 열 타입 선언만으로 모든 입력 형식이 보장된다고 생각하지 않습니다. 오류가 나면 출력 파일을 새 성공 결과로 해석하지 않습니다.
UNIQUE constraint failed는 중복 키 적재를 뜻합니다. CHECK constraint failed는 음수 등 값 범위 위반을 뜻합니다. JOIN: 행 수·합계·키 보존 실패는 적재 후 조회가 기준선을 깨뜨렸다는 뜻입니다. 이때 SQL의 INNER JOIN, 날짜 조건 누락, 관측소 직접 결합을 먼저 확인합니다. 테스트 이름과 traceback 마지막 메시지를 같이 읽고 실패 fixture만 재현하면 원인을 좁힐 수 있습니다.
시작 코드에서 실패를 경험합니다
starter는 앞 모듈 solution 전체를 이어받지만 새 join.sql에 직접 관측소 INNER JOIN이 남아 있습니다. 원본 계약과 일별 집계 테스트는 통과하고 새 결합 테스트 일부가 실패합니다. weather_daily CTE와 두 LEFT JOIN, 상태 CASE를 구현하면 통과합니다. 테스트를 삭제하거나 Python의 감사 검사를 끄면 문제를 해결한 것이 아닙니다. ZIP의 README-join.md에 집계 규칙과 출력 계약이 적혀 있습니다.
성공 후 입력 바이트가 바뀌지 않는지 확인하고 재실행한 joined.csv가 같은지 비교합니다. joined.csv와 join-audit.json을 다음 모듈에 전달하며 누락 제외 정책은 아직 적용하지 않았다고 기록합니다. 이전 실행의 출력이 남은 채 새 실행이 실패할 수 있으므로 종료 코드와 감사 기록의 생성 시점을 함께 확인합니다. 실제 운영의 원자적 교체와 품질 게이트는 후속 모듈에서 확장합니다.
보존 조건을 여러 축에서 확인합니다
감사 기록의 before_rows와 after_rows는 같은 교통 입력을 기준으로 비교합니다. before_total은 원본 교통 파일의 합계가 아니라 daily의 합계입니다. 원본 중복 제거와 결합을 서로 다른 단계로 나누었으므로 그 경계를 유지합니다. 행 수만 같아도 누락 한 행과 중복 한 행이 상쇄될 수 있고 합계만 같아도 같은 수치의 두 키가 바뀔 수 있습니다. 키 유일성, 개별 표본 값, 상태 분류 검사를 함께 두는 이유입니다.
현재 Python 감사 검사는 행 수와 합계 및 출력 키 중복을 확인합니다. 모든 입력 키와 출력 키의 집합이 같거나 각 교통값이 그대로라는 사실까지 이 검사 하나로 보장하지는 않습니다. 그런 의미는 조회의 왼쪽 키 선택과 표본 테스트로 함께 점검합니다. 상태 열 역시 문자열이 출력됐다는 이유만으로 정확한 분류라고 판단하지 않습니다. 누락 조건을 한 종류씩 바꾼 fixture에서 기대 건수를 확인해야 합니다.
각 실패가 어떤 계약을 검증하는지 읽습니다
test_mapping_missing은 대응표에서 B를 뺀 입력으로 교통 세 행이 코드 부재 상태가 되는지 확인합니다. test_empty_weather는 날씨 원본만 비우고 교통 여섯 행이 날씨 부재 상태로 보존되는지 봅니다. test_null_and_zero_station은 결측 하나와 0 하나의 평균이 0이고 유효 수가 1인지 검사합니다. 입력을 한 축씩 바꾸면 실패가 키 연결, 행 보존, 평균 계산 중 어디에 있는지 구분하기 쉽습니다.
중복 대응표, 중복 관측소, 중복 daily 검사는 적재 시 IntegrityError가 발생하는 것을 기대합니다. 이 경우 예외가 발생한 테스트가 통과할 수 있습니다. 반대로 정상 표본에서 예외가 나면 실패입니다. 콘솔의 오류 문자열만 세지 말고 테스트의 기대 동작과 마지막 OK 또는 FAILED를 확인합니다. starter는 잘못된 결합 SQL 때문에 정상 입력 검사 일부가 실패하며 기존 원본 검사까지 실패하도록 만든 연습은 아닙니다.
파일 형식과 실행 상태를 인계합니다
브라우저의 NULL 표시와 달리 joined.csv에서 SQL NULL은 빈 필드로 저장됩니다. 평균 3.0처럼 소수 표현이 있어도 강수량 단위는 mm이며 문자열 모양을 바꾸려고 값을 정수로 잘라내지 않습니다. join-audit.json 파일은 들여쓰기 있는 JSON이고 콘솔은 같은 감사 객체를 한 줄로 출력합니다. 화면 출력의 공백 모양을 CSV 계약과 혼동하지 말고 CSV 헤더와 실제 열 순서를 직접 확인합니다.
실행 전에는 입력 CSV의 헤더가 계약과 같은지 확인합니다. 누락 열과 불필요한 추가 열은 HEADER 오류이며 행마다 필드 수가 다른 경우도 거부됩니다. daily에서 total_vehicles가 빈칸이면 valid_observations는 0이어야 하고 값이 있으면 관측 수가 양수여야 합니다. 값 0과 관측 수 1은 정상입니다. 이 관계를 깨뜨린 입력은 SQL을 고쳐 숨기는 대신 앞 단계 산출물의 계약부터 점검합니다.
미션 ZIP에는 daily.csv와 앞 단계 코드가 함께 있으므로 검사 명령을 바로 실행할 수 있습니다. 최종 산출물을 만들 때는 pipeline.py와 join_pipeline.py를 순서대로 실행합니다. 테스트는 임시 복사본에 fixture를 기록하므로 제출 폴더 원본을 바꿔 가며 시험할 필요가 없습니다. 반복 실행 검사는 출력 바이트가 같은지 확인하고 원본 보존 검사도 수행합니다. 성공한 조회문과 감사 결과를 함께 남겨 후속 분석자가 결합 규칙을 추적할 수 있게 합니다.
따라하기
기준선 기록
아래 준비 표에서 조회를 실행하고 결과를 비교합니다.
SELECT COUNT(*),SUM(total_vehicles),COUNT(total_vehicles) FROM daily;실행 결과
4 300 4
상태별 누락 읽기
아래 준비 표에서 조회를 실행하고 결과를 비교합니다.
SELECT t.region,t.date,CASE WHEN m.region IS NULL THEN 'mapping_missing' ELSE 'weather_missing' END AS reason FROM daily t LEFT JOIN region_map m ON t.region=m.region LEFT JOIN weather_daily w ON m.weather_region=w.weather_region AND t.date=w.date WHERE m.region IS NULL OR w.weather_region IS NULL ORDER BY t.region,t.date;실행 결과
B 2026-09-02 weather_missing
기본키 위반 메시지 읽기
import sqlite3
db=sqlite3.connect(':memory:')
db.execute('CREATE TABLE daily(region TEXT NOT NULL,date TEXT NOT NULL,PRIMARY KEY(region,date))')
db.execute("INSERT INTO daily VALUES('A','2026-09-01')")
try:
db.execute("INSERT INTO daily VALUES('A','2026-09-01')")
except sqlite3.IntegrityError as e:
print(e)
finally:
db.close()실행 결과
UNIQUE constraint failed: daily.region, daily.date
ZIP에서 산출물 생성하기
solution ZIP을 푼 폴더에서 python3 pipeline.py를 실행한 뒤 python3 join_pipeline.py를 실행합니다. 아래는 두 명령의 표준 출력입니다. starter에서는 SQL 수정과 테스트 통과 후 실행합니다. joined.csv의 6행과 join-audit.json의 누락 건수를 함께 확인합니다.
실행 결과
raw_rows=7 raw_total=470 unique_total=390 daily_rows=6
{"after_rows": 6, "after_total": 390, "before_rows": 6, "before_total": 390, "mapping_missing": 0, "rain_missing": 1, "weather_missing": 1}
확인 문제
실습
ZIP의 queries/join.sql을 고쳐 관측소 집계 CTE, 두 LEFT JOIN, 상태 CASE를 완성합니다. README-join.md의 열 계약을 따릅니다. Python 감사 검사를 유지하고 기존 테스트와 새 테스트를 모두 통과시킵니다. python3 pipeline.py 후 python3 join_pipeline.py를 실행하여 joined.csv와 join-audit.json을 만듭니다. 입력 원본은 보존합니다.
실행 명령
python3 -m unittest discover -s tests -v
기대 결과
기존 19개와 결합 10개, 총 29개 테스트 통과(OK). starter는 새 결합 테스트 일부 실패.
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 결합 결과를 인계할 때 행 수·합계 외에 무엇을 검증하나요?
- 실행이 실패했는데 이전 출력 파일이 남아 있다면 어떻게 판단하나요?