Apache Spark — 느린 잡의 답은 실행 계획과 이벤트 로그에 있다 · DataFrame 과 SQL · 실습
월별 매출·분류별 순위·누적합을 SQL 과 DataFrame 으로
목표
같은 질문을 Spark SQL 과 DataFrame API 로 각각 풀어 같은 물리 계획이 나오는 것을 확인하고, 윈도 함수로 순위·누적합·전월 대비를 계산한다. 마지막으로 정확한 고유 수와 근사 고유 수를 견준다.
왜 중요한가
Spark 에서 SQL 과 DataFrame 은 두 개의 엔진이 아니다. 둘 다 같은 논리 계획으로 바뀌고, 같은 옵티마이저(Catalyst)를 거쳐, 같은 물리 계획으로 실행된다. 그래서 "SQL 이 더 빠르다" 나 "DataFrame 이 더 빠르다" 는 대개 틀린 질문이다. 팀이 무엇으로 쓰든 계획을 보면 같은지 다른지 바로 안다.
집계 다음에 가장 자주 쓰는 것이 윈도 함수다. 행을 줄이지 않고 옆 행을 볼 수 있어서 순위·누적합·전월 대비 같은 보고서 숫자가 전부 여기서 나온다. 윈도의 틀(파티션·정렬·범위)을 잘못 잡으면 오류 없이 틀린 숫자가 나오므로, 틀을 말로 설명할 수 있어야 한다.
고유 수 세기는 셔플이 큰 연산이다. 몇 퍼센트의 오차를 받아들이면 HyperLogLog++ 로 훨씬 싸게 셀 수 있다. 대시보드에는 근사, 청구서에는 정확 — 어느 쪽을 쓸지는 숫자의 쓰임이 정한다.
단계
1. /root/spk/sql/common.py 에 주문·상품을 스키마로 읽어 임시 뷰 orders·products 로 등록하는 load(spark) 를 만들고, /root/spk/sql/sql_monthly.py(앱 spk-sql-monthly)에서 SQL 로 월별 매출(칼럼 month,revenue)을 /root/spk/sql/out/monthly_sql 에 머리줄 있는 CSV 로 쓰세요.
2. /root/spk/sql/df_monthly.py(앱 spk-sql-df)에서 spark.sql 없이 DataFrame API 로 같은 결과를 /root/spk/sql/out/monthly_df 에 쓰세요.
3. /root/spk/sql/plans.py(앱 spk-sql-plans)에서 두 방식의 explain(mode="formatted") 출력을 /root/spk/sql/out/plan_sql.txt·/root/spk/sql/out/plan_df.txt 에 저장한 뒤, 두 DataFrame 을 각각 collect() 로 실제로 돌리세요.
4. /root/spk/sql/top3.py(앱 spk-sql-top3)에서 분류(category)마다 매출 상위 3 상품을 /root/spk/sql/out/top3 에 칼럼 category,product_id,revenue,rank 로 쓰세요. 동점이면 product_id 가 앞선 쪽이 위입니다.
5. /root/spk/sql/running.py(앱 spk-sql-running)에서 2026년 1월의 채널별 일 매출과 채널 안 누적합을 /root/spk/sql/out/running 에 칼럼 channel,day,revenue,running 으로 쓰세요.
6. /root/spk/sql/mom.py(앱 spk-sql-mom)에서 채널별 월 매출과 전월 매출, 증가율(퍼센트, 소수 둘째 자리 반올림)을 /root/spk/sql/out/mom 에 칼럼 channel,month,revenue,prev_revenue,growth_pct 로 쓰세요. 첫 달의 전월 값은 비워 둡니다.
7. /root/spk/sql/distinct.py(앱 spk-sql-distinct)에서 결제 완료 주문을 한 고객 수를 정확히(countDistinct), 그리고 approx_count_distinct(rsd=0.05) 로 세어 /root/spk/sql/out/distinct.json 에 {"exact": 정수, "approx": 정수} 로 쓰세요.
8. /root/spk/sql/report.md 에 ## SQL 과 DataFrame ## 윈도 함수 ## 근사 집계 세 절을 쓰세요. 셋째 절에는 7단계의 두 숫자를 넣으세요.
참고
- 원본:
/data/shop/orders.csv(order_id, customer_id, product_id, qty, order_ts, status, channel),/data/shop/products.csv(product_id, category, price). 매출 = 결제 완료(status='paid') 주문의qty × price입니다. - 스크립트를
common.py와 같은 폴더(/root/spk/sql)에 두면from common import load가 됩니다(실행한 스크립트의 폴더가 파이썬 모듈 경로에 들어갑니다). - 결과 CSV 는 작으니
coalesce(1)로 파일 하나에 모으면 눈으로 보기 쉽습니다(채점기는 파일이 여럿이어도 읽습니다). explain은 표준출력으로 찍습니다.contextlib.redirect_stdout으로 받아 파일에 쓰세요.- 흔한 실수:
rank를 써서 동점에 같은 순위가 둘 나오는 것, 누적합의 창을 기본값(정렬이 있으면 RANGE … CURRENT ROW)으로 두어 같은 날이 겹치는 것, 취소·환불 주문을 매출에 넣는 것. - 공식 문서: [Spark SQL Guide](https://spark.apache.org/docs/4.2.0/sql-programming-guide.html) · [Window Functions](https://spark.apache.org/docs/4.2.0/sql-ref-syntax-qry-select-window.html) · [EXPLAIN](https://spark.apache.org/docs/4.2.0/sql-ref-syntax-qry-explain.html) · [Built-in Functions](https://spark.apache.org/docs/4.2.0/sql-ref-functions-builtin.html)
8단계
- 임시 뷰와 SQL 로 월별 매출
- 같은 질문을 DataFrame API 로
- 두 방식의 물리 계획이 같은지 보기
- 분류별 상위 3 상품 — 윈도 순위
- 채널별 누적합 — 창의 범위 정하기
- 전월 대비 — lag 로 옆 행 보기
- 정확한 고유 수와 근사 고유 수
- 같은 계획·윈도의 틀·근사의 오차를 남기기