テーマ切替
確認問題|Professional ① Python/SQL コード開発(配点 22%)
教材 01-code-development.md の内容に基づく確認問題です(全10問)。 Professional は「コードを読んで挙動・結果・性能を答える」問題が中心です。まず問題を解き、下の「解答・解説」で答え合わせをしてください。
問題
Q1. 次の運用パイプラインのコードについて、Professional の観点で最も適切な指摘はどれか。
python
df = spark.read.table("bronze.events") # 1
df = df.filter(col("status") == "OK") # 2
print(df.count()) # 3
df2 = df.withColumn("d", to_date("ts")) # 4
display(df2) # 5
df2.write.mode("overwrite").saveAsTable("silver.events") # 6- A. 1〜6 のすべてが変換(transformation)なので、ジョブは 1 回だけ起動される
- B. 3 と 5 は変換なので実行を起動しない。実行を起動するのは 6 だけである
- C. 3(
count)・5(display)・6(write)がアクションでそれぞれジョブを起動する。運用データパイプラインでは通常「データ書き込みアクションのみ」を存在させるべきで、3 と 5 はクエリ最適化を妨げボトルネックの原因になる - D. 2 の
filterは元のdfを破壊的に更新するため、結果を変数へ代入しなくても後続行は同じ結果になる
Q2. 次の DataFrame チェーンについて、シャッフル(ステージ境界)を伴う操作の分類として正しいものはどれか。
python
result = (spark.read.table("bronze.orders")
.filter(col("amount") > 0) # (1)
.withColumn("d", to_date("ts")) # (2)
.join(dim_customer, on="cust_id") # (3)
.groupBy("d", "region") # (4)
.agg(sum("amount").alias("total"))
.orderBy(desc("total"))) # (5)- A. (1) と (2) がシャッフルを伴い、(3)(4)(5) はシャッフル不要
- B. (3)(4)(5) が広い変換(wide transformation)でシャッフルを伴い、(1)(2) は狭い変換(narrow transformation)でシャッフル不要
- C. (1)〜(5) のすべてがシャッフルを伴うため、この時点で 5 回のシャッフルが確定する
- D.
joinは常にブロードキャスト結合に最適化されるためシャッフルは起きず、(4)(5) のみがシャッフルする
Q3. 次のクエリを Engineering 部門(Fred 21000 / Chloe 23000 / Tom 23000 / Paul 29000)に対して実行したとき、Chloe の行の rows_total と range_total の組み合わせはどれか。
sql
SELECT name, salary,
SUM(salary) OVER (ORDER BY salary
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rows_total,
SUM(salary) OVER (ORDER BY salary
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS range_total
FROM employees WHERE dept = 'Engineering';- A.
rows_total = 44000,range_total = 44000 - B.
rows_total = 67000,range_total = 67000 - C.
rows_total = 44000,range_total = 67000 - D.
rows_total = 67000,range_total = 44000
Q4. 次のクエリの total 列に入る値の説明として正しいものはどれか。
sql
SELECT name, dept, salary,
SUM(salary) OVER (PARTITION BY dept ORDER BY salary) AS total
FROM employees;- A. フレームを省略した場合の既定は
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWなのでtotalは部署内の累積和になる。部署全体の合計が欲しいならORDER BYを外すか、フレームをROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWINGにする - B.
ORDER BYを付けてもフレームの既定はパーティション全体なので、totalは部署ごとの総合計になる - C. 集計ウィンドウ関数はフレーム句を省略できないため、この文は構文エラーになる
- D. 既定フレームは
ROWS BETWEEN 1 PRECEDING AND CURRENT ROWなので、totalは直前行と現在行の 2 行分の和になる
Q5. 次の 2 つのクエリを実行したときの結果として、正しい説明はどれか。
sql
-- (1)
SELECT name, RANK() OVER (PARTITION BY dept ORDER BY salary
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS r
FROM employees;
-- (2)
SELECT name, SUM(salary) OVER (PARTITION BY dept
RANGE BETWEEN 5000 PRECEDING AND CURRENT ROW) AS s
FROM employees;- A. (1) は正常に動作し、(2) だけがエラーになる
- B. (1) と (2) はどちらも正常に動作する
- C. (2) は正常に動作し、(1) だけがエラーになる
- D. (1) はランキング関数にウィンドウフレームを付けているため
WINDOW_FUNCTION_AND_FRAME_MISMATCH、(2) は RANGE フレームにORDER BYがないためDATATYPE_MISMATCH.RANGE_FRAME_WITHOUT_ORDERとなり、どちらもエラーになる
Q6. Sales 部門に Lisa 10000 / Alex 30000 / Evan 32000 の 3 行があるとき、次のクエリの Evan の行の lag と lead はどうなるか。
sql
SELECT name, salary,
LAG(salary) OVER (PARTITION BY dept ORDER BY salary) AS lag,
LEAD(salary, 1, 0) OVER (PARTITION BY dept ORDER BY salary) AS lead
FROM employees WHERE dept = 'Sales';- A.
lag = 32000,lead = NULL - B.
lag = 30000,lead = 0 - C.
lag = 30000,lead = NULL - D.
lag = NULL,lead = 0
Q7.(シナリオ)毎日数十 TB を処理する本番 ETL ジョブで、複雑な文字列正規化を実装する必要がある。一部は組み込み関数の組み合わせでは表現しづらい。教材の性能特性に基づく実装方針として最も適切なものはどれか。
- A. 行ごとの Python UDF が最も汎用的で、大規模 ETL でも性能上の問題は起きない
- B. 性能順は「行ごとの Python UDF > pandas UDF > Scala UDF > 組み込み関数」なので、Python UDF を選ぶ
- C. pandas UDF は Apache Arrow を使うため Scala UDF より高速であり、まず pandas UDF で実装すべき
- D. まず組み込み関数 / SQL UDF で表現し、どうしても無理なら Apache Arrow でベクトル化される pandas UDF を使う。性能順は「組み込み関数 / SQL UDF > Scala UDF > pandas UDF > 行ごとの Python UDF」であり、行ごとの Python UDF は最終手段
Q8. 次の pandas UDF について、型ヒントから判断できる種類と特徴の組み合わせとして正しいものはどれか。
python
from typing import Iterator, Tuple
import pandas as pd
from pyspark.sql.functions import pandas_udf
@pandas_udf("double")
def calc(batch_iter: Iterator[Tuple[pd.Series, pd.Series]]) -> Iterator[pd.Series]:
for weight, height in batch_iter:
yield weight / (height ** 2)- A. 「Iterator[複数 Series] → Iterator[Series]」型で、複数列を入力に取るスカラー操作。バッチのイテレーターを受け取るため、ML モデルのロードなど重い状態初期化を 1 回だけ行える
- B. 「Series → Scalar」型の集計 UDF で、
groupBy().agg()またはWindowでしか使えない - C. UDTF であり、入力行ごとに複数行(複数列)を返す
- D. UDAF であり、部分集計に対応するため各グループ全体をメモリにロードする必要がない
Q9. 次の SQL は、strlen が NULL 入力で例外を投げる Python UDF のとき、実行時エラーになることがある。この理由と対策として正しいものはどれか。
sql
SELECT s FROM test1 WHERE s IS NOT NULL AND strlen(s) > 1;- A.
WHERE句では UDF を使えないため、必ずSELECT句に移す必要がある - B.
ANDは左から右に短絡評価されるので、この文は必ず安全に動作する。エラーの原因は別にある - C. Spark SQL では部分式の評価順序が保証されず、
AND/ORに短絡(ショートサーキット)セマンティクスがないため、NULL 除外前に UDF が呼ばれ得る。対策は UDF 自体を NULL 対応にするか、IF/CASE WHENで NULL チェックした条件分岐内で UDF を呼ぶこと - D. UDF を
DETERMINISTICとして登録すれば、評価順序が保証されるので解決する
Q10. vals 列に array(-2, 3, 4) を持つ行に対して次のクエリを実行したとき、s に入る値はどれか。
sql
SELECT id,
aggregate(filter(transform(vals, x -> x * x), y -> y > 5), 0, (acc, v) -> acc + v) AS s
FROM t;- A.
5 - B.
25 - C.
29 - D. 高階関数はネストできないため、この文は解析エラーになる
解答・解説
Q1. 正解: Ccount / display / write はいずれもアクションであり、呼ばれた時点でそれまでの変換をまとめて実行するジョブを起動する。教材は「運用データパイプラインでは通常、データ書き込みアクションのみを存在させるべき。その他のアクション(count / display / collect など)はクエリ最適化を妨げ、ボトルネックの原因になる」と明記している。A は変換とアクションの区別が誤り。B は count / display をアクションではなく変換としている点が誤り。D は DataFrame の不変性(変換は元を変更せず新しい DataFrame を返す)に反する。
Q2. 正解: Bfilter / withColumn / select は各出力パーティションが単一の入力パーティションに依存する狭い変換でシャッフル不要。join / groupBy / orderBy / distinct / repartition は出力パーティションが複数の入力パーティションに依存する広い変換で、パーティション間のデータ再分配(シャッフル)とネットワーク I/O が発生し、ステージ境界を作る。D はブロードキャスト結合が AQE のしきい値(既定 30MB)などの条件下で選ばれる最適化であり「常に」ではない点が誤り。
Q3. 正解: CROWS は現在行の前後の物理的な行数でフレームを表し、同じ ORDER BY 値のタイ行も個別にカウントする。よって Chloe(2 行目)は 21000 + 23000 = 44000。RANGE は現在行の ORDER BY 値からの値のオフセットでフレームを表し、同値の行をまとめて扱うため、23000 のタイ 2 行を含めて 21000 + 23000 + 23000 = 67000 になる。Tom の行では rows_total = 67000, range_total = 67000 で一致するため、差が出るのはタイの先頭行である点がポイント。
Q4. 正解: A 集計ウィンドウ関数に ORDER BY を付けてフレームを明示しない場合、既定フレームは RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW になり、結果は累積計算になる。「全期間の合計を出したいのに ORDER BY を付けたら累積和になってしまう」は典型的な落とし穴。ORDER BY を付けない場合はパーティション全体が対象になる(B の説明は ORDER BY を付けた場合に当てはまらない)。C はフレーム句が任意である点、D は既定フレームが ROWS ではない点が誤り。
Q5. 正解: D ランキング関数(ROW_NUMBER / RANK / DENSE_RANK / NTILE / PERCENT_RANK)は ORDER BY を含む window_spec が必須だが、ウィンドウフレームを含めてはならない。付けると WINDOW_FUNCTION_AND_FRAME_MISMATCH エラーになる。RANGE フレームは ORDER BY が必須で式は 1 つのみであり、ORDER BY がなければ DATATYPE_MISMATCH.RANGE_FRAME_WITHOUT_ORDER、複数式なら DATATYPE_MISMATCH.RANGE_FRAME_MULTI_ORDER になる。したがって両方エラー。
Q6. 正解: BLAG(col) は現在行より前の行の値、LEAD(col) は後の行の値を返す。給与昇順(Lisa 10000 → Alex 30000 → Evan 32000)で Evan は最後の行なので、lag は直前の Alex の 30000。lead は後続行がないが LEAD(salary, 1, 0) で範囲外時のデフォルト値 0 を指定しているため 0 になる(デフォルトを指定しなければ NULL)。A は lag/lead が逆、C はデフォルト値指定を無視、D は先頭行 Lisa の lag の説明。
Q7. 正解: D 教材が示す性能順(速い → 遅い)は「組み込み関数 = SQL UDF > Scala UDF > pandas UDF > 行ごとの Python UDF」。Python UDF / pandas UDF はデータをシリアライズして JVM から Python インタプリタへ移動するため Scala UDF より遅い傾向がある(C が誤りである理由)。ただし pandas UDF は Apache Arrow でシリアライズコストを削減しベクトル化するため、行ごとの Python UDF より最大 100 倍高速。また大規模データセット・ETL・ストリーミングなど定期/継続実行のワークロードでは、分散処理向けに最適化された組み込み関数を優先するのが原則。
Q8. 正解: AIterator[Tuple[pd.Series, ...]] -> Iterator[pd.Series] は「Iterator[複数 Series] → Iterator[Series]」型で、複数列を入力に取るスカラー操作。イテレーター型(Iterator[pd.Series] -> Iterator[pd.Series] も同様)の利点は、ループの外側で状態初期化(ML モデルのロードなど)を 1 回だけ実行できること。B の「Series → Scalar」は pd.Series, ... -> Any の型ヒントで、集計用途・部分集計非対応・各グループ全体をメモリにロードするもの(D の説明も逆)。C の UDTF は @udtf で yield により入力行ごとに複数行を返すもので別物。
Q9. 正解: C Spark SQL では部分式の評価順序が保証されず、AND / OR に左から右への短絡セマンティクスはない。したがって WHERE s IS NOT NULL AND strlen(s) > 1 と書いても、NULL 除外後に UDF が呼ばれる保証はない。対策は 2 つ:(1)UDF 自体を NULL 対応にする(例: lambda s: len(s) if s is not None else -1)、(2)IF / CASE WHEN で NULL チェックし条件分岐内で UDF を呼ぶ(例: where if(s is not null, strlen(s), null) > 1)。D の DETERMINISTIC は最適化のためのマークで、評価順序を保証するものではない。
Q10. 正解: B 内側から順に評価する。transform(vals, x -> x * x) で [-2, 3, 4] → [4, 9, 16]。filter(..., y -> y > 5) は述語が真の要素だけを残すので [9, 16]。aggregate([9, 16], 0, (acc, v) -> acc + v) は初期値 0 から畳み込むので 9 + 16 = 25。A は 2 乗前の元配列の和、C は filter を無視した 4 + 9 + 16。D は誤りで、高階関数はネストして組み合わせられる。高階関数は explode → 集計 → collect_list のようなシャッフルを伴う往復を避けて配列を直接操作できるため、ネストデータ処理で性能面のメリットがあり、UDF より高速。ラムダ構文 x -> expr と 2 引数 (acc, x) -> expr を読めるようにしておくこと。