날짜와 타임스탬프 다루기
Spark: The Definitive Guide (Excerpts, Databricks Preview, 2017) — Chapters 2-6 — Bill Chambers & Matei Zaharia Chapter 6: Working with Dates and Timestamps 절 (PDF pp.105-111)
이 모듈을 다 읽으면
- Spark의 Date/Timestamp 타입 구분과 TimestampType의 정밀도 제약을 설명할 수 있다
- to_date가 파싱 실패 시 보이는 동작(에러 대신 null)과 그로 인한 위험을 설명할 수 있다
- unix_timestamp와 dateFormat 문자열을 이용해 비표준 형식의 날짜를 안전하게 파싱하는 절차를 설명할 수 있다
Spark는 날짜(Date, 달력 날짜만)와 타임스탬프(Timestamp, 날짜+시간)를 구분하며, inferSchema로 흔한 포맷은 자동 인식하지만 TimestampType은 초 단위까지만 정밀도를 지원한다. date_add/date_sub/datediff/months_between로 날짜 연산을 하고, to_date로 문자열을 날짜로 변환하는데 파싱에 실패해도 에러 없이 null을 반환하므로 파이프라인에서 위험한 버그의 원인이 될 수 있다. 비표준 포맷은 Java SimpleDateFormat 표준에 맞춘 dateFormat 문자열과 unix_timestamp를 함께 써서 안전하게 파싱해야 한다.
Date와 Timestamp, 그리고 정밀도 제약
Spark는 시간 관련 정보를 두 종류로 명확히 구분한다. Date는 달력 날짜만 다루고, Timestamp는 날짜와 시간 정보를 모두 포함한다. inferSchema를 활성화하면 Spark가 흔히 쓰이는 날짜/시간 포맷은 별도 지정 없이도 잘 인식해 컬럼 타입을 자동으로 잡아준다.
다만 중요한 제약이 있다. Spark의 TimestampType은 초 단위(second-level) 정밀도까지만 지원한다. 밀리초나 마이크로초 단위가 필요하다면 이 값을 long 타입으로 다뤄 직접 우회 처리해야 하며, TimestampType으로 강제 변환(coerce)하는 순간 그보다 세밀한 정밀도는 제거된다. Spark는 내부적으로 Java의 날짜/시간 표준을 따른다.
핵심 포인트
- Date는 달력 날짜만, Timestamp는 날짜+시간을 다루며 inferSchema로 흔한 포맷은 자동 인식된다
- TimestampType은 초 단위까지만 정밀도를 지원하므로 밀리초/마이크로초가 필요하면 long으로 우회해야 한다
날짜 연산: add/sub/diff/months_between
current_date()와 current_timestamp() 함수로 오늘 날짜와 현재 시각을 얻는다. date_add(컬럼, n)과 date_sub(컬럼, n)은 날짜에 n일을 더하거나 뺀다.
datediff(날짜1, 날짜2)는 두 날짜 사이의 일수 차이를 반환한다. 달은 일수가 들쭉날쭉하므로, 개월 수 차이가 필요하면 별도로 months_between 함수를 쓴다. to_date(lit("2016-01-01"))처럼 lit으로 감싸 리터럴 문자열을 날짜로 변환하며, 이때 문자열은 반드시 year-month-day(yyyy-MM-dd) 포맷이어야 한다.
핵심 포인트
- date_add/date_sub는 날짜에 일수를 더하고 빼며, datediff는 두 날짜의 일수 차이, months_between은 개월 수 차이를 계산한다
- to_date(lit(문자열))은 기본적으로 yyyy-MM-dd 포맷의 문자열만 올바르게 날짜로 변환한다
파싱 실패는 에러가 아니라 null
Spark는 날짜 파싱에 실패해도 에러를 던지지 않고 조용히 null을 반환한다. 이는 대형 파이프라인에서 특히 위험한데, 예를 들어 날짜 포맷이 year-month-day에서 year-day-month로 바뀐 데이터가 섞여 들어오면, 어떤 행은 정상 파싱되고 어떤 행은 null이 되거나, 심지어 날짜가 뒤바뀐 채로(11월 12일이 12월 11일로) 조용히 잘못 파싱될 수 있다. Spark는 그 값이 실제로 잘못된 것인지 형식만 다른 것인지 알 수 없기 때문에 에러를 내지 않는다.
이를 막으려면 Java SimpleDateFormat 표준에 따라 명시적으로 dateFormat 문자열(예: "yyyy-dd-MM")을 지정하고, unix_timestamp(리터럴, dateFormat)로 먼저 초 단위 Unix 타임스탬프(bigint)로 파싱한 뒤, 이를 timestamp로 캐스팅하고 다시 to_date로 변환하는 절차를 거쳐야 한다.
val dateFormat = "yyyy-dd-MM"
to_date(unix_timestamp(lit("2017-12-11"), dateFormat).cast("timestamp")).alias("date")
날짜/타임스탬프끼리 비교할 때는 날짜/타임스탬프 타입을 쓰거나, 문자열이라면 yyyy-MM-dd 포맷을 지켜야 올바르게 비교된다.
핵심 포인트
- to_date를 포함해 Spark의 날짜 파싱은 실패 시 에러 대신 null을 반환하며, 포맷이 뒤섞인 경우 일부 행이 조용히 잘못 파싱될 위험이 있다
- 비표준 포맷은 Java SimpleDateFormat 문자열을 unix_timestamp에 지정해 초 단위로 먼저 파싱한 뒤 timestamp로 캐스팅하는 절차로 안전하게 처리한다