Spark를 사용하다 보면 null과 관련된 이슈는 피할 수 없다.
특히 join이나 fillna 작업 중에는 생각보다 복잡하고 직관적이지 않은 동작을 마주하게 된다.
이번 글에서는 Spark DataFrame에서 null이 join에 미치는 영향과 fillna의 문자열 타입 제약에 대해 정리한다.
1. null은 조인 대상이 아니다
Spark에서 두 DataFrame을 join할 때, 조인 키가 null인 경우는 절대 매칭되지 않는다.
이는 SQL의 기본적인 조인 규칙과 동일하지만, Spark에서는 이 특성이 실무에서 꽤 자주 문제를 일으키는 것 같다.
이에 대한 사실을 몰랐을 때, 똑같은 데이터인데도 불구하고 실행할때마다 계속해서 중복이 일어나는 경우가 있었다.
"left_anti"란 A 테이블에서 B테이블이 없는 경우, 즉 이미 존재하고 있는 existing_df가 아닌 새로운 df만 테이블에 insert 하려고 할때 사용하는 Join 방식인데, 똑같은 데이터 파일로 계속 수행한다면, "새로운 데이터가 없습니다" 라고 print 되어야 하지만, 자꾸 새로운 데이터가 생겨났다.
non_dup_df = spark_df.join(existing_df, on=[*df_cols], how="left_anti")
데이터를 확인해보니 특정한 컬럼에 null값들이 들어가 있었구 이에 대한 것은 조인이 되지 않기 때문에 계속 새로운 데이터가 생겨났던 것이였다.
해결로써는 조인 전에 null 값을 특정 값(예: 'UNKNOWN'이나 '') 으로 대체해주는 방법이 있다.
2. fillna는 문자열 타입에는 적용되지 않는다?
많은 사람들이 Spark의 fillna는 모든 타입에 적용 가능하다고 생각하지만, 실제로는 다르다.
특히 컬럼별로 대체할 때, string 컬럼에 None을 채우려면 주의해야 한다.
데이터프레임 내에 string 타입이 아닌 다른 타입이 같이 있고, 전체에 fillna를 걸었다면 다른 type에 안 먹는 경우가 있다.
fillna는 string에만 먹히기 때문이다.
#null값 빈값으로 변환
spark_df = spark_df.fillna(" ", subset=df_cols)
#date_type 형식이라 null은 1111-11-11로 치환완료
spark_df =spark_df.withColumn('date', when(col('date').isNull(), to_date(lit('1111-11-11'))).otherwise(col('date')))
해결로써는 명시적으로 컬럼 타입을 확인하고 별도로 처리해줘야 한다.
타입을 지정하고 fillna를 한다음에 다시 해당 타입을 date 타입으로 변경해도 빈값들은 모두 null로 처리된다.
그럼 또 새로운 데이터가 생겨난다.
그래서 string이 아닌 date 타입들은 1111-11-11로 지정했다.
Spark에서 null은 단순한 빈 값이 아니라, 처리 흐름을 바꿔버리는 복병이다.
조인 시 조인 대상이 되지 않고, fillna 시에도 타입에 따라 무시되기 때문에, 항상 다음 사항을 체크해보자.
• 데이터에 null이 있는지?
> 있다면 null을 대체하고 조인해야 한다.
• fillna를 사용하는 컬럼 타입이 string인지?
> 다른 타입이라면 해당 값을 fillna가 아닌 다른 값으로 메꿔야 한다.