← 학습 카테고리

Learn

Spark

38개 모듈 · 현재 12번째

Spark 모듈 12/38 spark-learn-12

DataFrame 스키마 정의와 컬럼·표현식

Spark: The Definitive Guide (Excerpts, Databricks Preview, 2017) — Chapters 2-6 — Bill Chambers & Matei Zaharia Chapter 5: Basic Structured Operations - Schemas, Columns and Expressions, Records and Rows 절 (PDF pp.54-63)

이 모듈을 다 읽으면

  • schema-on-read 방식과 수동 스키마 정의 방식의 트레이드오프를 설명할 수 있다
  • StructType과 StructField의 구성 요소를 설명할 수 있다
  • col()과 expr()의 차이, 그리고 expr()이 SQL 표현식과 동일한 논리 트리로 컴파일된다는 점을 설명할 수 있다

DataFrame은 Row 타입 레코드와 컬럼(레코드별 계산식)으로 구성되며, 스키마는 컬럼 이름·타입을 정의하고 파티셔닝은 물리적 분산 배치를 정의한다. schema-on-read는 ad hoc 분석에 편리하지만 정밀도 문제로 프로덕션 ETL에는 수동 스키마 정의가 권장된다. 컬럼은 DataFrame 맥락 안에서만 의미를 갖는 표현식이며, expr()로 만든 표현식은 동등한 SQL 표현식과 같은 논리 트리로 컴파일된다.

DataFrame 정의와 파티셔닝 개요

DataFrame은 테이블의 행처럼 생긴 Row 타입의 레코드들과, 스프레드시트의 열처럼 생긴 여러 컬럼으로 구성된다. 각 컬럼은 데이터셋의 개별 레코드마다 수행되는 계산 표현식을 나타낸다. 스키마는 각 컬럼의 이름과 타입을 정의한다.

파티셔닝은 DataFrame(또는 Dataset)이 클러스터 전체에 물리적으로 어떻게 분산 배치되는지를 정의한다. 파티셔닝 스킴은 특정 컬럼의 값을 기준으로 정해질 수도 있고, 비결정적으로 나뉠 수도 있다. 이 챕터는 집계, 윈도우 함수, 조인은 다루지 않고 단일 DataFrame에 대한 연산에 집중한다.

핵심 포인트

  • DataFrame은 Row 타입 레코드와 컬럼(각 레코드에 적용되는 계산식)으로 구성된다
  • 파티셔닝은 DataFrame이 클러스터에 물리적으로 분산되는 방식을 정의하며 특정 컬럼 기준이거나 비결정적일 수 있다

스키마: schema-on-read vs 수동 정의

스키마는 데이터 소스로부터 자동으로 추론(schema-on-read)하거나 명시적으로 직접 정의할 수 있다. ad hoc 분석에서는 schema-on-read가 편리하지만, csv나 json 같은 일반 텍스트 포맷에서는 다소 느릴 수 있고, 예컨대 long 타입이 integer로 잘못 추론되는 등 정밀도 문제가 생길 수 있다. 그래서 프로덕션 ETL에서는, 특히 csv나 json처럼 스스로 타입 정보를 강하게 갖지 않는 소스를 다룰 때는 스키마를 수동으로 정의하는 편이 권장된다 - 추론 결과가 읽어 들이는 데이터에 따라 달라질 수 있기 때문이다.

스키마는 StructType이라는 타입으로 표현되며, 여러 개의 StructField로 구성된다. 각 StructField는 이름, 타입, 그리고 해당 컬럼이 결측치(null)를 허용하는지를 나타내는 boolean 플래그를 갖는다. 만약 런타임에 실제 데이터 타입이 지정한 스키마와 맞지 않으면 Spark는 에러를 던진다.

핵심 포인트

  • schema-on-read는 ad hoc 분석에 편리하지만 정밀도 문제·성능 저하 가능성이 있어 프로덕션 ETL에는 수동 스키마 정의가 권장된다
  • 스키마는 StructType이며 이름·타입·nullable 플래그를 갖는 StructField들로 구성된다
  • 런타임 데이터 타입이 지정한 스키마와 일치하지 않으면 Spark가 에러를 던진다

컬럼과 표현식

사용자 입장에서 컬럼은 스프레드시트나 pandas DataFrame의 컬럼과 비슷하게 느껴진다. 하지만 Spark 입장에서 컬럼은 레코드 하나하나에 대해 표현식(expression)으로 계산되는 값을 나타내는 논리적 구성체일 뿐이다. 그래서 컬럼의 실제 값을 얻으려면 행(row)이 있어야 하고, 행이 있으려면 DataFrame이 있어야 한다 - 즉 컬럼은 DataFrame의 맥락 밖에서는 단독으로 조작할 수 없다.

col()이나 column() 함수로 컬럼을 참조할 수 있는데, 이렇게 참조한 컬럼 이름은 analyzer 단계에서 catalog와 비교되기 전까지는 resolve되지 않는다. 특정 DataFrame에 귀속된 컬럼임을 명시하고 싶으면 `df.col("count")`처럼 쓸 수 있는데, 이는 조인 시 동명 컬럼을 구분해야 할 때 유용하며 Spark가 별도로 그 컬럼을 resolve할 필요가 없어지는 부가 이점도 있다.

expr() 함수는 문자열로부터 표현식과 컬럼 참조를 파싱해낼 수 있는 가장 유연한 방법이다. `expr("someCol - 5")`는 `col("someCol") - 5`나 `expr("someCol") - 5`와 완전히 동일한 트랜스포메이션인데, 그 이유는 이들 모두가 동일한 연산 순서를 나타내는 논리 트리로 컴파일되기 때문이다. 이 논리 트리는 하나의 DAG이며, 중요한 점은 이 트리가 동등한 SQL 표현식(예: `SELECT` 절에 들어갈 수 있는 조건식)과도 완전히 같은 논리 트리로 컴파일된다는 것이다. 즉 DataFrame 코드로 작성하든 SQL 표현식으로 작성하든 동일한 최적화 이점을 얻는다.

핵심 포인트

  • 컬럼은 Spark 관점에서 레코드마다 표현식으로 계산되는 값을 나타내는 논리적 구성체이며 DataFrame 밖에서 단독으로 존재할 수 없다
  • 컬럼 이름은 analyzer 단계에서 catalog와 비교되어야 resolve되며, df.col()로 명시하면 Spark가 별도로 resolve할 필요가 없다
  • expr()로 만든 표현식과 동등한 컬럼 연산·SQL 표현식은 모두 같은 논리 트리(DAG)로 컴파일되어 동일한 최적화를 받는다

Row 다루기

Row는 컬럼 표현식이 연산해 값을 만들어내는 대상이며 물리적 바이트 배열을 표현하는 내부 형식이지만, 사용자에게는 이 바이트 배열 인터페이스가 직접 노출되지 않고 항상 컬럼 표현식을 통해서만 다뤄진다.

중요한 점은 Row 자체는 스키마를 갖지 않는다는 것이다 - 스키마는 DataFrame만 갖는다. 따라서 Row를 수동으로 생성할 때는, 그 Row가 나중에 append될 DataFrame의 스키마와 동일한 순서로 값을 지정해야 한다. 값에 위치로 접근할 때 Scala에서는 `getString(0)`, `getInt(2)`처럼 타입을 명시적으로 강제 변환하는 헬퍼 메서드가 필요하지만, Python에서는 위치에 따라 자동으로 올바른 타입을 반환해 준다.

핵심 포인트

  • Row 자체는 스키마를 갖지 않으며, DataFrame에 append하려면 그 DataFrame 스키마와 동일한 순서로 값을 넣어야 한다
  • Scala에서는 Row 값에 위치로 접근할 때 getString/getInt 같은 헬퍼로 타입을 명시해야 하지만 Python은 자동으로 타입을 반환한다