[SQL] 8. 다양한 데이터 타입 (날짜 및 시간)
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시라고 나와버림)
- ex) 1704176819711 ms ➔ 2024-01-02 15:26:59 (DATETIME) 로 바꾸고 싶은 경우
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로 표현됨)
- `추출할_내용`에 다양한 내용을 설정할 수 있음 : YEAR(연도), MONTH(월), DAY(일), HOUR(시간) 등
DATETIME_TRUNC
- `DATETIME_TRUNC(데이트타임_표현, 자를 부분)` : 자를 부분 이후로는 다 기본값으로 바꿔줌 (ex. 1월, 0 등)
- ➕ 자세한 정보는 링크 참고 : Datetime functions | BigQuery | Google Cloud )
- 사용 예시: `hour_trunc`로 시간대별 고객 수요 집계 (= 몇 시인지가 중요하지 그 뒤의 분,초는 중요하지 않으니까 자름)
PARSE_DATETIME
- `PARSE_DATETIME(문자열_형태, 문자열)` : 문자열로 저장된 DATETIME을 DATETIME 타입으로 변환(파싱)해줌
- `%`로 문자열의 각 부분이 어떤 요소(ex. Year, Month, Hour 등)인지 알려줘야 함
(➕외울 필요는 없고 Format elements | BigQuery | Google Cloud 링크 참고해서 적용하면 됨)
- `%`로 문자열의 각 부분이 어떤 요소(ex. Year, Month, Hour 등)인지 알려줘야 함
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과 정반대!)- "%c" 말고 다른 거 쓰면 다른 타입으로도 바꿀 수 있음 : Format elements | BigQuery | Google Cloud )
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 형식으로 바꾸는 설정 찾아봄
- 정확히 저 형태는 없음😅 (https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements)
- 각 요소를 따로 뽑아서 연결하자 ⇒ 연도는 %Y, 월은 %m, 일은 %d로 지정하면 되네!

4) 앞의 쿼리를 서브쿼리로 감싸고, 바깥 SELECT문에서 FORMAT_DATE 로 변환
- 사실 알아서 DATE인지 DATETIME인지 인식하기 때문에 FORMAT_DATETIME 써도 오류는 안 남
- (문자열을 이렇게 한 번에 다 입력하는 건지 몰랐음!)

▶전체 코드
문제 4. 배틀이 일어난 날짜(battle_date)를 기준으로, 요일별로 배틀이 얼마나 자주 일어났는지 계산해주세요.
▶내 풀이 과정 ✅
1) 데이터 확인
2) EXTRACT에 DAYOFWEEK 설정해서 요일 출력해봄 = 잘 나옴

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
*/