Devin.KR

수치 불일치 추적

90분 안팎

학습 목표

기간·시간대·필터·조인·집계 단계별 건수를 SQL로 비교하여 보고서 차이의 원인을 찾습니다.

개념

차이를 없애기 전에 차이의 의미를 찾습니다

보고서에서 통행량 합계가 300인데 원본을 더하면 390이라는 문의를 받았다고 가정합니다. 곧바로 숫자를 원본 합계로 바꾸면 비와 통행량을 함께 비교한다는 지표 정의가 사라집니다. 먼저 두 수치의 기간, 지역, 관측 단위와 제외 조건을 나란히 적습니다. 불일치는 오류일 수도 있지만 서로 다른 질문의 답을 비교한 결과일 수도 있습니다. 이번 레슨은 단계별 건수와 합계를 조회해 두 가능성을 구분합니다.

조사 출발점은 보고서 안의 성공 lineage와 입력 해시입니다. 최신 실행 로그만 선택하면 보고서를 만들지 못한 실패 시도의 입력으로 수치를 대조하게 됩니다. 같은 해시의 원본을 확보한 뒤 정책에 기록한 시간대, 날짜 경계, 필터, 조인 방식, 집계 대상을 확인합니다. 코드가 같다는 사실만으로 자료와 정책이 같다고 판단하지 않습니다. 실행마다 무엇이 입력되었는지와 어떤 규칙이 적용되었는지가 함께 필요합니다.

기간과 시간대를 먼저 맞춥니다

날짜 조건은 시작일 포함, 종료일 미포함으로 표현하면 연속 기간이 경계를 공유해도 중복되지 않습니다. 예를 들어 9월 1일부터 9월 3일까지 비교하려면 date가 2026-09-01 이상이고 2026-09-04 미만인 행을 선택합니다. 마지막 날을 제외한 9월 3일 미만 조건은 하루를 누락합니다. 날짜가 ISO 형식으로 정규화된 경우 문자열 순서로 이 범위를 조회할 수 있으며 다른 날짜 표현은 먼저 변환해야 합니다.

시각이 있는 실제 자료에서는 UTC 자정과 서울 자정이 서로 다른 순간입니다. 2026-09-01 16:00 UTC는 서울에서 다음 날짜입니다. 날짜만 있는 이번 표본은 upstream에서 Asia/Seoul의 하루 단위로 정리되었다는 계약을 사용하며 SQL에서 다시 아홉 시간을 더하지 않습니다. 관측 날짜를 시각처럼 변환하면 경계가 이동할 수 있습니다. 원본의 시간대와 집계 날짜를 확인한 뒤에만 필요한 변환을 결정합니다.

단계별 표를 만들어 최초 차이를 찾습니다

SQL 실습의 traffic은 지역·날짜별 통행량이고 weather는 같은 키의 강수량입니다. source 단계는 traffic 전체, period 단계는 지정 기간, filtered 단계는 그 기간의 통행량이 null이 아닌 행입니다. joined 단계는 왼쪽 조인 후 강수량도 null이 아닌 유효 비교 행입니다. aggregated 단계는 이 비교 행들을 날짜별로 묶은 결과입니다. 각 단계의 row_count와 sum_vehicles를 같은 형식으로 출력합니다.

건수는 단계마다 무엇을 세는지 다릅니다. source의 건수는 관측 행, joined는 비교 가능한 관측 행, aggregated는 날짜 그룹 수입니다. 그룹 수가 줄었다고 데이터를 잃었다고 단정하지 않습니다. 정상 집계라면 입력 관측들을 묶으면서 합계가 유지되어야 합니다. 행의 단위가 바뀌는 지점에는 단계 이름과 함께 단위를 적습니다. 한 열 이름이 같아도 집계 전후에 같은 뜻이라고 가정하지 않는 습관이 필요합니다.

각 단계는 CTE로 이름을 붙여 다음 단계가 참조하게 만듭니다. source, period, filtered, joined, daily를 차례로 읽으면 어디서 행이 제외되는지 알 수 있습니다. 마지막 UNION ALL로 진단 결과를 모으고 순서 번호로 정렬합니다. 결과를 임의 출력 순서에 맡기지 않습니다. CTE는 단계별 조회의 표현 도구이며 원본을 영구 보존하는 로그가 아닙니다. 실제 파이프라인에는 입력 해시와 실행 정책을 별도로 기록합니다.

조인 증가를 눈에 보이게 합니다

날씨 표에 같은 지역·날짜가 두 행 있으면 교통 한 행이 조인 결과 두 행으로 늘 수 있습니다. 강수 유효 여부를 걸러도 두 날씨 행이 모두 유효하면 통행량이 두 번 합산됩니다. 키별 COUNT를 먼저 조회하고 중복이 있다면 결과를 공개하지 않고 원인을 찾습니다. SELECT DISTINCT로 결과만 감추면 어떤 정정본을 선택했는지 근거가 없어집니다. 실습에는 이 증가가 나타나는 입력을 넣어 진단 출력으로 발견하도록 합니다.

왼쪽 조인은 날씨가 없는 교통 행도 남깁니다. 이어지는 rain_mm IS NOT NULL 조건은 날씨가 없거나 강수량이 결측인 행을 유효 비교 대상에서 제외합니다. 따라서 조인을 왼쪽으로 썼다는 사실만 보고 모든 원본 행이 최종 비교에 남는다고 말할 수 없습니다. 행 수 감소가 조인 종류 때문인지 뒤의 필터 때문인지 단계별로 확인합니다. 결측 날씨를 강수 0으로 채우면 비가 오지 않은 관측으로 오해하므로 이번 정의에서는 사용하지 않습니다.

COUNT와 SUM의 결측 처리를 읽습니다

COUNT(*)는 행을 세고 COUNT(total_vehicles)는 값이 null이 아닌 행을 셉니다. 이 실습의 단계별 건수는 COUNT(*)이며 filtered 단계에서 수치 결측을 명시적으로 제거합니다. SUM은 null 값을 더하지 않으므로 통행량 결측이 한 개 있어도 합계가 나올 수 있습니다. 합계만 보고 데이터가 완전하다고 판단하지 않도록 단계별 행 수와 유효 값 수를 함께 확인합니다. 빈 입력에서는 SUM이 null이 되어 별도 처리가 필요합니다.

진단용 합계는 COALESCE로 빈 단계의 합계를 0으로 표시합니다. 이는 관측 결측값을 영점으로 보정한다는 뜻이 아닙니다. 아무 값도 더하지 않은 집합의 표시 규칙일 뿐입니다. 날짜 그룹에 교통 영점이 실제로 있는 사례와 입력이 없는 사례는 행 수를 함께 보면 구분됩니다. 출력 숫자의 표현을 비교할 때도 SQLite 결과의 정수와 소수 표시를 확인하며 진단기에서 정수인 합계는 정수 문자열로 표시합니다.

정정 범위를 근거와 함께 좁힙니다

period에서 최초 차이가 나면 날짜 경계나 시간대 계약을 확인합니다. filtered에서 차이가 나면 제외한 통행 결측 행을 조회합니다. joined에서 합계가 증가하면 날씨 키 중복을 확인하고 감소하면 날씨 누락과 강수 결측을 나누어 봅니다. aggregated에서 합계가 달라지면 그룹 조건이나 추가 필터가 끼어들었는지 찾습니다. 이 순서로 조사하면 마지막 숫자를 바꾸는 대신 오류가 시작된 단계에 수정이 연결됩니다.

수치 수정이 필요하면 원인, 영향 지역·기간, 사용한 입력 해시, 수정 전후 행 수와 합계를 기록합니다. 조인 키를 바로잡은 수정이라면 전체 데이터를 다시 계산하고 보고서도 재생성합니다. 보고서 JSON의 합계만 손으로 바꾸면 계보와 저장 결과가 달라집니다. 정상 제외로 설명되는 차이라면 값은 유지하고 지표 정의와 표본 수 안내를 보완합니다. 수정 여부 자체가 조사 결과에 따라 달라진다는 점이 핵심입니다.

no such table: traffic은 테스트 준비 SQL이 실행되지 않았거나 다른 DB를 조회했다는 신호입니다. ambiguous column name: region은 조인 뒤 어느 표의 지역인지 모호하다는 뜻으로 t.region처럼 별칭을 사용합니다. GROUP BY에 필요한 차원을 빼면 오류가 없어도 다른 단위의 합계가 나올 수 있습니다. 실행 성공 여부와 수치 의미 검증을 구분하여, 오류 메시지가 없는 경우에도 작은 표본을 손으로 계산해 대조합니다.

브라우저 실습을 마치면 정상 여섯 행에서 통행 결측 제외, 강수 유효 쌍 제외, 날짜 묶음의 변화를 설명할 수 있어야 합니다. 빈 표, 기간 밖 행, 실제 영점, 중복 날씨 키를 포함한 테스트가 같은 조회를 평가합니다. 더 읽기의 CTE 장에서 조회 표현을 확장하고 여기서는 단계 간 보존되어야 할 합계와 변해도 되는 건수를 판단합니다. 진단 출력이 이상을 보이면 그 자료를 승인하는 도구로 사용하지 않고 조사 근거로 남깁니다.

따라하기

기간·필터·조인·집계 대조

각 단계의 최초 차이를 찾아 제외 조건과 연결합니다.

WITH period AS (
 SELECT * FROM traffic WHERE date >= '2026-09-01' AND date < '2026-09-04'
), filtered AS (
 SELECT * FROM period WHERE total_vehicles IS NOT NULL
), joined AS (
 SELECT t.region,t.date,t.total_vehicles,w.rain_mm FROM filtered t
 LEFT JOIN weather w ON t.region=w.region AND t.date=w.date
 WHERE w.rain_mm IS NOT NULL
), daily AS (
 SELECT date,SUM(total_vehicles) AS total FROM joined GROUP BY date
), trace AS (
 SELECT 1 AS n,'source' AS stage,COUNT(*) AS row_count,COALESCE(SUM(total_vehicles),0) AS total FROM traffic
 UNION ALL SELECT 2,'period',COUNT(*),COALESCE(SUM(total_vehicles),0) FROM period
 UNION ALL SELECT 3,'filtered',COUNT(*),COALESCE(SUM(total_vehicles),0) FROM filtered
 UNION ALL SELECT 4,'joined',COUNT(*),COALESCE(SUM(total_vehicles),0) FROM joined
 UNION ALL SELECT 5,'aggregated',COUNT(*),COALESCE(SUM(total),0) FROM daily
)
SELECT stage,row_count,total FROM trace ORDER BY n;

실행 결과

source 6 390
period 6 390
filtered 5 390
joined 4 300
aggregated 2 300

중복 날씨 키 찾기

같은 교통량이 조인에서 반복되는 원인을 먼저 확인합니다.

SELECT region,date,COUNT(*) FROM weather GROUP BY region,date HAVING COUNT(*) > 1 ORDER BY region,date;

실행 결과

A 2026-09-01 2

시간대 경계 해석

UTC 시각을 서울 시각으로 옮겨 날짜 차이를 직접 확인합니다.

from datetime import datetime,timezone,timedelta
t=datetime(2026,9,1,16,tzinfo=timezone.utc)
print('UTC',t.date())
print('Seoul',t.astimezone(timezone(timedelta(hours=9))).date())

실행 결과

UTC 2026-09-01
Seoul 2026-09-02

확인 문제

실습

traffic(region,date,total_vehicles)과 weather(region,date,rain_mm)에서 source, period, filtered, joined, aggregated 순서의 단계명·행 수·통행량 합계를 출력합니다. 기간은 2026-09-01 포함부터 2026-09-04 미포함입니다. filtered는 통행량 null 제외, joined는 지역·날짜 왼쪽 조인 후 강수 null 제외, aggregated는 날짜별 합계의 그룹 수와 전체 합계입니다. 빈 단계 합계는 0으로 표시합니다. 중복 날씨 키를 임의 제거하지 말고 합계 증가가 진단 출력에 나타나게 합니다. 단계별 ORDER BY를 명시합니다.

모범 답안
WITH period AS (
 SELECT * FROM traffic WHERE date >= '2026-09-01' AND date < '2026-09-04'
), filtered AS (
 SELECT * FROM period WHERE total_vehicles IS NOT NULL
), joined AS (
 SELECT t.region,t.date,t.total_vehicles,w.rain_mm FROM filtered t
 LEFT JOIN weather w ON t.region=w.region AND t.date=w.date
 WHERE w.rain_mm IS NOT NULL
), daily AS (
 SELECT date,SUM(total_vehicles) AS total FROM joined GROUP BY date
), trace AS (
 SELECT 1 AS n,'source' AS stage,COUNT(*) AS row_count,COALESCE(SUM(total_vehicles),0) AS total FROM traffic
 UNION ALL SELECT 2,'period',COUNT(*),COALESCE(SUM(total_vehicles),0) FROM period
 UNION ALL SELECT 3,'filtered',COUNT(*),COALESCE(SUM(total_vehicles),0) FROM filtered
 UNION ALL SELECT 4,'joined',COUNT(*),COALESCE(SUM(total_vehicles),0) FROM joined
 UNION ALL SELECT 5,'aggregated',COUNT(*),COALESCE(SUM(total),0) FROM daily
)
SELECT stage,row_count,total FROM trace ORDER BY n;

더 읽기

면접 질문

  • 보고서의 수치와 원본 데이터가 다를 때 확인할 순서를 설명해 주시면 됩니다.