PySparkとは? PythonやSQLと何が違うのか実際に比較してみた
PySparkという名前は前から耳にしていたが、ぶっちゃけ最初は何者なのかよく分かっていなかった。
「PySpark」っていうくらいだし、Pythonを使って何かやるライブラリなのだろうとは思っていたが、
普通のPythonと何が違うんだろう?
という部分がイマイチピンときていなかった。
さらに調べていると、Spark SQLなんて言葉も登場。
SQLで集計できるなら、わざわざPySpark使わなくてもよくね?
という新たな疑問も浮上。
もう一つ気になったのが「大量データ処理」の文脈。
なぜデカいデータを扱うときにPySparkがもてはやされるのか。
「Pythonより単に速いからなのかな?」とも思ったが、考えててもラチがあかないので、今回はWindowsのローカル環境でPySparkを動かしてPythonやSQLite + SQLと比較してみることにした。
PySparkの基本的な仕組みを調べつつ、同じデータをPython・SQL・PySparkで回してみて、実行計画や処理時間も確認してみる。
この記事の対象読者
- PySparkという言葉を初めて聞いた人
- PySparkと普通のPythonの違いがピンときていない人
- SQLとPySparkの違いや使い分けが分からない人
- Sparkが裏側で何をやっているのか知りたい人
- ローカル環境でPySparkをちょっと試してみたい人
まず結論
今回調べて、実際に動かしてみて分かったことを先にまとめておく。
結論1:PySparkは「Pythonを高速化したやつ」ではない
PySparkは、Apache SparkのPython API。
つまり、Pythonそのものを爆速にするライブラリというより、PythonからSparkのデータ処理エンジンを操作するための入口と考えた方がしっくりくる。PySparkは公式でも「Python API for Apache Spark」と説明されていて、PyPIからサクッとインストールしてローカルで試すこともできる。
結論2:Spark SQLとPySparkは単純な別物ではない
PySparkからSpark SQLを普通に実行できる。
しかもSpark内部では、SQLで書こうがDataFrame APIで書こうが、最終的な計算にはまったく同じ実行エンジンが使われる仕組みになっている。
なので、
「SQLかPySparkか、どちらか一方を選ぶ」
みたいな単純な二者択一の関係ではないっぽい。
結論3:PySparkだからといって必ずPythonより速いわけではない
今回1,000万行のデータで検証してみたところ、読み込み後の集計処理だけを見れば local[5] のPySparkが一番速かった。
ただ、Sparkの起動時間やCSV読み込みまで含めたトータル実行時間だと、今回のローカル環境ではまさかのPython単体の方が速いという結果に。
つまり、
「PySparkを使えばPythonより絶対に速くなる」
とは一概には言えない。
「PySparkってPythonと何が違うの?」という素朴な疑問に対しては、速度というより処理を実行する仕組みそのものが根本的に違うと捉えるのが一番しっくりきた。
そもそもPySparkとは何者?
基本的な定義
PySparkは、Apache SparkをPythonから操作するためのAPI。
Apache Spark自体は、大規模なデータを扱うための分散処理基盤。PySparkを使うことで、PythonからSparkのDataFrameやSQL機能を扱えるようになる。
イメージ的にはこんな感じ。
Python
↓
PySpark
↓
Apache Spark
↓
データ処理ここが最初に少し混乱したポイントだった。
「PySpark」という名前から、最初は「Python製の高速データ処理ライブラリ」みたいなイメージを持っていたが、実際はPythonからSparkという別物を動かすための窓口と考える方がしっくりくる。
Sparkの内部はどうなってるの?
ここで「じゃあそもそもSparkって中で何やってるの?」という話。
Sparkアプリケーションは、主に Driver と Executor というプロセス群で動いている。
公式ドキュメントによると、Driverが SparkContext を保持して全体を指揮し、Executorが実際にタスクを実行してデータをメモリ上に保持する構成になっている。Driverから各ExecutorへTaskが配られるイメージ。
構成を図にするとこんな感じ。
flowchart TB
App["Sparkアプリケーション"]
Driver["Driver (SparkContext)"]
Executor1["Executor 1"]
Executor2["Executor 2"]
Task1["Task 1"]
Task2["Task 2"]
Data1["Data 1"]
Data2["Data 2"]
App --> Driver
Driver --> Executor1
Driver --> Executor2
Executor1 --> Task1
Executor2 --> Task2
Task1 --> Data1
Task2 --> Data2今回の検証は1台のWindows PC上でSparkを動かしている。
なので本格的な複数台マシンによる分散処理ではない。
とはいえ、Sparkの分散処理モデルの雰囲気をローカルで体感することは十分可能。
local[1] と local[5] って何?
Sparkをローカル環境で動かすとき、以下のような指定ができる。
local[1]
local[5]
local[*]Spark公式での意味は以下の通り。
local[1]→ 1つのワーカースレッドで動かすlocal[K]→ K個のワーカースレッドで動かすlocal[*]→ マシンの論理コア数いっぱいで動かす
今回の検証PCは20論理プロセッサ搭載だが、CPU負荷を考慮して local[1] と local[5] の2パターンで試してみた(local[*] は今回は見送り)。
なぜにSparkの概念が必要になるのか?
ここが一番気になった部分。
単にデータを集計するだけなら、
CSV
↓
Python
↓
集計で済むし、SQLを使うにしても、
CSV
↓
SQLite
↓
SQL
↓
集計で十分なはず。
じゃあ、なんでわざわざSparkなんて大掛かりなものが必要になるのか?
Spark公式のクラスタ概要を見ると、DriverがTaskを分散し、複数のExecutorで並列処理する前提で作られている。つまり最初から「1つのPythonプロセスで全データを順番に処理する」という世界観ではない。
なのでSparkの本当の価値は、
Pythonコードを単に高速化すること
というより、
超巨大なデータを複数のブロックに分解して並列実行できる仕組み
にあると考える方が自然っぽい。
ただし、このあたりの恩恵は1台のPCのローカル検証だけだと全貌は見えにくいかもしれない。
PySparkのDataFrameって普通のリストやPandasと違うの?
PySparkでは、データをDataFrameという単位で扱う。
コードの書き方自体はこんな感じ。
result = (
df.withColumn(
"total_sales",
col("price") * col("quantity")
)
.groupBy("category")
.agg(
spark_sum("total_sales").alias("total_sales")
)
.orderBy("category")
)一見すると、普通のPythonオブジェクトをメソッドチェーンで操作しているように見える。
が、PySparkのDataFrameはPythonの普通のリストやDictとは別物。
Spark公式によると、DataFrameは構造化されたデータの「分散コレクション」として定義されている。さらにSpark SQLの最適化エンジン(Catalyst Optimizerなど)が働くことで、計算手順が最適化されてから実行される。
Pythonの for row in rows: みたいに1行ずつ真面目に処理していくのとは、根本的に仕組みが違う。
実際にPython・SQL・PySparkを比較してみた
というわけで、ここから実際に動かして比較してみる。
検証環境
| 項目 | 内容 |
|---|---|
| OS | Windows 11 |
| CPU | Intel Core Ultra 7 265K |
| 論理プロセッサ | 20 |
| メモリ | 約63.35GB |
| Python | 3.13.15 |
| Java | Temurin 21.0.12.1 |
| PySpark | 4.2.0 |
| Spark | 4.2.0 |
| Spark実行方式 | local[1] / local[5] |
| SQL | SQLite |
※PySpark 4.2.0公式では Python 3.10+、Java 17+ が要件。今回は Python 3.13.15 と Java 21 を使用。
今回使用したデータ
検証用に、以下のようなシンプルな売上データ(CSV)を用意した。
id,product,category,price,quantity
1,Apple,A,100,3
2,Banana,B,200,2
3,Orange,A,150,4
4,Grape,C,300,1
5,Melon,B,500,2
...ここから、
売上金額 = price × quantityを計算し、category ごとに合計売上金額を集計する。
たとえば10行のダミーデータなら、期待する結果は以下。
| category | total_sales |
|---|---|
| A | 1700 |
| B | 2600 |
| C | 1770 |
検証1:Python単体で集計する
まずは一番素直なPython標準ライブラリでの処理。
with CSV_FILE.open(newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
category = row["category"]
price = int(row["price"])
quantity = int(row["quantity"])
totals[category] += price * quantityPythonがCSVを開き、1行ずつDictにしてループで集計していくスタイル。
CSV
↓
Python
↓
1行ずつル〜プ処理
↓
集計出力結果は当然こうなる。
A 1700
B 2600
C 1770検証2:SQLite + SQLで集計する
次はSQL。
Python組み込みの sqlite3 を使ってメモリ上(":memory:")にテーブルを作って集計してみた。
SQL文は標準的なやつ。
SELECT
category,
SUM(price * quantity) AS total_sales
FROM sales
GROUP BY category
ORDER BY category結果は当然同じ。
A 1700
B 2600
C 1770Pythonのループ処理と比べると、コードの書き味はかなり変わる。
Pythonでの書き方
for row in reader:
totals[category] += price * quantitySQLでの書き方
SELECT
category,
SUM(price * quantity)
FROM sales
GROUP BY category最初は「SQLとPySparkってライバル関係なのかな?」と思ってたが、ここで少し視点が変わってきた。
検証3:PySpark(DataFrame API)で集計する
PySparkでの書き方はこんな感じ。
result = (
df.withColumn(
"total_sales",
col("price") * col("quantity")
)
.groupBy("category")
.agg(
spark_sum("total_sales").alias("total_sales")
)
.orderBy("category")
)実行結果は勿論これ。
A 1700
B 2600
C 1770結果自体は一緒だが、大事なのは「裏で何が起きているか」。
PythonはPythonのインタプリタが1行ずつ律儀に回しているが、PySparkの方は「こういう処理をしてね」というプランをSparkエンジン側に組み立ててから実行させている。
検証4:PySparkの中でSQLを使う
ここで「SQLじゃだめなの?」という疑問を深掘りしてみる。
実はPySparkでは、DataFrameを一時ビュー(TempView)として登録できる。
df.createOrReplaceTempView("sales")登録しちゃえば、普通にSQLが書ける。
result = spark.sql("""
SELECT
category,
SUM(price * quantity) AS total_sales
FROM sales
GROUP BY category
ORDER BY category
""")つまり構造としてはこう。
PySpark
├─ DataFrame API (メソッドチェーンで書く)
│
└─ Spark SQL (クエリ文字列で書く)両方使える。
Spark公式ドキュメントでも「SQLとDataFrame APIは表現が違うだけで、裏側の計算エンジンは同一」と明言されている。
なので、
「SQLかPySparkか」
という比較ではなく、
「Sparkの上で、SQLとDataFrame APIのどっちの書き方で指示を出すか」
という話だったことが分かった。
検証5:実行計画を見てみる
せっかくなので、Sparkの裏側(実行計画)を覗いてみる。
PySparkには explain() メソッドがある。
result.explain(mode="formatted")Spark SQLで書いた場合の実行計画を出してみると、こんな感じのフローになっていた。
Scan csv
↓
HashAggregate
↓
Exchange
↓
HashAggregate
↓
Exchange
↓
Sort
↓
AdaptiveSparkPlanDataFrame APIで書いた場合もほぼ同じ。
Scan csv
↓
Project
↓
HashAggregate
↓
Exchange
↓
HashAggregate
↓
Exchange
↓
Sort
↓
AdaptiveSparkPlanScan csv って何?
CSVファイルを読み取っているステップ。category price quantity といった必要な列をスキャンしている。
Project って何?
DataFrame APIで指定した .withColumn("total_sales", col("price") * col("quantity")) の部分。price * quantity の計算を行って total_sales という列を作るステップが Project として現れている。
SQL版だと SUM(price * quantity) の中に直接書いたため独立した Project ノードとしては見えなかったっぽい。
Exchange って何?(ちょっと気になったポイント)
一番目についたのが Exchange。
詳細を見ると hashpartitioning(category, 200) みたいな記述がある。GROUP BY category を処理するために、データをカテゴリごとにパーティション再配置(シャッフル)しているということ。
元のデータ
↓
categoryを基準にデータを再配置(Exchange)
↓
集計(HashAggregate)単にファイルを上から読んで加算しているわけではなく、分散処理に適した形でデータを並び替えてから集計しているのが見て取れる。
性能も比較してみた
ここまでの仕組みを踏まえて、「で、実際に処理速度はどれくらい違うの?」を測ってみた。
※測定条件:
- Windows 11 / 同一PC / 同一CSV / 同一集計処理
- PySparkは
local[1]とlocal[5]
注意点として、PythonとPySparkではメモリ構造も動作原理も違うため、あくまで「今回の条件での測定値メモ」として見てほしい。
1,000万行のデカいCSVで勝負
1,000万行のテスト用CSVを作成。
Python側はCSV読み込み完了後の「純粋な集計処理(5回測定)」のタイムを計測。
Python単体
測定値(秒):
- 0.392956
- 0.389598
- 0.380789
- 0.408727
- 0.390020
平均:0.392418秒 / 中央値:0.390020秒
PySpark local[1] (スレッド数: 1)
ウォームアップ実行後、5回測定。
測定値(秒):
- 0.602338
- 0.589854
- 0.591355
- 0.588073
- 0.573362
平均:0.588996秒 / 中央値:0.589854秒
PySpark local[5] (スレッド数: 5)
測定値(秒):
- 0.294400
- 0.247066
- 0.238032
- 0.230247
- 0.231689
平均:0.248287秒 / 中央値:0.238032秒
性能結果を並べてみる
| 方法 | 平均 | 中央値 |
|---|---|---|
| Python | 0.392418秒 | 0.390020秒 |
PySpark local[1] | 0.588996秒 | 0.589854秒 |
PySpark local[5] | 0.248287秒 | 0.238032秒 |
中央値で比較すると、
PySpark local[5] → 0.238秒 (最速)
Python → 0.390秒
PySpark local[1] → 0.590秒マルチスレッドな local[5] にすると、Python単体より約1.64倍速くなった。
一方でシングルスレッドの local[1] だと、オーバーヘッドのせいかPython単体より遅いという結果に。
ここからも、
「PySparkにすれば無条件でPythonより速くなる」
わけではなく、並列度を効かせることで初めて集計処理が速くなるということが良くわかる。
実行時間全体(トータルタイム)でも比較してみた
今度は「CSVの読み込み」や「Spark/JVMの起動コスト」も含めた全体の時間を測定してみた。
1,000万行データでの結果:
| 方法 | 実行時間 |
|---|---|
| Python | 7.065秒 |
PySpark local[1] | 12.823秒 |
PySpark local[5] | 8.443秒 |
まさかのPython単体が一番早いという結果に!
【集計処理だけ】
PySpark local[5] > Python
【起動や読み込みも含めた全体】
Python > PySpark local[5]見事な逆転劇。
なぜこうなるかというと、PySparkは背景でJVM(Java仮想マシン)やSparkセッションを立ち上げる初期化コストがかかるため。
ローカルPCで中規模程度の処理を1回だけサクッと動かすようなケースだと、PySparkの初期化オーバーヘッドが響いてトータルではPython単体の方が早くなる、というオチ。
「PySpark=単なるPythonの高速化ツール」という認識がいかにズレているかが実感できる面白い結果となった。
local[1] と local[5] の並列度の差
データ量によって並列度の差がどう出るかも実験してみた。
10万行
local[1]:約0.336秒local[5]:約0.337秒
→ データが小さいと差はほぼゼロ。
100万行
local[1]:約0.378秒local[5]:約0.374秒
→ まだ明確な差は出ない。
1,000万行
local[1]:0.590秒local[5]:0.238秒
→ ここで一気に差が開く、、、!
データ量が小さいうちはオーバーヘッドの方が大きく、1,000万行クラスになって初めてマルチスレッド(local[5])の効果がはっきり現れるっぽい。
調べる前と後で考え方が変わった
調べる前
- 「PySparkってPythonで大量データを爆速処理するライブラリでしょ?」
- 「SQLで集計できるならPySparkなんて要らなくね?」
調べた後
- PySparkはPythonを直接速くする魔法ではなく、Sparkエンジンを動かすためのAPI。
- 裏ではDriver / Executor / Task という分散実行モデルが動いている。
- SQLかPySparkかの二者択一ではなく、Sparkの上で「SQL表記で書くかDataFrame表記で書くか」の違い。
イメージがガラッと変わった。
「Pythonじゃだめなの?」への回答
ローカルPCで小〜中規模のデータをサクッと集計するだけなら、普通にPython単体で十分なケースが多い。
実際、今回の1,000万行の全体処理時間はPython単体の方が速かった。
ただ、データ量が単一マシンのメモリを超えたり、分散クラスタ環境で処理したい場合には、Python単体だと限界が来る。その時にSparkの分散実行モデルをPythonから使えるのがPySparkの強み。
「Pythonより必ず速いから使う」というより、「Sparkの分散実行モデルを使いたいから、書きやすいPythonからPySpark経由で触る」 というのが正しい捉え方っぽい。
「SQLじゃだめなの?」への回答
SQLでも全然OK。今回SQLiteでも同じ集計ができた。
しかもSpark内部にもSpark SQLがある。
なので「SQLだと対応できないからPySparkを使う」というわけではない。
Spark
├─ Spark SQL(SQLで記述)
└─ DataFrame API(Python等のコードで記述)どっちで書いても裏の計算エンジンは同じなので、チームの好みや処理の複雑さ(前処理のループや条件分岐が多いならDataFrame API、単純集計ならSQLなど)で選べばOK。
注意点・今回分からなかったこと
- 1台のローカルPCでの検証に過ぎない
Spark本来の強みである複数台ノードでの分散環境では試していない。 - 動作環境依存
Windows 11, Core Ultra 7 265K, メモリ64GBでの検証結果なので、環境が変われば数値も変わる。 - Windows環境での警告
winutils.exeやnative-hadoopが無いという警告が出たが、今回のCSV処理自体は正常終了した。実運用環境では要確認。
まとめ
- PySparkはPythonを高速化するライブラリではなく、Apache SparkをPythonから操作するAPI。
- 裏では実行計画が組み立てられ、データが再配置(Exchange)されたりTaskに分割されて動く。
- SQLとPySpark(DataFrame API)は競合ではなく、同じSparkエンジン上の表現違い。
- ローカル環境&1,000万行程度だとトータル処理時間はPython単体の方が速いこともある。
- 大量データや分散処理の仕組みが必要になって初めてPySpark(Spark)の真価が発揮される。
「PySpark=Pythonの高速版」と思って触ると「あれ、意外と遅くね?」と困惑するが、実行モデルや裏側の仕組みを理解すると使いどころが見えてくる、という話だった。
今後調べたいこと
今回の検証で、逆に新しい疑問も出てきた。
特に気になったのが、
Partition
Task
Stage
Exchange
Shuffle
Driver
Executorの関係。
今回 explain() を見ることで、Sparkが単純なPythonのループ処理ではないことは分かった。
ただ、
「実際にデータがどのように分割され、Taskとして処理されているのか?」
までは、まだ十分に理解できていない。
また、今回はローカルPCの local[1] / local[5] までしか試していないため、
複数ノードのSparkクラスタになると何が変わるのか?
も気になっている。
このあたりは、今回の記事とは分けて書かないとごちゃごちゃになっちゃいそうなので、別記事で。。。
参考資料
今回使用したGithubコード群
Apache Spark 4.2.0 PySpark Installation
PySparkのPython対応バージョン、PyPIからのインストール、Java要件など。Apache Spark 4.2.0 Spark SQL, DataFrames and Datasets Guide
Spark SQL、DataFrame API、同一実行エンジンについて。Apache Spark 4.2.0 Cluster Mode Overview
Driver、Executor、Task、Stageなどの構成について。Apache Spark 4.2.0 Submitting Applications
local[K]、local[*]、spark-submitなどについて。PySpark 4.2.0
DataFrame.explain()
実行計画の確認方法について。Python 3.13
sqlite3documentation
SQLiteとPython標準のsqlite3について。
※今回の記事の検証結果は、2026年8月29日時点の環境で実施したもの。
PySparkやJava、Pythonのバージョン、Sparkの仕様は今後変更される可能性があるため、記事公開時には公式ドキュメントも再確認してね。