[SQL 쿡북/09장] 10절. 누락된 날짜 채우기

정은아·2025년 5월 23일

[도서] SQL 쿡북

목록 보기
9/13
post-thumbnail

09장. 날짜 조작 기법


  • 이 부분의 요령은, 사원이 고용되지 않은 경우에도 매월 행을 반환하는 것입니다. (즉, 개수가 0이 됩니다.)

🎨 DB2

  • 재귀 WITH 절을 사용하여 매월(2000년 1월 1일부터 2003년 12월 1일까지 매월 1일)을 생성합니다.
  • 필요한 날짜 범위에 대한 모든 월이 만들어지면 EMP 테이블에 외부 조인하고 집계 함수 COUNT를 사용하여 매월 고용 수를 계산합니다.
WITH x(start_date, end_date) AS (
  SELECT (MIN(hiredate) - DAYOFYEAR(MIN(hiredate)) DAY + 1 DAY) start_date,
         (MAX(hiredate) - DAYOFYEAR(MAX(hiredate)) DAY + 1 DAY) + 1 YEAR end_date
  FROM emp

  UNION ALL

  SELECT start_date + 1 MONTH, end_date
  FROM x
  WHERE (start_date + 1 MONTH) < end_date
)

SELECT x.start_date mth,
       COUNT(e.hiredate) num_hired
FROM x
LEFT JOIN emp e
  ON x.start_date = (e.hiredate - DAY(e.hiredate) DAY + 1 DAY)
GROUP BY x.start_date
ORDER BY 1;

✅ 해석

  1. MIN(hiredate) - DAYOFYEAR(...) + 1 DAY

    • 입사일 중 가장 이른 날짜 기준으로 그 해의 1월 1일을 계산
    • 이후 월 단위로 반복 생성할 출발점이 됨
  2. MAX(hiredate) - DAYOFYEAR(...) + 1 DAY + 1 YEAR

    • 가장 늦은 입사일 기준으로 다음 해의 1월 1일을 계산
    • 반복 종료 지점을 넉넉하게 설정하기 위한 기준
  3. WITH RECURSIVE x(start_date, end_date)

    • start_date부터 1개월씩 증가하는 날짜 목록 생성
    • 종료 조건: start_date < end_date인 동안만 생성
  4. LEFT JOIN emp e ON x.start_date = (e.hiredate - DAY(...) + 1 DAY)

    • 입사일을 해당 월의 1일로 보정한 값과 start_date를 매칭
    • 즉, 입사한 달의 1일을 기준으로 JOIN 수행
    • 채용 내역이 없는 달도 포함하기 위해 LEFT JOIN 사용
  5. COUNT(e.hiredate)

    • 해당 월에 입사한 인원의 수를 집계

✅ 예시 결과

💎 채용 기록이 없는 월도 누락 없이 출력됨 (LEFT JOIN + 재귀 CTE 덕분!)

mthnum_hired
1981-01-010
1981-02-012
1981-03-010
......

🎨 Oracle

  • CONNECT BY 절을 사용하여 2000년과 2003년 사이에 매월을 생성합니다.
  • 그런 다음 EMP 테이블에 외부 조인하고 집계 함수 COUNT를 사용하여 매월 고용된 사원 수를 계산
WITH x AS (
  SELECT ADD_MONTHS(start_date, LEVEL - 1) start_date
  FROM (
    SELECT MIN(TRUNC(hiredate, 'Y')) start_date,
           ADD_MONTHS(MAX(TRUNC(hiredate, 'Y')), 12) end_date
    FROM emp
  )
  CONNECT BY LEVEL <= MONTHS_BETWEEN(end_date, start_date)
)

SELECT x.start_date MTH,
       COUNT(e.hiredate) num_hired
FROM x
LEFT JOIN emp e
  ON x.start_date = TRUNC(e.hiredate, 'MM')
GROUP BY x.start_date
ORDER BY 1;

✅ 해석

  1. MIN(TRUNC(hiredate, 'Y'))

    • 입사일 기준으로 가장 오래된 연도의 1월 1일을 계산
  2. MAX(TRUNC(hiredate, 'Y')) + 12개월

    • 가장 최근 연도의 1월 1일에 12개월을 더함 → 다음 해의 1월 1일
    • ✅ 전체 조회 범위: 첫 해 1월 1일 ~ 마지막 해의 다음 해 1월 1일
  3. CONNECT BY LEVEL <= MONTHS_BETWEEN(end_date, start_date)

    • start_date부터 end_date까지 한 달 단위로 증가하는 행 생성
    • LEVEL은 1부터 시작
    • 각 월: ADD_MONTHS(start_date, LEVEL - 1)로 계산
  4. x.start_date = TRUNC(e.hiredate, 'MM')

    • 입사일을 월 단위(해당 월의 1일) 로 자름
    • 해당 월 기준으로 조인
  5. COUNT(e.hiredate)

    • 월별 입사자 수 집계
    • LEFT JOIN 사용 → 입사자가 없는 달도 0명으로 출력

✅ 예시 결과

CONNECT BY + LEFT JOIN 덕분에, 입사 기록이 없는 달도 누락 없이 출력된다.

mthnum_hired
1981-01-010
1981-02-012
1981-03-010
......

🎨 PostgreSQL

  • CTE를 사용하여 최초 고용일 이후 월을 입력한 다음, 생성된 각 월의 월과 연도를 사용하여 EMP 테이블에 LEFT OUTER JOIN을 사용해 각 기간의 고용일수를 계산할 수 있습니다.
WITH RECURSIVE x(start_date, end_date) AS (
  SELECT
    CAST(MIN(hiredate) - (CAST(EXTRACT(DAY FROM MIN(hiredate)) 
    AS integer) - 1) AS date), MAX(hiredate)
  FROM emp

  UNION ALL

  SELECT
    CAST(start_date + INTERVAL '1 month' AS date),
    end_date
  FROM x
  WHERE start_date < end_date
)

SELECT x.start_date,
       COUNT(emp.hiredate)
FROM x
LEFT JOIN emp
  ON EXTRACT(MONTH FROM start_date) = EXTRACT(MONTH FROM emp.hiredate)
 AND EXTRACT(YEAR FROM start_date) = EXTRACT(YEAR FROM emp.hiredate)
GROUP BY x.start_date
ORDER BY 1;

✅ 해석

  1. MIN(hiredate) - (EXTRACT(DAY FROM ...) - 1)

    • 가장 빠른 입사일에서 일(day)을 빼고 1을 더해, 해당 월의 1일을 구함
      • 예: 1981-03-151981-03-01
  2. MAX(hiredate)

    • 입사일 중 가장 늦은 날짜
    • 반복 종료 조건으로 사용됨
  3. RECURSIVE x(start_date, end_date)

    • start_date부터 시작하여 매번 +1달씩 증가
    • start_date < end_date일 때까지만 재귀적으로 생성
  4. LEFT JOIN ON (MONTH/YEAR 일치)

    • emp.hiredatex.start_date연도와 월이 일치할 경우 매칭
    • PostgreSQL에서는 TRUNC(hiredate, 'MM') 대신 EXTRACT(YEAR) + EXTRACT(MONTH) 조합 사용
  5. COUNT(emp.hiredate)

    • 해당 월에 입사한 직원 수를 카운트
    • LEFT JOIN으로 입사자가 없는 달도 0으로 표시

✅ 결과 예시

start_datecount
1980-12-010
1981-01-010
1981-02-012
1981-03-011
......

🎨 MySQL

  • 재귀 CTE 사용하여 시작일과 종료일 사이에 매월을 생성한 다음 EMP 테이블에 대한 외부 조인을 사용하여 고용 내역을 확인합니다.
WITH RECURSIVE x(start_date, end_date) AS (
  SELECT 
    ADDDATE(MIN(hiredate), -DAYOFYEAR(MIN(hiredate)) + 1) start_date,
    ADDDATE(MAX(hiredate), -DAYOFYEAR(MAX(hiredate)) + 1) end_date
  FROM emp

  UNION ALL

  SELECT 
    DATE_ADD(start_date, INTERVAL 1 MONTH),
    end_date
  FROM x
  WHERE DATE_ADD(start_date, INTERVAL 1 MONTH) < end_date
)

SELECT x.start_date mth,
       COUNT(e.hiredate) num_hired
FROM x
LEFT JOIN emp e
  ON EXTRACT(YEAR_MONTH FROM start_date) = EXTRACT(YEAR_MONTH FROM e.hiredate)
GROUP BY x.start_date
ORDER BY 1;

✅ 해석

  1. ADDDATE(MIN(hiredate), -DAYOFYEAR(...) + 1)

    • 입사일 중 가장 이른 날짜를 기준으로 그 해의 1월 1일을 계산
    • → 월 생성의 시작점
  2. ADDDATE(MAX(hiredate), -DAYOFYEAR(...) + 1)

    • 가장 늦은 입사일을 기준으로 그 해의 1월 1일을 계산
    • → 월 생성의 종료 조건
  3. RECURSIVE ... UNION ALL

    • 시작일부터 1개월씩 증가하며 매달 1일을 재귀적으로 생성
    • 종료일에 도달할 때까지 반복
  4. LEFT JOIN emp ON EXTRACT(YEAR_MONTH ...)

    • start_datee.hiredate의 연월(YEAR * 100 + MONTH)이 일치할 경우 매칭
    • 월 기준으로 입사자 데이터를 연결
  5. COUNT(e.hiredate)

    • 해당 월에 입사한 인원 수를 집계
    • LEFT JOIN이므로 입사자가 없는 달도 0으로 출력

✅ 결과 예시

mthnum_hired
1981-01-010
1981-02-011
1981-03-012
......

🎨 SQL Server

  • 재귀 WITH 절을 사용하여 매월(2000년 1월 1일부터 2003년 12월 1일까지 매월 1일)을 생성합니다.
  • 필요한 날짜 범위에 대한 모든 월이 있으면 EMP 테이블에 외부 조인하고 집계 함수 COUNT를 사용하여 매월 고용 일수를 계산합니다.
WITH x(start_date, end_date) AS (
  SELECT 
    DATEADD(dd, 1 - DATEPART(dy, MIN(hiredate)), MIN(hiredate)),
    DATEADD(yy, 1, DATEADD(dd, 1 - DATEPART(dy, MAX(hiredate)), MAX(hiredate)))
  FROM emp

  UNION ALL

  SELECT DATEADD(mm, 1, start_date), end_date
  FROM x
  WHERE DATEADD(mm, 1, start_date) < end_date
)

SELECT x.start_date mth,
       COUNT(e.hiredate) num_hired
FROM x
LEFT JOIN emp e
  ON x.start_date = DATEADD(dd, 1 - DAY(e.hiredate), e.hiredate)
GROUP BY x.start_date
ORDER BY 1;

✅ 해석

  1. DATEADD(dd, 1 - DATEPART(dy, MIN(hiredate)), MIN(hiredate))

    • 가장 이른 입사일을 기준으로 해당 해의 1월 1일을 계산
    • → 월 생성의 시작점
  2. DATEADD(yy, 1, DATEADD(dd, 1 - DATEPART(dy, MAX(hiredate)), MAX(hiredate)))

    • 가장 늦은 입사일에서 그 해의 1월 1일을 구한 뒤 +1년
    • → 월 생성의 종료점
  3. RECURSIVE UNION ALL

    • start_date부터 시작해 매번 +1개월씩 재귀 증가
    • 종료 조건: 생성한 날짜가 end_date보다 이전일 때까지만 반복
  4. LEFT JOIN ON x.start_date = DATEADD(dd, 1 - DAY(e.hiredate), e.hiredate)

    • e.hiredate해당 월의 1일로 바꾸고
    • x.start_date와 조인 → 월 기준 매칭
  5. COUNT(e.hiredate)

    • 해당 월에 입사한 인원 수를 집계
    • LEFT JOIN 덕분에 입사자가 없는 달도 0으로 출력

✅ 예시 결과

mthnum_hired
1981-01-010
1981-02-011
1981-03-012
......
profile
꾸준함의 가치를 믿는 개발자

0개의 댓글