Data Engineer Associate — 第 4 章
この章で学ぶこと
この章に出てくる用語
困りごとから入ります。ノートブックに変換を 10 行ほど書いて上から実行すると、どのセルも 1 秒たらずで終わります。ところが最後に `display(df)` と書いた 1 行だけが 40 分固まり、その間クラスターの料金は流れ続けます。原因は単純で、Spark は書いた順に処理していないからです。
Spark の操作は 2 種類しかありません。`select` `filter` `withColumn` `join` `groupBy` のような変換 (transformation) は、実行計画にメモを 1 行足すだけで、データには触れません。Apache Spark の公式ドキュメントは「Spark のすべての変換は遅延 (lazy) であり、結果をすぐには計算しない」と明記しています。計算が始まるのは `show()` `count()` `collect()` `write` `display()` といったアクション (action) を呼んだ瞬間です。
積み上がった変換のつながりを DAG (有向非巡回グラフ) と呼びます。処理どうしの依存関係を、後戻りしない矢印だけで表した図のことです。Spark UI には Job と Stage ごとに DAG Visualization という図が出ます。
遅延評価は、エラーが出るタイミングまで変えます。次のコードは存在しない列を参照していますが、例外を捕まえられません。`/Volumes` で始まるパスは、Databricks がファイルを置く場所の 1 つです。
try:
df = spark.read.json("/Volumes/main/raw/events")
df.withColumn("new", df["missing"] + 1) # 変換だけ。ここでは走らない
except Exception as e:
print("Error:", e) # 何も表示されないこの `try` ブロックにはアクションが 1 つも無いため、列名が実在するかの検証すら行われません。`AnalysisException` は後続の `show()` や `write` で初めて飛びます。エラーの出た行と原因の行がずれるのは Spark ではふつうのことで、「例外が出なかったから正常」と判断して本番に出すと、書き込みの瞬間に落ちます。
アクションを呼ぶと、Spark は計画を 3 段に分けます。Apache Spark の用語集にある定義が、そのまま試験で問われる語彙です。
| 単位 | 公式の定義 | 個数を決めるもの |
|---|---|---|
| Job | アクションに応じて起動する、複数の Task からなる並列計算 | アクションを呼んだ回数 |
| Stage | 互いに依存する小さな Task の集合へ Job を分割したもの | シャッフル回数 + 1 |
| Task | 1 つの Executor へ送られる仕事の単位 | その Stage が扱うパーティション数 |
冒頭の「最後の 1 行だけ 40 分」は、この表から説明できます。`display(df)` がアクションなので、そこで初めて Job が 1 つ起動し、それまでの 10 行ぶんの変換がまとめて走ります。セルごとの実行時間は処理の重さを表していません。読むべきなのは Spark UI の Job と Stage の一覧です。
Task の数え方は素直で、1 パーティションにつき 1 Task です。パーティションが 8 個しかないデータを 64 コアのクラスターに投げても、同時に動くのは 8 Task で、残り 56 コアは遊びます。「クラスターを大きくしたのに速くならない」の典型がこれです。
Stage の境界を決めるのはシャッフル (shuffle) です。公式ドキュメントは「データをパーティション間で別のグループに再配置する仕組み」と定義し、「ディスク I/O、データのシリアライズ、ネットワーク I/O を伴う高価な操作」と説明しています。
出力の 1 パーティションが入力の 1 パーティションだけを見れば作れる操作が narrow transformation、同じキーの行を 1 か所へ集めないと答えが出せない操作が wide transformation です。
| 操作 | 種類 | シャッフル |
|---|---|---|
| select / filter / withColumn / cast | narrow | 起きない |
| union / explode / coalesce | narrow | 起きない |
| groupBy + agg | wide | 起きる |
| join (ブロードキャスト以外) | wide | 起きる |
| distinct / dropDuplicates | wide | 起きる |
| orderBy / sort / repartition | wide | 起きる |
| ウィンドウ関数 (OVER 句) | wide | 起きる |
`coalesce(n)` は既存のパーティションを隣どうしでまとめるだけなので narrow、`repartition(n)` は全行をハッシュで振り直すので wide です。出力ファイル数を減らしたいだけなら `coalesce` のほうが安く済みます。
書く順番にも効きます。行と列を削る操作どうしの前後は Catalyst (記述を実行計画へ組み立て直す最適化エンジン) が自動で入れ替えますが、集約をまたぐ絞り込みは前倒しできません。`groupBy().agg().filter()` は `HAVING` 相当で、集約前の絞り込みとは結果そのものが違うからです。
| 設定 | 既定値 | 効き方 |
|---|---|---|
| spark.sql.shuffle.partitions | 200 | シャッフル後のパーティション数、つまり次の Stage の Task 数 |
| spark.sql.files.maxPartitionBytes | 134217728 (128 MB) | ファイル読み込み時の 1 パーティションの上限 |
| spark.sql.autoBroadcastJoinThreshold | 10485760 (10 MB) | これ以下なら小さい側を全 Executor へ配り join のシャッフルを回避 |
| spark.databricks.optimizer.adaptive.enabled | true | Databricks で AQE を有効にする |
200 という固定値は両側に外れます。数 MB のテーブルを `groupBy` しても 200 Task が作られ、1 Task が数十 KB を処理して終わる空回りになります。逆に 1 TB を 200 分割すると 1 パーティション 5 GB になり、メモリに載らず spill (ディスクへの書き出し) が起きます。Databricks は固定値ではなく `spark.sql.shuffle.partitions=auto` を推奨しています。読み取り側の 128 MB からは、20 GB の Parquet (列ごとに固めて保存する分析向けのファイル形式) が約 160 パーティションになると見積もれます。
実行の途中で計画を作り直すのが AQE (Adaptive Query Execution) です。シャッフルとブロードキャストの交換が終わった時点 (AQE はこの区切りを query stage と呼びます) で実測の統計を読み、細かすぎるパーティションの結合、両側を並べ替えて突き合わせる sort merge join から小さい側を配る broadcast hash join への切り替え、偏ったパーティションの分割、空の関係の検出という 4 つの手を打ちます。論理プランの最適化そのものは Catalyst の担当で、この章では扱いません。
Driver は main() を実行して SparkContext を作るプロセス、Executor は worker node 上で Task を実行しデータを保持するプロセスです。`collect()` や `toPandas()` が危険なのはここに理由があり、全行を Driver 1 台のメモリへ集めるため、Executor をいくら増やしても Driver だけが落ちます。計画を手元で確かめるには `df.explain()` を使います。mode は simple / extended / codegen / cost / formatted の 5 種類で、simple は物理プランのみ、extended は論理プランと物理プランの両方を表示します。
確認 — 穴あけ 3 問
0 / 3
空欄を押すと選択肢が出ます。間違えても減点はありません。
シャッフル後のパーティション数を決める spark.sql.shuffle.partitions の既定値は です。
Job をシャッフルの境界で切り分けた単位を と呼びます。
変換だけを書いたコードでは列名の誤りが検出されず、 を呼んだ時点で AnalysisException が出ます。
困りごとから入ります。要件を渡されたとき毎回迷うのが「これは SQL 1 本で書けるのか、Python に落として組み立てるのか」という線引きです。迷った末に Python の `for` ループと自作関数で書き、レビューで「それ SQL で書けます」と言われる。この節は構文の一覧表ではなく、SQL でどこまで書けるかの境界線を引くことに使います。
Databricks で `spark.sql("SELECT ...")` と書いても、`df.groupBy(...).agg(...)` と書いても、最終的には同じ論理プランに変換されます。どちらが速いという話ではなく、読みやすいほうを選んでよいのが Spark SQL の設計です。境界が生まれるのは、Python の関数を UDF (ユーザー定義関数) として差し込んだ瞬間です。UDF の中身は Spark から見えない箱になり、最適化の対象から外れます。つまり「SQL で書ける範囲」は「最適化してもらえる範囲」と同義です。
UDF に逃げがちな処理の筆頭が条件分岐ですが、これは SQL 側にあります。SQL では `CASE WHEN 条件 THEN 値 ... ELSE 既定値 END`、PySpark では `when(条件, 値).otherwise(既定値)` で、両者は同じものです。`.when()` を並べれば if / elif / else と同じ構造になります。
df.withColumn("category",
when(col("price") > 1000, "High")
.when(col("price") > 300, "Mid")
.otherwise("Low"))押さえるべき仕様が 4 つあります。1 つ目、条件は上から順に評価され、最初に真になった枝の値だけが採用されます。上のコードで price=1200 は 1 つ目で `High`、price=500 は 1 つ目が偽で 2 つ目が真なので `Mid`、price=100 はどちらも偽で `Low` です。2 つの `when` を逆順に書くと、1200 も `Mid` で止まります。
2 つ目、`ELSE` と `otherwise` は省略できますが、省略すると既定は NULL です。Databricks SQL のリファレンスは「`def` を省略した場合の既定は NULL」と明記しています。どの条件にも当たらない行が NULL になり、後段の `filter` で静かに消えます。
3 つ目、比較演算子の境界です。`col("amount") > 1000` は等号を含まないので、amount がちょうど 1000 の行は偽の側に落ちます。`>=` との取り違えは構文エラーにならず、件数だけがずれます。4 つ目、`withColumn(名前, 式)` は同名の列があれば上書き、なければ追加です。`df.withColumn("price", col("price") * 1.1)` は `price_1` のような別列を作らず、`price` を置き換えます。行の絞り込みは `filter`、列の追加は `withColumn`、集約は `groupBy().agg()` と役割が分かれている点も、そのまま出題されます。
`GROUP BY` を書いたクエリの `SELECT` と `ORDER BY` に並べてよいのは、(1) `GROUP BY` に含めた列と、(2) 集計関数の結果、の 2 種類だけです。それ以外を書くと `MISSING_AGGREGATION` 系のエラーになります。次の SQL が落ちるのは `age` がどちらにも該当しないからです。
SELECT department, COUNT(*) AS cnt
FROM employees
GROUP BY department
ORDER BY age; -- age は GROUP BY にも集計にも無い直し方は `GROUP BY department, age` にするか、`ORDER BY cnt DESC` に変えるかの二択です。`SELECT` で付けた別名は `ORDER BY` から参照できます。集計後に絞り込むなら `HAVING`、集計前なら `WHERE` で、DataFrame API では `groupBy().agg().filter()` の最後の `filter` が `HAVING` に当たります。
INNER JOIN の結果が想定より大幅に少ないとき、まず疑うのは結合キーです。SQL の三値論理では `NULL = NULL` が真にならないため、キーが NULL の行は静かに全部落ちます。エラーも警告も出ないので、件数を数えるまで気づけません。前段で `WHERE key IS NOT NULL` を入れて落ちる行を可視化し、必要なら別テーブルへ退避します。`coalesce(key, 0)` のような穴埋めは、本来無関係な行どうしを結合させる事故になります。
もう 1 つの原因が型と表記のずれで、`STRING` と `INT` の暗黙キャスト、前後の空白、大文字小文字の違いは、いずれも一致しない側に倒れます。なお「右に存在するかどうかだけ知りたい」なら `LEFT SEMI JOIN`、「存在しないものを取りたい」なら `LEFT ANTI JOIN` を使うと、右側の列を返さず行数も増えません。10 億行と 1 万行のような組み合わせでは第 1 節の Broadcast Join が効き、SQL では `/*+ BROADCAST(regions) */`、PySpark では `broadcast(regions)` で明示できます。
JSON から取り込んだテーブルには、1 行の中に配列が入っている列がよくあります。`explode` はこれを行へほどく関数で、ARRAY を渡すと要素ごとに 1 行を作り列名は `col`、MAP を渡すと `key` と `value` の 2 列になります。ここで入力が NULL または空配列だと 1 行も出力されず、その行ごと結果から消えます。行を残したいときは `explode_outer`、要素の位置も欲しいときは `posexplode` です。Databricks Runtime 12.2 以降では `SELECT` リストや `LATERAL VIEW` からの呼び出しは非推奨で、`FROM` 句のテーブル参照として書くのが推奨です。
逆方向の `PIVOT` は、ある列の値を列名へ展開してクロス集計表を作ります。構文は `PIVOT (集計 FOR 列 IN (値, ...))` で、`IN` に並べる値はリテラルでなければならず、`1 + 0` のような式を書くと `NON_LITERAL_PIVOT_VALUES` になります。集計を複数書いた場合、出力列名は `q1_total` のように「値の別名_集計の別名」になります。列を行へ戻す操作は `UNPIVOT` で、配列を行へ開く `explode` とは目的が違います。
`WITH 名前 AS (クエリ)` の CTE は読みやすさのための道具で、結果を保存する仕組みではありません。同じ CTE を 2 か所から参照すれば 2 回評価されることがあります。再帰 CTE は Databricks Runtime 17.0 以降で使え、`MAX RECURSION LEVEL` の既定は 100、結果セットは `LIMIT` が無い限り 1,000,000 行に制限されます。ビューは 3 種類で、`CREATE VIEW` はカタログに永続、`CREATE TEMPORARY VIEW` は作成セッション限り、`CREATE GLOBAL TEMPORARY VIEW` は `global_temp` スキーマ経由で他セッションからも見えます。いずれもクエリ定義の保存であって、データのコピーではありません。
集合演算では既定値が事故になります。Databricks SQL の `UNION` `INTERSECT` `EXCEPT` は既定が `DISTINCT` で、重複行が黙って消えます。単純に連結したいだけなら `UNION ALL` を明示します。最後に `QUALIFY` です。`WHERE` が集計前、`HAVING` が集計後を絞るのに対し、`QUALIFY` はウィンドウ関数の計算後を絞ります。次の節で扱う「グループごとに 1 件だけ取る」が、サブクエリ無しの 1 文で書けます。
SELECT customer_id, order_id, order_date
FROM orders
QUALIFY ROW_NUMBER() OVER (
PARTITION BY customer_id ORDER BY order_date DESC) = 1;確認 — 穴あけ 3 問
0 / 3
空欄を押すと選択肢が出ます。間違えても減点はありません。
when チェインで otherwise を省くと、どの条件にも当たらなかった行の値は になります。
withColumn に既存と同じ列名を渡すと、その列は されます。
GROUP BY のあるクエリの SELECT と ORDER BY に並べてよいのは、GROUP BY のキーと の結果だけです。
困りごとから入ります。「顧客ごとに最新の注文を 1 件だけ取ってきて」という依頼は、`GROUP BY customer_id` では解けません。`MAX(order_date)` は取れますが、一緒に欲しい商品名や金額を `SELECT` に並べた瞬間に第 2 節のエラーになります。行を減らさずに集計値や順位を付けたい——これがウィンドウ関数の出番です。
`GROUP BY` はグループごとに 1 行へ潰します。ウィンドウ関数は元の行数を保ったまま、各行に集計値や順位を列として足します。書き方は集計関数のうしろに `OVER` 句を付けるだけです。
SELECT customer_id, order_date, amount,
SUM(amount) OVER (PARTITION BY customer_id ORDER BY order_date) AS running_total
FROM orders;`OVER` の中身は 3 つのパーツでできています。`PARTITION BY` が計算を区切る単位、`ORDER BY` がその中での並び、そしてフレーム句が「同じ区切りの中でもどこからどこまでを足すか」です。3 つとも省略でき、省略したときの挙動を知らないことが事故の元になります。PySpark では同じものを `Window` クラスで組み立てます。
from pyspark.sql.window import Window
from pyspark.sql import functions as F
w = Window.partitionBy("customer_id").orderBy("order_date") \
.rowsBetween(Window.unboundedPreceding, Window.currentRow)
df2 = df.withColumn("running_total", F.sum("amount").over(w))境界の定数は `Window.unboundedPreceding` (区画の先頭)、`Window.currentRow` (現在行)、`Window.unboundedFollowing` (区画の末尾) の 3 つです。行ベースのオフセットは現在行を 0 とした相対位置なので、`rowsBetween(Window.unboundedPreceding, 0)` は上のコードとまったく同じ累積フレームを指します。試験では整数リテラルで書かれた選択肢も出るので、`0` と `Window.currentRow` は同じものだと結び付けておきます。同様に `-2` は 2 行前です。
フレームを書かなかったときの既定は、`ORDER BY` の有無で変わります。Databricks の `Window` クラスのドキュメントは、順序が定義されていない場合は無制限フレーム (rowFrame, unboundedPreceding, unboundedFollowing)、順序が定義されている場合は伸びていくフレーム (rangeFrame, unboundedPreceding, currentRow) が既定だと書いています。SQL に直すと、前者が `ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING`、後者が `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` です。
問題になるのは `RANGE` のほうです。`ORDER BY order_date` だけを書いて累積和を取ると、既定は `RANGE` なので同じ日付の行がすべて 1 つの塊として同時に加算されます。1 日に 3 件の注文がある顧客では、3 行とも同じ累積値が並び、1 件ずつ増える階段になりません。意図が「行単位の累積」なら `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` を明示するのが正解です。
2 つの違いは一言で言えます。`ROWS` は行数を数え、`RANGE` は `ORDER BY` 式の値を数えます。給与が同額の 2 人を並べたとき、ROWS では 1 行ずつ数えるので累計が違い、RANGE では同じ値を共有するので累計が同じになります。
| やりたいこと | フレーム指定 (PySpark) |
|---|---|
| 累積 (running total) | rowsBetween(Window.unboundedPreceding, Window.currentRow) |
| 逆累積 (この行から末尾まで) | rowsBetween(Window.currentRow, Window.unboundedFollowing) |
| 直近 3 行の移動合計 | rowsBetween(-2, Window.currentRow) |
| 区画全体の合計を各行に付ける | rowsBetween(Window.unboundedPreceding, Window.unboundedFollowing) |
`RANGE` には制約もあります。`ORDER BY` が無いと `DATATYPE_MISMATCH.RANGE_FRAME_WITHOUT_ORDER`、`ORDER BY` に式が 2 つ以上あると `DATATYPE_MISMATCH.RANGE_FRAME_MULTI_ORDER` のエラーになります。オフセットは `ORDER BY` 式から足し引きできる型でなければならず、日付列なら `INTERVAL` を渡します。
冒頭の「顧客ごとに最新 1 件」に戻ります。答えは `row_number()` です。
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) AS rn
FROM orders
) WHERE rn = 1;| 関数 | 同点の扱い | 並び (1, 1, 2 の入力) | = 1 で 1 行に絞れるか |
|---|---|---|---|
| row_number | 必ず別々の番号 | 1, 2, 3 | 絞れる |
| rank | 同じ番号を共有し次を飛ばす | 1, 1, 3 | 絞れない |
| dense_rank | 同じ番号を共有し飛ばさない | 1, 1, 2 | 絞れない |
ここで `RANK()` を選ぶと、同じ日付の注文が 2 件ある顧客で `= 1` が 2 行返り、「1 件に絞る」という要件を満たせません。順位関数は `ORDER BY` が必須で、フレーム句は書けません。第 2 節の `QUALIFY` を併用すれば、この 2 段のサブクエリを 1 文に畳めます。
`ORDER BY` を省くと、`row_number()` や `lag()` の結果は非決定的になります。同じ入力でも実行のたびに値が変わるため、日次バッチで前日と差分を取ると毎回差分が出ます。並び順が一意に決まる列 (タイムスタンプ + ID など) を必ず指定します。
`PARTITION BY` を省くと、全行が 1 つのウィンドウにまとめられます。結果が「カテゴリ別ランキング」ではなく「全件ランキング」になり、「ユーザーごとの累計のはずが全行に総合計が入っている」という症状になります。実行面ではもっと深刻で、全行が 1 つのパーティションへ集められ 1 Task で処理されます。ウィンドウ関数は第 1 節でいう wide transformation で、`PARTITION BY` のキーでシャッフルが起きます。キーの値が偏れば特定の Task だけが巨大になり、spill や OOM (メモリ不足) の原因になります。AQE の偏り対策は結合が対象なので、ウィンドウ側の偏りはキー設計で避けるしかありません。
前後の行を参照する `LAG` と `LEAD` は、`ORDER BY` の順で 1 行ずらした値を返し、端の行では NULL になります。これらと `FIRST_VALUE` `LAST_VALUE` には `IGNORE NULLS` を付けられ、NULL を飛ばして「直前の有効な計測値」を引けます。ここで `collect_list` で全明細を 1 配列に集めて Python で計算したり、`crossJoin` で全組み合わせを作って累計を出したりするのは、どちらも行数とメモリが破綻する典型的なアンチパターンです。
確認 — 穴あけ 3 問
0 / 3
空欄を押すと選択肢が出ます。間違えても減点はありません。
ORDER BY を書いてフレームを省略したときの既定は BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW です。
顧客ごとに最新の 1 件だけを取り出すには を使い、= 1 で絞ります。
ウィンドウ関数で を省くと、全行が 1 つのウィンドウにまとめられ単一 Task に集中します。
困りごとから入ります。1 行の中に配列が入っているテーブルを渡されたとき、多くの人がやるのは「`explode` で行にほどいて、加工して、`collect_list` で配列に戻す」という往復です。書けはしますが、行を増やして減らす操作なのでシャッフルが 2 回入ります。Python の `for` ループを UDF にして配列を回す手もありますが、こちらは 1 行ごとに JVM と Python の間でデータを詰め替えるので、さらに遅くなります。配列は配列のまま、行の中で加工する——それが高階関数です。
まず配列を作る側です。`collect_list(col)` は `GROUP BY` と組み合わせて、グループ内の値を `ARRAY` にまとめる集約関数で、Databricks では `array_agg` の別名です。重複を落としたいときは `collect_set(col)`、あるいは `collect_list(DISTINCT col)` と書きます (`DISTINCT` を付けると `collect_set` と同義になります)。
SELECT customer_id, collect_list(product_id) AS products
FROM orders
GROUP BY customer_id;仕様で押さえるべき点が 2 つあります。1 つ目、NULL は要素に含まれません。公式ドキュメントは「NULL 値は除外される」と明記しています。したがってグループの値がすべて NULL のときの結果は NULL ではなく空の配列です。「欠損も含めて数えたい」なら `collect_list(coalesce(col, 'unknown'))` のように埋めてから集約するか、事前に `WHERE` で不正値を除外します。
2 つ目、配列の中の順序は非決定的です。パーティションの並びに依存するため、上流で `orderBy` してもシャッフルをまたいだ時点で保証されません。順序が意味を持つなら `array_sort` や `sort_array` を必ずかぶせます。
失敗としてよくあるのが 2 つあります。1 つは集約キーの取り違えで、「ユーザーごとの購入商品リスト」を作るつもりで `groupBy("shop_id")` と書いても構文エラーは出ず、店舗ごとに全ユーザーの商品が混ざった配列が静かに出来上がります。そもそも `groupBy` を付けずに呼べば、データセット全体が 1 つの配列になります。もう 1 つは、すでに配列型の列にもう一度 `collect_list` をかけて `ARRAY<ARRAY<STRING>>` という二重配列ができるパターンです。中間テーブルで集約済みの列を別の `groupBy` でもう一度たたむと起きます。`flatten` で平らにするか、集約を 1 回に整理します。
`transform(expr, func)` は配列の各要素にラムダ式を適用し、同じ長さの新しい配列を返します。ラムダには 2 つの形があり、要素だけを受ける `x -> x + 1` と、要素と 0 始まりの添字を受ける `(x, i) -> x + i` が書けます。`transform(array(1, 2, 3), x -> x + 1)` は `[2,3,4]`、`transform(array(1, 2, 3), (x, i) -> x + i)` は `[1,3,5]` になります。
`filter(expr, func)` は述語が真の要素だけを残し、`expr` と同じ型の配列を返します。`filter(array(1, 2, 3), x -> x % 2 == 1)` は `[1,3]`、`filter(array(0, null, 2, 3, null), x -> x IS NOT NULL)` は `[0,2,3]` です。NULL を落とす処理も、専用関数ではなくこのラムダで書きます。
ここを取り違える出題が多いので整理します。長さ 3 以上の要素だけを残したいのに `transform(tags, t -> length(t) >= 3)` と書くと、返るのは元の文字列ではなくブール値の配列です。要素を選ぶなら `filter`、要素を作り替えるなら `transform`、という対応で覚えます。
`exists(expr, func)` は「条件を満たす要素が 1 つでもあるか」を返します。落とし穴は三値論理です。条件を満たす要素があれば `true`、無ければ `false` ですが、満たす要素が無く、かつ配列に NULL が含まれる場合は `NULL` を返します。公式ドキュメントの例では `exists(array(1, NULL, 3), x -> x % 2 == 0)` が `NULL` です。これを `WHERE` に置くと、その行は真ではないので結果から消えます。「条件に合わない行を数えたら合計が合わない」の正体がこれです。全要素が条件を満たすかを見たいときは `forall` を使います。
`aggregate(expr, start, merge [, finish])` は配列を 1 つの値へ畳み込む関数で、`reduce` の別名です。`start` が初期値、`merge` が要素を足し込むラムダ、省略可能な `finish` が最後の仕上げです。
SELECT aggregate(array(1, 2, 3), 0, (acc, x) -> acc + x); -- 6
SELECT aggregate(array(1, 2, 3), 0, (acc, x) -> acc + x, acc -> acc * 10); -- 60注意すべきは型です。`finish` を省いた場合、結果の型は `start` の型に一致します。平均を求めるつもりで `start` に整数の `0` を渡すと、途中の計算まで整数に丸められます。小数が必要なら初期値を `0D` のように書くか、`finish` で割り算します。
ここまでの関数はすべて Spark SQL の式として表現されるため、Catalyst の最適化とコード生成の対象になり、Photon (Databricks の C++ 実行エンジン) でも動きます。一方 Python UDF は、Executor の JVM から Python プロセスへ 1 行ずつデータを送り、結果を戻す往復が入ります。Spark からは中身の見えない箱なので、述語の押し込みも列の刈り込みも効きません。どうしても Python のロジックが必要なら、Apache Arrow でまとめて転送する Pandas UDF が次善の策で、組み込み関数と高階関数 → Pandas UDF → Python UDF という優先順位がそのまま設計の指針になります。
UDF を書くときは型の指定でつまずきます。`pyspark.sql.functions.udf(f=None, returnType=StringType(), useArrow=None)` という署名のとおり、`returnType` の既定は `StringType` です。整数を返すつもりで型を省くと、例外は出ないまま文字列の列ができあがり、下流の計算で崩れます。`udf(lambda x: x + 1, IntegerType())` のように必ず明示します。Pandas UDF も同じで、`@pandas_udf("long")` のように戻り値の型を宣言する必要があり、Spark が自動で推論してくれることはありません。
確認 — 穴あけ 3 問
0 / 3
空欄を押すと選択肢が出ます。間違えても減点はありません。
配列の各要素を別の値へ作り替え、同じ長さの配列を返す高階関数は です。
collect_list は NULL を要素に含めないため、グループの値がすべて NULL なら結果は になります。
pyspark.sql.functions.udf で returnType を省略すると、既定の が使われます。
この章のまとめ
この端末にだけ保存されます(登録不要)
この章の根拠
最終確認 2026-08-09 / 対応バージョン DEA 2026-05-04 改訂版
コース全体