SQL 날짜 및 시간: 오늘 날짜부터 12가지 질문을 통한 생일 알림까지
요약
SQL을 사용하여 날짜와 시간을 다루는 다양한 방법과 함수를 12가지 단계별 질문을 통해 학습합니다. 현재 시간 확인부터 날짜 추출, 나이 계산 등 실무적인 쿼리 작성법을 다룹니다.
핵심 포인트
- CURRENT_DATE와 NOW()의 차이점 이해
- EXTRACT 함수를 이용한 연, 월, 일 데이터 추출
- AGE() 함수를 활용한 두 날짜 사이의 간격 계산
- 실제 생일 알림 시스템 구축을 위한 쿼리 응용
모든 데이터셋에는 어딘가에 날짜가 포함되어 있습니다. 생년월일, 시험 날짜, 주문 날짜, 등록 날짜 등입니다. 그리고 해당 데이터에 대해 던질 수 있는 거의 모든 유용한 질문은 이러한 날짜를 가지고 무언가를 수행하는 것—날짜를 비교하거나, 형식을 지정하거나, 일부를 추출하거나, 날짜 사이의 거리를 계산하는 것—을 포함합니다.
오늘 우리는 Greenwood Academy의 학생 및 시험 기록에 대한 12가지 질문을 살펴보았습니다. 각 질문은 이전 질문을 바탕으로 진행되었습니다. 12번 질문에 도달했을 때 우리는 1번 질문에서 소개했던 것과 동일한 함수들을 사용하여 실제 학교 시스템에서 실행될 수 있는 생일 알림 쿼리를 구축하고 있었습니다.
여기 모든 질문과 모든 함수, 그리고 핵심적인 순간들이 있습니다.
Q1: 지금은 몇 시인가요?
테이블 데이터에 손을 대기 전에, 가능한 가장 단순한 날짜 질문으로 시작했습니다.
SELECT
CURRENT_DATE AS today,
NOW() AS current_datetime,
...
today | current_datetime | timestamp
------------|-------------------------------|-------------------------------
2026-07-05 | 2026-07-05 10:23:41.847+03 | 2026-07-05 10:23:41.847+03
PostgreSQL에 현재 시간을 묻는 세 가지 방법입니다. 그 차이점은 중요합니다:
CURRENT_DATE- 날짜만 포함하며 시간 구성 요소는 없음NOW()및CURRENT_TIMESTAMP- 둘 다 날짜 + 시간 + 시간대(timezone)를 반환하며, 사실상 동일합니다
"날짜만 중요할 때(오늘 날짜로 필터링하거나 두 날짜 사이의 일수를 계산할 때)는 CURRENT_DATE를 사용하세요. 무언가가 발생한 정확한 순간이 필요할 때(감사 로그(audit logs), 레코드의 타임스탬프 등)는 NOW()를 사용하세요."
Q2: 연, 월, 일을 각각 분리하여 추출하기
2008-03-15로 저장된 생년월일은 단일 값입니다. 만약 연도만 원한다면 어떻게 될까요? 월만 원한다면요? EXTRACT가 이를 수행합니다.
SELECT
first_name,
date_of_birth,
...
first_name | date_of_birth | birth_year | birth_month | birth_day
-----------|---------------|------------|-------------|----------
Amina | 2008-03-15 | 2008 | 3 | 15
...
구문: EXTRACT(field FROM column). field에는 YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DOW (day of week, 요일) 등을 사용할 수 있습니다. 이번 세션이 끝나기 전에 이 중 여러 가지를 사용해 볼 것입니다.
"EXTRACT는 컬럼을 변경하지 않습니다. 컬럼을 읽어서 숫자를 반환할 뿐입니다. 무언가를 수정하는 것이 아니라, 날짜의 특정 부분에 대해 구체적인 질문을 던지는 것입니다."
Q3: 각 학생의 나이는 몇 살인가요?
AGE()는 두 날짜 사이의 간격(interval)을 계산합니다. CURRENT_DATE와 생년월일을 전달하면 년, 월, 일로 구성된 전체 나이를 반환합니다.
SELECT
first_name,
last_name,
...
first_name | date_of_birth | full_age
-----------|---------------|---------------------------
Amina | 2008-03-15 | 18 years 3 mons 21 days
...
출력값은 PostgreSQL의 interval(간격) 타입으로, 읽기 쉬운 기간 형태입니다. 보고서에 사람의 나이를 표시하기에 완벽합니다. 하지만 나이를 숫자로 정렬하거나 비교해야 하는 경우에는 아직 유용하지 않으며, 그 해결책이 바로 Q4에 있습니다.
Q4: 전체 연도 기준 나이 - 중첩(Nesting) 기술
정렬, 필터링 또는 깔끔한 숫자를 표시하려면 연도 단위의 나이만 필요합니다. 해결 방법은 AGE()를 EXTRACT() 안에 중첩(nest)하는 것입니다.
SELECT
CONCAT(first_name, ' ', last_name) AS full_name,
EXTRACT(YEAR FROM AGE(date_of_birth)) AS age_years
...
full_name | age_years
----------------|----------
Njeri Kamau | 19
...
저는 칠판에 연산 순서를 적었습니다. "안쪽의 AGE()가 먼저 실행됩니다. 오늘과 생년월일 사이의 간격(interval)을 계산하죠. 그다음 바깥쪽의 EXTRACT(YEAR FROM ...)가 그 간격에서 연도 숫자만 뽑아냅니다. 두 개의 함수를 사용하여 하나의 깔끔한 정수(integer)를 얻는 것입니다."
이것은 이번 세션의 첫 번째 중첩(nesting) 패턴이며 매우 중요합니다. SQL 함수들이 서로 조합될 수 있으며, 각 함수의 출력이 다음 함수의 입력으로 전달될 수 있음을 보여주기 때문입니다.
Q5: 각 시험은 며칠 전에 있었나요?
PostgreSQL에서의 날짜 산술(date arithmetic)은 간단합니다. 두 날짜를 빼면 그 사이의 일수가 정수(integer)로 반환됩니다.
SELECT
result_id,
exam_date,
...
| result_id | exam_date | days_ago |
|---|---|---|
| 1 | 2024-03-15 | 843 |
| ... |
date1 - date2는 정수(integer)를 반환합니다. 함수가 필요 없습니다. 이 단순함이 핵심입니다. 저는 수업 시간에 이렇게 말했습니다: "두 날짜 사이의 일수가 필요할 때는 그냥 빼기만 하면 됩니다. PostgreSQL이 달력 처리를 알아서 해줍니다. 윤년, 월 길이 등 모든 것을요. 정확한 숫자를 얻을 수 있습니다."
Q6: 결과 발표일은 언제인가요? INTERVAL로 시간 추가하기
INTERVAL을 사용하여 날짜에 시간을 더할 수 있습니다. 값과 단위는 따옴표 안에 함께 들어갑니다.
SELECT
result_id,
exam_date,
...
----------|-------------|--------------------
1 | 2024-03-15 | 2024-03-29
...
INTERVAL '2 weeks'와 INTERVAL '14 days'는 같은 결과를 반환합니다. 제가 직접 두 가지를 실행하여 증명했습니다. 일(days), 주(weeks), 월(months), 년(years), 시간(hours), 분(minutes) 등 문제에 맞는 단위라면 무엇이든 사용할 수 있습니다.
실제 사용 사례: 결제 기한일, 구독 갱신일, 수습 기간 만료일, 보증 만료일. 저장된 날짜에 고정된 기간을 더해야 하는 모든 상황입니다.
Q7: 사람이 읽기 쉬운 형식으로 날짜 표시하기
2024-03-15와 같이 저장된 날짜는 기계가 읽기에 용이합니다. 보고서에는 Friday, 15th March 2024와 같은 형식이 필요합니다. TO_CHAR() 함수가 이 서식을 처리합니다.
SELECT
result_id,
exam_date,
...
----------|-------------|---------------------------
1 | 2024-03-15 | Friday , 15th March 2024
...
알아두면 좋은 점이 하나 있습니다. PostgreSQL은 Day와 Month를 공백으로 채워 고정 너비로 처리하는 경향이 있어,
result_id | exam_date | first_format | month_year
----------|-------------|--------------|------------
1 | 2024-03-15 | 15/03/2024 | March 2024
...
암기해둘 만한 일반적인 형식 코드 (Common format codes):
| 코드 | 의미 | 예시 |
|---|---|---|
DD | 일 (Day) 번호 (앞에 0 포함) | 05 |
| ... |
Q9: 날짜의 일부를 이용한 필터링 (Filtering by Part of a Date)
EXTRACT는 SELECT 절에서만 유용한 것이 아닙니다. WHERE 절 내부에서도 작동합니다. 이는 실제 쿼리에서 가장 흔히 사용되는 날짜 패턴 중 하나입니다.
SELECT
first_name, last_name, date_of_birth
FROM greenwood_academy.students
...
first_name | last_name | date_of_birth
-----------|-----------|---------------
Amina | Hassan | 2008-03-15
...
"전체 날짜를 알 필요는 없습니다. 연도만 있으면 됩니다. EXTRACT가 연도를 추출하고, WHERE가 이를 비교합니다. 이 패턴은 어떤 부분에 대해서도 작동합니다. 3월 생일을 찾기 위해 월(month)로 필터링하거나, 어느 달이든 1일에 태어난 모든 사람을 찾기 위해 일(day)로 필터링할 수 있습니다."
Q10: DATE_TRUNC를 사용하여 월별로 시험 그룹화하기 (Grouping Exams by Month with DATE_TRUNC)
각 월에 몇 번의 시험이 치러졌을까요? 과제는 이렇습니다: 시험 날짜는 특정 일자(2024-03-15, 2024-03-21)로 되어 있습니다. 정확한 날짜로 GROUP BY를 하면 시험당 하나의 행이 생성됩니다. 모든 3월 날짜를 하나의 값으로 맞추어야(snap) 합니다.
DATE_TRUNC()가 이 역할을 수행합니다. 이 함수는 날짜를 지정된 단위의 시작 시점으로 내림(round down) 처리합니다.
SELECT
DATE_TRUNC('month', exam_date) AS exam_month,
COUNT(*) AS total_exams
...
exam_month | total_exams
---------------------|------------
2024-03-01 00:00:00 | 4
...
DATE_TRUNC('month', '2024-03-15')는 2024-03-01을 반환합니다. DATE_TRUNC('month', '2024-03-21')도 마찬가지입니다. 두 3월 날짜 모두 동일한 값이 되므로, 이제 GROUP BY가 이들을 하나의 그룹으로 취급할 수 있습니다.
"DATE_TRUNC는 원본 데이터를 변경하지 않고 일간(daily) 데이터를 주간(weekly) 또는 월간(monthly) 요약 데이터로 전환하는 방법입니다. 날짜를 삭제하는 것이 아니라, 그룹화라는 목적을 위해 일시적으로 무시하는 것입니다."
Q11: 시험이 평일이었나요, 주말이었나요? (Was the Exam on a Weekday or Weekend?)
EXTRACT(DOW FROM date)는 요일을 숫자로 반환합니다: 0 = 일요일, 1 = 월요일, ..., 6 = 토요일.
SELECT
result_id,
exam_date,
...
result_id | exam_date | day_name | day_number | day_type
----------|-------------|-----------|------------|----------
1 | 2024-03-15 | Friday | 5 | Weekday
...
여기서는 두 가지 요소가 함께 작동합니다: TO_CHAR(exam_date, 'FMDay')는 읽기 쉬운 이름을 제공합니다. EXTRACT(DOW FROM exam_date)는 CASE WHEN에서 테스트할 수 있는 숫자를 제공합니다. 일요일은 0이고 토요일은 6입니다. 이 두 값은 주말(Weekend)이며, 그 외의 모든 값은 평일(Weekday)입니다.
이 쿼리는 CASE WHEN이 날짜 함수와 직접 연결되는 첫 번째 사례이기도 합니다. 이는 실제 보고서 작성 시 매우 빈번하게 나타나는 패턴으로, 어떤 일이 발생한 시점에 따라 데이터에 라벨을 붙이는 방식입니다.
Q12: 세션의 마무리 - 생일 알림 (The Session Closer - Birthday Reminders)
이번 세션에서 가장 복잡한 쿼리입니다. "향후 30일 이내에 생일이 있는 모든 학생을 찾으세요.""
과제: 생년월일은 수년 전의 날짜입니다. 2008-03-15는 연도가 다르기 때문에 오늘 날짜 범위와 직접 비교할 수 없습니다. 각 생일의 "올해 버전"을 다시 만들어야 합니다.
SELECT
first_name,
last_name,
...
쿼리를 실행하기 전에 단계별로 나누어 분석했습니다:
1단계 - 현재 연도를 텍스트로 추출: TO_CHAR(CURRENT_DATE, 'YYYY') → '2026'
2단계 - 생일의 월과 일 추출: TO_CHAR(date_of_birth, 'MM-DD') → '03-15'
3단계 - || (PostgreSQL의 문자열 결합 연산자)를 사용하여 결합: '2026' || '-' || '03-15' → '2026-03-15'
4단계 - 해당 텍스트를 실제 날짜로 다시 변환: TO_DATE('2026-03-15', 'YYYY-MM-DD') → 2026-03-15
5단계 - 해당 날짜가 향후 30일 이내에 있는지 확인: BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '30 days'
결과: 다음 달에 생일이 다가오는 모든 학생이 출력되며, 생일은 15th March 형식으로 포맷팅됩니다.
"이것은 실제 현업에서 사용되는 패턴입니다. 학교에서는 생일 명단을 작성할 때 이를 사용합니다. 인사(HR) 시스템에서는 계약 갱신을 위해 사용합니다. 구독 서비스에서는 갱신 알림을 위해 사용합니다. 로직은 항상 동일합니다. 저장된 날짜를 기준으로 올해 버전을 다시 구성한 다음, 해당 날짜가 목표 기간 내에 포함되는지 테스트하는 것입니다."
전체 함수 참조 (The Full Function Reference)
| 함수 (Function) | 기능 | 예시 |
|---|---|---|
CURRENT_DATE | 오늘 날짜 | 2026-07-05 |
| ... |
연습 문제 (Practice Problems)
초급 (Easy):
-- 1. 각 학생의 이름(first name)과 출생 연도를 표시하세요
-- 2. 각 학생의 나이는 며칠인가요? (CURRENT_DATE - date_of_birth)
-- 3. 각 시험 날짜를 'April 2024' 스타일로 포맷팅하세요
중급 (Medium):
-- 각 학생의 전체 이름(full name)과 만 나이(age in whole years)를 표시하세요
-- 나이가 많은 순서부터 적은 순서대로 정렬하세요
...
도전 (Challenge):
-- 월별 시험 요약:
-- 각 시험이 어느 달에 있었는지, 해당 월에 시험이 몇 건 있었는지,
-- 그리고 해당 월이 1학기(1월-4월), 2학기(5월-8월) 중 어디에 속하는지 표시하세요,
...
저는 나이로비(Nairobi)에서 전체 데이터 프로그램을 운영하는 데이터 트레이너(data trainer)입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기