SQL/BigQuery

[SQL] 8. 다양한 데이터 타입 (날짜 및 시간)

simon919 2025. 2. 13. 12:47

 

1. 날짜 및 시간 데이터

날짜 및 시간 데이터 타입

1) 시간 데이터 타입은 아래와 같은 종류가 있음

  • DATE : 날짜만 표시 (ex. `2024-12-31`)
  • TIME : 시간만 표시 (ex. `14:59:58.00`)
  • DATETIME : 날짜와 시간을 표시 (ex. `2024-12-31 14:59:58`)
    • 단, TimeZone 정보는 없음!
  • TIMESTAMP : UTC(또는 특정 기준)부터 경과한 시간을 나타내는 값 (ex. `2024-12-31 14:59:58 UTC`)
    • TimeZone 정보 있음! (스탬프 찍었으니까)
    • 대부분 테이블에 시간 데이터가 TIMESTAMP로 저장된 경우가 많은데, DATETIME으로 변환 자유자재로 할 줄 알아야 함!
타임존(TimeZone) 개념을 이해해야 함!!!

GMT(Greenwich Mean Time)
 - 영국 그리니치 천문대(경도 0°)를 기준으로 지역에 따른 시간 차이를 조정하기 위해 생긴 구분선

 - ex) 한국 시간 = GMT+9
UTC(Universal Time Coordinated, 협정세계시)
 - 국제적인 표준 시간. 최근에는 GMT보다 UTC를 많이 사용함. (차이는 거의 미약하긴 함)

 - ex) 한국 시간 = UTC+9
 - '타임존이 존재한다' = 특정 지역의 표준 시간대가 존재한다

 

 

2) 초(sec)보다 더 짧은 시간 단위도 있음

  • Millisecond (ms)
    • 1/1000초 (즉, 1초 = 1,000ms)
    • 1초보다 더 정확하고 빠른 반응이 필요한 분야에 사용됨 (ex. 개발에서 서버 요청 처리 순서 등)
    • `TIMESTAMP_MILLIS`를 사용해 TIMESTAMP 타입으로 변환 가능
  • Microsecond (μs)
    • 1/1000ms (즉, 1초 = 1,000ms = 1,000,000μs)
    • `TIMESTAMP_MICROS`를 사용해 TIMESTAMP 타입으로 변환 가능
    • millisecond보다는 덜 흔하지만, 혹시 회사에서 이렇게 저장된 경우엔 알고 있어야 함

 

TIMESTAMP와 DATETIME 비교

1) 시간 데이터가 TIMESTAMP로 저장된 경우가 많은데, DATETIME으로 변환 자유자재로 할 줄 알아야 함!

  • `DATETIME(타임스탬프 정보, 타임존 정보)`와 같이 쿼리 작성!
  • 보통 millisecond ➔ TIMESTAMP  DATETIME으로 변환해서 사용함
    • ex) 1704176819711 ms ➔ 2024-01-02 15:26:59 (DATETIME) 로 바꾸고 싶은 경우
      • 단, TIMESTAMP ➔ DATETIME할 때 타임존 정보 반드시 입력! (ex. `'Asia/Seoul'`)
      • 타임존 없이 변환하면, 데이터가 잘못 나오게 됨(ex. 오후 3시에 구매한 건데 오전 6시라고 나와버림)
SELECT
    TIMESTAMP_MILLIS(1704176819711) AS milli_to_timestamp_value, -- 밀리초 → TIMESTAMP
    TIMESTAMP_MICROS(1704176819711000) AS micro_to_timestamp_value, -- 마이크로초 → TIMESTAMP (밀리초에서 000 붙이면 됨)
    DATETIME(TIMESTAMP_MILLIS(1704176819711)) AS datetime_value, --❎TIMESTAMP → DATETIME
    DATETIME(TIMESTAMP_MILLIS(1704176819711), 'Asia/Seoul') AS datetime_value_asia --✅이게 올바른 변환
    -- 또는 TIMESTAMP_MICROS(1704176819711000) 처럼 마이크로초 사용해도 됨

 

2) ⭐TIMESTAMP와 DATETIME이 헷갈린다면?

  • 동일한 시간에 대해 TIMESTAMP와 DATETIME을 함께 출력해보면... (참고로, `CURRENT_TIMESTAMP()`는 현재 타임스탬프 정보를 알려줌)
  • TIMESTAMP : 타임존이 UTC라고 나옴. `한국 시간 - 9시간`으로 나옴 (=01시39분)
  • DATETIME : 타임존 없어서 그냥 T(=time)라고 나옴. 한국 시간과 동일하게 나옴 (=10시39분)

 

 

2. 시간 데이터 관련 함수

CURRENT_DATETIME

  • `CURRENT_DATETIME()` : 현재 DATETIME을 출력 (타임존 정보를 넣을 수도 있음)
    • ex) 한국 타임존 넣은 경우와 넣지 않은 경우 9시간 차이 나는 것을 볼 수 있음
  • `CURRENT_DATE()` : 현재 날짜를 출력 (타임존 정보를 넣을 수도 있음)

 

EXTRACT

  • `EXTRACT(추출할_내용 FROM 데이트타임_표현)` : DATETIME에서 특정 내용만 추출해줌
    • `추출할_내용`에 다양한 내용을 설정할 수 있음 : YEAR(연도), MONTH(월), DAY(일), HOUR(시간) 등
      (➕더 자세한 정보는 링크 참고: Datetime functions  |  BigQuery  |  Google Cloud )
    • 요일은 DAYOFWEEK로 추출 가능 (일요일을 시작으로, 1~7로 표현됨)

 

DATETIME_TRUNC

  • `DATETIME_TRUNC(데이트타임_표현, 자를 부분)` : 자를 부분 이후로는 다 기본값으로 바꿔줌 (ex. 1월, 0 등)
  • 사용 예시: `hour_trunc`로 시간대별 고객 수요 집계 (= 몇 시인지가 중요하지 그 뒤의 분,초는 중요하지 않으니까 자름)

PARSE_DATETIME

  • `PARSE_DATETIME(문자열_형태, 문자열)` : 문자열로 저장된 DATETIME을 DATETIME 타입으로 변환(파싱)해줌
SELECT
    PARSE_DATETIME('%Y-%m-%d%H:%M:%S', '2024-01-1112:35:35') AS parse_datetime;

 

FORMAT_DATETIME

  • `FORMAT_DATETIME("%c", 데이트타임_표현)` : DATETIME 타입 데이터를 문자열 타입으로 변환해줌
      (🆚 PARSE_DATETIME과 정반대!)
SELECT
    FORMAT_DATETIME("%c", DATETIME "2024-01-1112:35:35") AS formatted;

 

LAST_DAY

  • `LAST_DAY(데이트타임_표현, 기준)` : Month 또는 Week의 마지막 날짜가 필요한 경우에 사용
    • ex) 매달 마지막 날짜 기준으로 D-day 계산해서 표시해주세요
  • 기준의 기본값은 `Month`, 그 주의 마지막 값이 필요하면 `Week`로 설정
    • `Week`는 일주일의 시작을 Sunday/Monday 중에 선택 가능 (주로 Sunday 사용)

 

DATETIME_DIFF

  • `DATETIME_DIFF(데이트타임1, 데이트타임2, 기준)` : 두 DATETIME의 차이를 계산
    • 몇 개월(MONTH) / 몇 주(WEEK) / 며칠(DAY) 차이나는지를 계산할 수 있음
    • (데이트타임1 - 데이트타임2)로 계산함 = 더 나중의 날짜를 앞에 넣어야 +값으로 나옴
  • ex) 21년 1월 1일과 24년 4월 2일의 차이 계산

 

 

👨‍🏫DATETIME 함수 위주로 다루었으나, 대부분 TIMESTAMP, DATE 타입에도 사용 가능한 함수니까 확장해서 사용하면 됨. 자주 쓰는 함수(TIMESTAMP_MILLIS, PARSE_DATETIME) 위주로 기억하고, 필요할 때 찾아서 사용하면 됨!

  • ex) FORMAT_DATE도 사용 가능함

 

 

3. 연습 문제

문제 1. 트레이너가 포켓몬을 포획한 날짜(catch_date)를 기준으로, 2023년 1월에 포획한 포켓몬의 수를 계산해주세요.

 

▶내 풀이 과정❎

더보기

문제1 풀이 과정

1) 일단 month 부분만 추출되는지 확인

2) `WHERE`로 조건 추가해서 출력해봄

 3) catch_month 기준으로 그룹화해서 `COUNT` 해보니 나옴

4) 정확히 1월 값만 뽑기 위해 `HAVING` 추가


▶강사님 풀이 과정✅

더보기

1) 데이터 특징 파악⭐

  • catch_date가 UTC 기준일 경우 KR 기준으로는 날짜가 달라질 수 있음 = 확인이 필요한 상황
    • catch_date 컬럼과 catch_datetime 컬럼을 KR 기준으로 직접 변환해서, catch_date와 차이나는지 체크
    • 똑같으면 그냥 써도 되지만, 차이가 나면 catch_date 컬럼은 쓸 수 없음!

 

2) 데이터 검증 (catch_date의 타임존에 대한 검증)

  • 실제로 차이나는 row들이 발견됨
  • 서브쿼리로 감싸서, 두 컬럼 값이 다른 행들만 뽑아봄 ⇒ 141건 발견 (= : 같은 행은 238건)
  • ➡️즉, catch_date를 그냥 사용하면 제대로 된 답을 얻지 못할 수 있겠구나!

 

3) 2023년 & 1월 데이터만 추출 (EXTRACT)

  • 먼저 YEAR 기준으로 출력해본 뒤, AND 쓰고 MONTH 기준도 추가

4) 2023년 1월에 잡힌 포켓몬의 '개수'를 원하는 거니까 COUNT 추가해줌
  ➡️ 답=85개

👨‍🏫출제 의도 : 요청이 들어온 걸 그대로 보면 틀릴 수 있다! 반드시 주체적으로 컬럼을 파악한 뒤 쿼리를 작성하자!


▶전체 코드

더보기
# 쿼리 작성하는 목표, 확인할 지표 : 포켓몬의 수
# 쿼리 계산 방법 : count
# 데이터의 기간 : 2023년 1월!
# 사용할 테이블 : trainer_pokemon
# JOIN KEY : 없음
# 데이터의 특징 :
    -- catch_date : DATE 타입
    -- catch_datetime : TIMESTAMP 타입 (이런 오차 있을 수 있으니 데이터 확인이 필수!!)
    -- 또, catch_date가 KR 기준인지 UTC 기준인지도 확인이 필요함;;

# 0. 데이터 검증을 위한 쿼리
SELECT
    *
FROM(
SELECT
    id,
    catch_date,
    DATE(DATETIME(catch_datetime, "Asia/Seoul")) AS catch_datetime_kr_date
    -- TIMESTAMP > DATETIME > DATE 순으로 변환
FROM basic.trainer_pokemon
)
WHERE
    catch_date != catch_datetime_kr_date;

# 1. 본 문제 해결을 위한 쿼리
SELECT
    COUNT(DISTINCT(id)) AS cnt -- DISTINCT는 혹시 모를 id 중복을 대비해서 적은 것
FROM basic.trainer_pokemon
WHERE
    EXTRACT(YEAR FROM DATETIME(catch_datetime, "Asia/Seoul")) = 2023
    -- catch_datetime이 TIMESTAMP 형태로 저장되어있으므로 DATETIME으로 변경해서 사용
    AND EXTRACT(MONTH FROM DATETIME(catch_datetime, "Asia/Seoul")) = 1

/* 내 풀이
SELECT
    EXTRACT(MONTH FROM catch_date) AS catch_month,
    COUNT(id) AS cnt
FROM basic.trainer_pokemon
GROUP BY
    catch_month
HAVING
    catch_month = 1
*/

 

 

문제 2. 배틀이 일어난 시간(battle_datetime)을 기준으로, 오전 6시에서 오후 6시 사이에 일어난 배틀의 수를 계산해주세요.

 

▶내 풀이 과정✅

더보기

1) 문제1에서 배운 대로, 데이터 특징 파악 먼저 함

  • battle_datetime은 KR 기준의 DATETIME 타입으로 잘 저장되어있으니, 그대로 사용해도 될 듯

2) 오전 6시 xx분에 일어난 것만 시험삼아 뽑아 봄 / 17시 xx분도 뽑아 봄

3) 비교 연산자로 '6시 xx분 ~ 17시 xx분'에 해당하는 행들만 추출 (=37건)

4) COUNT 추가해서 개수만 출력!


▶강사님 풀이 과정✅

더보기

1) 데이터 특징 파악

  • battle_datetime과 DATETIME(battle_timestamp, "Asia/Seoul")이 같은지 쿼리로 확인
    (⭐눈으로 대충 같아보일지라도, 반드시 쿼리로 확인)
  • (문제 1처럼) 위 쿼리에다 서브쿼리로 작성해도 되지만, 이번엔 COUNTIF 활용해봄
    • '='으로 같은 행의 개수를, `!=`로 다른 행의 개수를 출력!
    • 또는 '='만 하고, '테이블 세부 정보'에 가서 행 개수 확인해도 됨
  • 모든 행에서 일치함 ➡️즉, battle_datetime 그대로 사용해도 되겠구나!

2) EXTRACT로 hour 부분만 추출하고, 비교 연산자로 조건 설정

  • 내 풀이 과정과 동일! (단, 18로 하면 등호 빼야 함)

2-1) ➕BETWEEN을 사용하면 쿼리가 좀 더 간단해짐!

  • BETWEEN : 숫자형 뒤에 사용해서 범위 조건을 지정할 수 있음

▶전체 코드

더보기
# 쿼리 작성하는 목표, 확인할 지표 : 배틀의 수
# 쿼리 계산 방법 : count
# 데이터의 기간 : 6시 - 18시
# 사용할 테이블 : battle
# JOIN KEY : 없음
# 데이터의 특징 :
    -- battle_date : battle_datetime 기반으로 만들어진 DATE 타입
    -- battle_datetime : DATETIME 타입
    -- battle_timestamp가 UTC 기준인데 9시간 차이나는 걸 보니 KR 기준 잘 되어있음

# 0. battle_datetime, battle_timestamp 검증
SELECT
    -- id,
    -- battle_datetime,
    -- DATETIME(battle_timestamp, "Asia/Seoul") AS battle_timestamp_kr
    COUNTIF(battle_datetime = DATETIME(battle_timestamp, "Asia/Seoul")) AS battle_datetime_same_battle_timestamp_kr,
    COUNTIF(battle_datetime != DATETIME(battle_timestamp, "Asia/Seoul")) AS battle_datetime_diff_battle_timestamp_kr
FROM basic.battle;

# 1. 본 문제 풀이 (1)
SELECT
    COUNT(DISTINCT(id)) AS battle_cnt
FROM basic.battle
WHERE
    EXTRACT(HOUR FROM battle_datetime) < 18
    AND EXTRACT(HOUR FROM battle_datetime) >= 6

# 1. 본 문제 풀이 (2)
SELECT
    COUNT(DISTINCT(id)) AS battle_cnt
FROM basic.battle
WHERE
    EXTRACT(HOUR FROM battle_datetime) BETWEEN 6 AND 17
    -- EXTRACT의 결과가 숫자형이니까 BETWEEN 사용 가능

/* 내 풀이
SELECT
    COUNT(DISTINCT(id)) AS cnt
FROM basic.battle
WHERE
    EXTRACT(HOUR FROM battle_datetime) <= 17
    AND EXTRACT(HOUR FROM battle_datetime) >= 6
*/

 

 

문제 2-1 (보너스). 배틀이 일어난 시간(battle_datetime)을 기준으로, 시간대별로 배틀의 수가 몇 건인지 출력해주세요.

 

▶내 풀이 과정

더보기

1) GROUP BY에 EXTRACT 결과를 바로 입력함

  • 정답과 결과는 동일하게 나옴!

▶강사님 풀이 과정✅

더보기

1) 'hour'라는 컬럼을 별도로 만든 뒤, 서브쿼리로 다시 호출

SELECT
    hour,
    COUNT(DISTINCT(id)) AS battle_cnt
FROM(
    SELECT
        *, -- 이렇게 * 뒤에 콤마 붙이면 컬럼 추가 가능
        EXTRACT(HOUR FROM battle_datetime) AS hour
    FROM basic.battle
)
GROUP BY
    hour
ORDER BY
    hour

- 질문 내용을 찾아본 결과, 스타일 차이라서 크게 문제되지 않는다고 답변받은 것을 확인!


 

문제 3. 각 트레이너별로 그들이 포켓몬을 포획한 첫 날(catch_date)을 찾고, 그 날짜를 'DD/MM/YYYY' 형식으로 출력해주세요. (예시: 2024-01-01 ⇒ 01/01/2024)

 

▶내 풀이 과정❎

더보기

1) 데이터 확인

  • 트레이너별로 catch_date가 여러 개인지 보기 위해, ORDER BY  trainer_id 출력
  • trainer 1명 당 여러 개의 catch_date가 있네..

2) MIN 적용되는지 해봤는데 됨..!

3) trainer_id로 그룹화해서 적용하니까 '첫 포획 날짜'는 계산 완료

4) DD/MM/YYYY 형식으로 변경하려고 EXTRACT로 각 부분 추출 후 FORMAT_DATETIME 시도

  • 근데 EXTRACT 하니까 숫자형이 되어버려서 FORMAT_DATETIME 적용이 안 됨 ... (여기서 막혔음)

▶강사님 풀이 과정✅

더보기

1) 데이터 확인

2) 포획한 '첫 날'은 날짜 중에 제일 작은 값이니까 MIN 함수로 출력

  • (내 풀이 : MIN 쓴 거까진 잘했는데, "Asia/Seoul"로 타임존 맞추는 걸 빠뜨림)

 3) FORMAT_DATE 문서 들어가서, DD/MM/YYYY 형식으로 바꾸는 설정 찾아봄

4) 앞의 쿼리를 서브쿼리로 감싸고, 바깥 SELECT문에서 FORMAT_DATE 로 변환

  • 사실 알아서 DATE인지 DATETIME인지 인식하기 때문에 FORMAT_DATETIME 써도 오류는 안 남
  • (문자열을 이렇게 한 번에 다 입력하는 건지 몰랐음!)

▶전체 코드

 

 

문제 4. 배틀이 일어난 날짜(battle_date)를 기준으로, 요일별로 배틀이 얼마나 자주 일어났는지 계산해주세요.

 

▶내 풀이 과정 ✅

더보기

1) 데이터 확인

2) EXTRACTDAYOFWEEK 설정해서 요일 출력해봄 = 잘 나옴

3) 서브쿼리로 감싸고, '요일'에 대한 컬럼을 그룹화해서 COUNT 계산!


▶강사님 풀이 과정✅

더보기

1) 내 풀이와 동일하고, 아래 사항들만 약간 다름

  • 처음 쿼리에서 SELECT * 사용함
  • COUNT할 때 DISTINCT도 같이 작성함 (=이게 더 권장되긴 함!)
  • ORDER BY 정렬 추가함

▶전체 코드

더보기
# 쿼리 작성하는 목표, 확인할 지표 : 요일별 배틀 횟수
# 쿼리 계산 방법 : count
# 데이터의 기간 : 
# 사용할 테이블 : battle
# JOIN KEY : 없음
# 데이터의 특징 :
  -- 문제2 풀 때 battle_date는 KR 기준인 거 확인했었음 (그대로 사용 가능)

SELECT
  day_of_week,
  count(distinct(id)) as battle_cnt
FROM(
    SELECT
      *,
      extract(DAYOFWEEK from battle_date) AS day_of_week
    FROM basic.battle
)
GROUP BY day_of_week
ORDER BY day_of_week -- 월요일부터 나오도록 정렬

/* 내 풀이 과정
SELECT
  battle_dayofweek,
  count(id) as battle_cnt
FROM(
    SELECT
      id,
      battle_date,
      extract(DAYOFWEEK from battle_date) AS battle_dayofweek
    FROM basic.battle
)
GROUP BY
  battle_dayofweek

*/

 

 

문제 5. 트레이너가 포켓몬을 처음으로 포획한 날짜와 마지막으로 포획한 날짜의 간격이 큰 순으로 정렬하는 쿼리를 작성해주세요.

 

▶내 풀이 과정✅

더보기

1) 데이터 확인

2) MIN, MAX로 첫 날과 마지막 날 출력해봄

3) 서브쿼리로 감싸고, DATETIME_DIFF로 차이 계산

  • 처음에 모르고 - (뺄셈) 연산자로 계산했는데 약간 이상하게 나와서 수정함

4) 잘 출력되는 걸 확인 후, ORDER BY로 마무리!


▶강사님 풀이 과정✅

더보기

1) 내 풀이와 동일하고, 아래 사항들만 약간 다름

  • MIN, MAX 계산 시, DATE가 아니라 DATETIME을 사용함
  • 트레이너 정보를 요청한 거니까 trainer_id 컬럼도 같이 출력함 (바깥 쿼리에서 SELECT * 사용)

2) ⭐데이터 검증하는 과정도 필수

  • 날짜 계산기로 하면 됨! 날짜 하나 잡고 확인!

▶전체 코드

더보기
# 쿼리 작성하는 목표, 확인할 지표 : 첫날과 마지막날의 간격 / 큰 순 정렬
# 쿼리 계산 방법 : (MAX-MIN) / ORDER BY DESC
# 데이터의 기간 : 
# 사용할 테이블 : trainer_pokemon
# JOIN KEY : 없음
# 데이터의 특징 :
  -- 문제1 풀 때 catch_date는 KR 기준이 아니니까 주의해야 함을 확인했었음

SELECT
  max_catch_date,
  min_catch_date,
  DATETIME_DIFF(max_catch_date, min_catch_date, DAY) AS day_diff
FROM(
    SELECT
      trainer_id,
      MAX(DATE(catch_datetime, "Asia/Seoul")) AS max_catch_date,
      MIN(DATE(catch_datetime, "Asia/Seoul")) AS min_catch_date,
    FROM basic.trainer_pokemon
    GROUP BY
      trainer_id
)
ORDER BY day_diff DESC

/* 내 풀이 과정
SELECT
  max_catch_date,
  min_catch_date,
  DATETIME_DIFF(max_catch_date, min_catch_date, DAY) AS day_diff
FROM(
    SELECT
      trainer_id,
      MAX(DATE(catch_datetime, "Asia/Seoul")) AS max_catch_date,
      MIN(DATE(catch_datetime, "Asia/Seoul")) AS min_catch_date,
    FROM basic.trainer_pokemon
    GROUP BY
      trainer_id
)
ORDER BY day_diff DESC
*/

 


🙏References