PySpark Certification Practice Test 03
Data Engineer Review Notes
1. Core Concept An **Inner Join** evaluates two DataFrames against a join condition (equality predicate) and outputs only rows where matching keys exist in **both** the left and right DataFrames. Any row from either side whose key has no corresponding counterpart in the other DataFrame is excluded from the result set.
2. Detailed Breakdown In PySpark Catalyst optimization and physical execution: - The default join type when executing `df1.join(df2, on="key")` without specifying the `how` parameter is `how="inner"`. - Under the hood, Catalyst analyzes table statistics. If one side is smaller than `spark.sql.autoBroadcastJoinThreshold` (default 10 MB), Spark plans a **BroadcastHashJoinExec (BHJ)**. The small DataFrame is collected to the driver, hashed, and broadcast to all worker executors, eliminating shuffle. - If both tables exceed the threshold and Adaptive Query Execution (AQE) does not dynamically convert it, Spark executes a **SortMergeJoinExec (SMJ)** (or `ShuffledHashJoinExec` if enabled). Both DataFrames are shuffled across cluster nodes partitioned by `hash(join_key) % spark.sql.shuffle.partitions`, sorted locally by join key within each partition, and merged. - Rows with NULL join keys are automatically dropped from the output in an inner join because `NULL = NULL` evaluates to `UNKNOWN` (false in boolean join predicates).
3. Why the Correct Answer is Correct The business requirement explicitly dictates: *"Only customers existing in both datasets should appear in the output."* - An **Inner Join** strictly retains rows with matching keys in both datasets. - Customers present in the customer table but missing from address data are discarded. - Address records without a matching customer record are discarded. - Therefore, option A is precisely the join type required.
4. Why Each Distractor is Incorrect - **B (Left Outer Join):** A left outer join preserves *all* records from the left DataFrame regardless of whether a match exists on the right side. If a customer lacks an address, the customer row is still emitted with `NULL` address fields, violating the requirement that records must exist in both. - **C (Full Outer Join):** A full outer join preserves all records from *both* DataFrames. Unmatched customer records and unmatched address records would appear with NULLs, directly violating the exclusion requirement. - **D (Left Anti Join):** A left anti join returns only rows from the left DataFrame that have **no** match in the right DataFrame. It would return only customers *without* addresses, which is the exact opposite of what was requested.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import col
spark = SparkSession.builder.appName("InnerJoinDemo").getOrCreate()
# Customer demographics
customers = spark.createDataFrame([
(101, "Alice"),
(102, "Bob"),
(103, "Charlie")
], ["customer_id", "name"])
# Customer addresses (Bob has no address record, 104 is orphan address)
addresses = spark.createDataFrame([
(101, "123 Maple St, Seattle"),
(103, "456 Oak Ave, Denver"),
(104, "789 Pine Rd, Austin")
], ["customer_id", "address"])
# Perform Inner Join
inner_joined_df = customers.join(addresses, on="customer_id", how="inner")
inner_joined_df.show(truncate=False)
# Result:
# +-----------+-------+---------------------+
# |customer_id|name |address |
# +-----------+-------+---------------------+
# |101 |Alice |123 Maple St, Seattle|
# |103 |Charlie|456 Oak Ave, Denver |
# +-----------+-------+---------------------+6. Key Takeaway An Inner Join retains only intersecting records matching keys in both DataFrames, automatically filtering out non-matching rows and NULL keys on either side.
7. Production Best Practice When joining large datasets on nullable columns, pre-filter NULLs before joining (`df.filter(col("id").isNotNull())`) or ensure join keys are cleanly typed. In skewed joins where certain keys (e.g. guest customer IDs) appear millions of times, use AQE skew join optimization (`spark.sql.adaptive.skewJoin.enabled=true`) or broadcast the dimensional lookup table.
8. Real-World Engineering Scenario In a financial KYC (Know Your Customer) reporting pipeline, analysts need verified profiles where both core CRM customer identity and authenticated government ID documents are confirmed. An inner join ensures that unverified users without documentation and orphan document scans without registered users are excluded from the regulatory filing.
9. Common Pitfall / Anti-Pattern Specifying `df1.join(df2, df1["id"] == df2["id"])` creates duplicate column names in the resulting DataFrame, which breaks downstream references when calling `df["id"]`. Prefer joining with a string key `df1.join(df2, on="id")` or aliasing DataFrames and dropping the duplicate column explicitly.
10. Tutorial Reference Explore join mechanics and physical execution strategies in [PySpark DataFrame Joins](/tutorials/pyspark/df-joins).
PySpark Certification Practice Test 03
Data Engineer Review Notes
Insightful Saga โ Modern Data Engineering Certification Preparation
Question 41
A newly joined Data Engineer reviews the following requirement: "We have customer data and customer address data. Only customers existing in both datasets should appear in the output." Which join type should be recommended?
1. Core Concept An **Inner Join** evaluates two DataFrames against a join condition (equality predicate) and outputs only rows where matching keys exist in **both** the left and right DataFrames. Any row from either side whose key has no corresponding counterpart in the other DataFrame is excluded from the result set.
2. Detailed Breakdown In PySpark Catalyst optimization and physical execution: - The default join type when executing `df1.join(df2, on="key")` without specifying the `how` parameter is `how="inner"`. - Under the hood, Catalyst analyzes table statistics. If one side is smaller than `spark.sql.autoBroadcastJoinThreshold` (default 10 MB), Spark plans a **BroadcastHashJoinExec (BHJ)**. The small DataFrame is collected to the driver, hashed, and broadcast to all worker executors, eliminating shuffle. - If both tables exceed the threshold and Adaptive Query Execution (AQE) does not dynamically convert it, Spark executes a **SortMergeJoinExec (SMJ)** (or `ShuffledHashJoinExec` if enabled). Both DataFrames are shuffled across cluster nodes partitioned by `hash(join_key) % spark.sql.shuffle.partitions`, sorted locally by join key within each partition, and merged. - Rows with NULL join keys are automatically dropped from the output in an inner join because `NULL = NULL` evaluates to `UNKNOWN` (false in boolean join predicates).
3. Why the Correct Answer is Correct The business requirement explicitly dictates: *"Only customers existing in both datasets should appear in the output."* - An **Inner Join** strictly retains rows with matching keys in both datasets. - Customers present in the customer table but missing from address data are discarded. - Address records without a matching customer record are discarded. - Therefore, option A is precisely the join type required.
4. Why Each Distractor is Incorrect - **B (Left Outer Join):** A left outer join preserves *all* records from the left DataFrame regardless of whether a match exists on the right side. If a customer lacks an address, the customer row is still emitted with `NULL` address fields, violating the requirement that records must exist in both. - **C (Full Outer Join):** A full outer join preserves all records from *both* DataFrames. Unmatched customer records and unmatched address records would appear with NULLs, directly violating the exclusion requirement. - **D (Left Anti Join):** A left anti join returns only rows from the left DataFrame that have **no** match in the right DataFrame. It would return only customers *without* addresses, which is the exact opposite of what was requested.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import col
spark = SparkSession.builder.appName("InnerJoinDemo").getOrCreate()
# Customer demographics
customers = spark.createDataFrame([
(101, "Alice"),
(102, "Bob"),
(103, "Charlie")
], ["customer_id", "name"])
# Customer addresses (Bob has no address record, 104 is orphan address)
addresses = spark.createDataFrame([
(101, "123 Maple St, Seattle"),
(103, "456 Oak Ave, Denver"),
(104, "789 Pine Rd, Austin")
], ["customer_id", "address"])
# Perform Inner Join
inner_joined_df = customers.join(addresses, on="customer_id", how="inner")
inner_joined_df.show(truncate=False)
# Result:
# +-----------+-------+---------------------+
# |customer_id|name |address |
# +-----------+-------+---------------------+
# |101 |Alice |123 Maple St, Seattle|
# |103 |Charlie|456 Oak Ave, Denver |
# +-----------+-------+---------------------+6. Key Takeaway An Inner Join retains only intersecting records matching keys in both DataFrames, automatically filtering out non-matching rows and NULL keys on either side.
7. Production Best Practice When joining large datasets on nullable columns, pre-filter NULLs before joining (`df.filter(col("id").isNotNull())`) or ensure join keys are cleanly typed. In skewed joins where certain keys (e.g. guest customer IDs) appear millions of times, use AQE skew join optimization (`spark.sql.adaptive.skewJoin.enabled=true`) or broadcast the dimensional lookup table.
8. Real-World Engineering Scenario In a financial KYC (Know Your Customer) reporting pipeline, analysts need verified profiles where both core CRM customer identity and authenticated government ID documents are confirmed. An inner join ensures that unverified users without documentation and orphan document scans without registered users are excluded from the regulatory filing.
9. Common Pitfall / Anti-Pattern Specifying `df1.join(df2, df1["id"] == df2["id"])` creates duplicate column names in the resulting DataFrame, which breaks downstream references when calling `df["id"]`. Prefer joining with a string key `df1.join(df2, on="id")` or aliasing DataFrames and dropping the duplicate column explicitly.
10. Tutorial Reference Explore join mechanics and physical execution strategies in [PySpark DataFrame Joins](/tutorials/pyspark/df-joins).
Question 42
Review Requirement: "All customers must appear in the output. Address information should appear only when available." Which join should be chosen?
1. Core Concept A **Left Join** (also known as **Left Outer Join**) retains every row from the primary (left) DataFrame regardless of whether a matching record exists in the secondary (right) DataFrame. When a matching key exists on the right side, its attributes are populated; when no match is found, columns from the right DataFrame are filled with `NULL`.
2. Detailed Breakdown In PySpark: - Syntax: `customers_df.join(address_df, on="customer_id", how="left")` or `how="left_outer"`. - Execution: - If the right DataFrame is small, PySpark employs a **BroadcastHashJoinExec**. All left rows stream through; if a probe into the broadcast hash table yields a hit, fields are merged. If not, NULLs are appended for right-side schema columns. - If neither table can be broadcast, PySpark shuffles both DataFrames by the join key into partitions. For every key present in the left partition, if no corresponding right key exists, output rows are emitted with right-side column values set to NULL. - Preserving row count: If right-table join keys are 1-to-1 or 1-to-0, the output row count equals the left DataFrame row count. If right-table keys have 1-to-many relationships, duplicate rows are produced for that left key.
3. Why the Correct Answer is Correct The requirement states: 1. *"All customers must appear in the output."* -> Preserves 100% of rows from the customer (left) DataFrame. 2. *"Address information should appear only when available."* -> If a customer has no address record, the customer remains present and address columns become `NULL`. Therefore, placing the customer DataFrame on the left and performing a **Left Join** perfectly satisfies both criteria.
4. Why Each Distractor is Incorrect - **A (Inner Join):** Discards any customer who does not have an existing address record in the right DataFrame, violating "All customers must appear in the output." - **C (Cross Join):** Produces a Cartesian product (M x N rows), matching every customer with every single address in the database without considering customer IDs, causing severe data explosion and corruption. - **D (Right Join):** A Right Join would keep all records from the right table (addresses) and drop customers who don't have addresses while keeping orphan addresses. To make it work, the tables would have to be reversed, which is counter-intuitive when customers are the primary entity.
5. Code Example
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("LeftJoinDemo").getOrCreate()
customers = spark.createDataFrame([
(1, "Alice"),
(2, "Bob"),
(3, "Charlie")
], ["customer_id", "customer_name"])
addresses = spark.createDataFrame([
(1, "Seattle, WA"),
(3, "Denver, CO")
], ["customer_id", "city"])
# Left join keeps Bob even though he has no address
result_df = customers.join(addresses, on="customer_id", how="left")
result_df.show()
# +-----------+-------------+-----------+
# |customer_id|customer_name| city|
# +-----------+-------------+-----------+
# | 1| Alice|Seattle, WA|
# | 2| Bob| NULL|
# | 3| Charlie| Denver, CO|
# +-----------+-------------+-----------+6. Key Takeaway A Left Outer Join guarantees complete retention of all records from the left dataset, supplementing matched attributes from the right dataset and populating NULLs where matches do not exist.
7. Production Best Practice Always verify whether the right table contains duplicate keys before performing a left join. If the right table has 5 address records for customer 1, the left join will emit 5 rows for Alice, inflating your total row count. If only one address is needed, deduplicate the right table using `dropDuplicates(["customer_id"])` or window ranking beforehand.
8. Real-World Engineering Scenario In master data management (MDM) and customer 360 pipelines, customer profiles are enriched with optional auxiliary attributes such as loyalty program status, recent newsletter signups, or secondary phone numbers. A left join guarantees no core customer records are dropped even if optional attributes have not yet been populated.
9. Common Pitfall / Anti-Pattern Applying a `filter()` or `where()` on a right-table column after a left join (e.g., `.filter(col("city") != "New York")`) inadvertently converts the left join into an inner join because `NULL != "New York"` evaluates to false, silently discarding all customers who had no address! Use null-safe checks: `.filter((col("city") != "New York") | col("city").isNull())`.
10. Tutorial Reference Deepen your knowledge of join types in [PySpark DataFrame Joins](/tutorials/pyspark/df-joins).
Question 43
Review Requirement: "Return all records from both systems even when no matching key exists." Which join is appropriate?
1. Core Concept A **Full Outer Join** (or **Full Join**) combines the results of both left and right outer joins. It produces a comprehensive unified dataset containing all rows from the left DataFrame and all rows from the right DataFrame. When keys match, columns are joined in a single row; when keys exist in only one DataFrame, the missing counterpart's columns are filled with `NULL`.
2. Detailed Breakdown In PySpark: - Syntax: `df1.join(df2, on="key", how="full")` (or `how="outer"`, `how="full_outer"`, `how="fullouter"`). - Catalyst & Physical Plan: - Full outer joins cannot be implemented with standard BroadcastHashJoin unless Spark performs specialized non-broadcast handling, because unmatched records from both sides must be tracked and emitted. - Spark typically implements full outer joins using **SortMergeJoinExec (SMJ)** or **ShuffledHashJoinExec**. - Both DataFrames are shuffled by hash partition on the join key, sorted, and merged. The physical operator iterates through streams from both sides, emitting joined rows for matches, followed by unmatched left rows with right NULLs, and unmatched right rows with left NULLs.
3. Why the Correct Answer is Correct The requirement explicitly states: *"Return all records from both systems even when no matching key exists."* - Unmatched records from System A must be kept. - Unmatched records from System B must be kept. - Matched records between System A and System B must be combined. This is the foundational definition and exact behavior of a **Full Outer Join**.
4. Why Each Distractor is Incorrect - **A (Semi Join):** A Left Semi Join only returns rows from the left DataFrame that have a match in the right DataFrame, completely discarding unmatched left records and all right-side records. - **B (Left Join):** Only preserves records from the left DataFrame. Any record present in the right system that has no match in the left system is completely discarded. - **D (Inner Join):** Excludes unmatched records from both sides, keeping only the strict intersection.
5. Code Example
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("FullOuterJoinDemo").getOrCreate()
crm_users = spark.createDataFrame([
(10, "Alice"),
(20, "Bob")
], ["user_id", "crm_name"])
billing_users = spark.createDataFrame([
(20, "Paid-Tier"),
(30, "Trial-Tier")
], ["user_id", "plan"])
# Full outer join preserves 10 (CRM only), 20 (both), and 30 (Billing only)
merged = crm_users.join(billing_users, on="user_id", how="full")
merged.show()
# +-------+--------+----------+
# |user_id|crm_name| plan|
# +-------+--------+----------+
# | 10| Alice| NULL|
# | 20| Bob| Paid-Tier|
# | 30| NULL|Trial-Tier|
# +-------+--------+----------+6. Key Takeaway A Full Outer Join guarantees complete data retention across both input datasets, ensuring neither left nor right unmatched records are dropped.
7. Production Best Practice When performing full outer joins on large datasets, monitor partition skew and memory usage closely. Because unmatched keys from both sides are buffered and processed during the merge phase, memory pressure increases. Use coalesce or coalesce-aware coalesce joins (`coalesce(df1["id"], df2["id"]).alias("id")`) if using multiple conditions rather than a unified `on="id"` parameter.
8. Real-World Engineering Scenario During a post-merger integration of two enterprise ERP systems (e.g. legacy SAP and newly acquired NetSuite), data engineers run data reconciliation to find matched inventory items, items existing only in SAP, and items existing only in NetSuite. A full outer join provides the complete audit universe for discrepancy analysis.
9. Common Pitfall / Anti-Pattern Joining with `df1.join(df2, df1["id"] == df2["id"], "full")` results in two separate columns named `id`. In unmatched rows, one of those columns will be `NULL`! If you subsequently select `df1["id"]`, rows that existed only in `df2` will show `NULL` IDs. Always use the string syntax `on="id"` or `coalesce(df1["id"], df2["id"])` to ensure the final identifier is always populated.
10. Tutorial Reference Read about join strategies and physical execution in [PySpark DataFrame Joins](/tutorials/pyspark/df-joins).
Question 44
A retail company wants total sales amount by store. Which operation should be performed first?
1. Core Concept In PySpark, computing summary metrics (such as total sales) sliced across categorical dimensions (such as store, department, or date) requires grouping rows that share identical dimension values together. The primary operation that initiates this grouped data transformation is **`groupBy()`** (or its SQL alias `groupby()`).
2. Detailed Breakdown In the PySpark DataFrame API: - Calling `df.groupBy("store_id")` transforms an untyped DataFrame into a `RelationalGroupedDataset`. - `RelationalGroupedDataset` exposes aggregation operator methods, such as `.sum("sales_amount")`, `.avg()`, `.count()`, `.max()`, `.min()`, and the generalized `.agg()`. - Execution Plan: - In Spark's physical execution plan, `groupBy()` introduces an **Aggregate** operator. - Spark typically uses a two-phase aggregation: 1. **Partial Aggregate (Map-side combine):** Aggregates data locally within each partition on the worker nodes before shuffling, drastically reducing the volume of records transmitted over the network. 2. **Exchange (Shuffle):** Shuffles the partially aggregated intermediate results by `hash(grouping_key) % partitions`. 3. **Final Aggregate (Reduce-side):** Merges the intermediate sums across partitions to produce the final single row per unique store.
3. Why the Correct Answer is Correct To compute the total sales amount *by store*, the dataset must be partitioned by store keys so that the sum function knows which boundary to calculate totals over. - The fundamental syntax is `df.groupBy("store").sum("amount")`. - Therefore, **`groupBy()`** is the essential foundational operation that must be invoked to establish the grouping context before aggregation.
4. Why Each Distractor is Incorrect - **A (filter()):** Filters rows based on boolean predicates (e.g. `amount > 100`). While filtering can precede grouping to prune data, it does not group data by store or calculate totals. - **B (drop()):** Drops specific columns or removes rows containing null values. It does not perform dimensional partitioning or aggregation. - **C (cast()):** Alters column data types (e.g., converting a string timestamp to a date). It does not aggregate or group records.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import sum as spark_sum
spark = SparkSession.builder.appName("GroupByDemo").getOrCreate()
sales_data = [
("Store_East", 250.0),
("Store_West", 400.0),
("Store_East", 150.0),
("Store_North", 300.0),
("Store_West", 100.0)
]
df = spark.createDataFrame(sales_data, ["store_id", "sales_amount"])
# groupBy establishes the RelationalGroupedDataset
grouped = df.groupBy("store_id").agg(spark_sum("sales_amount").alias("total_sales"))
grouped.show()
# +-----------+-----------+
# | store_id|total_sales|
# +-----------+-----------+
# | Store_East| 400.0|
# | Store_West| 500.0|
# |Store_North| 300.0|
# +-----------+-----------+6. Key Takeaway Aggregations across business entities require partitioning the DataFrame with `groupBy("key")` before applying mathematical reductions like `sum()`.
7. Production Best Practice To optimize `groupBy()` performance across large datasets, ensure that low-cardinality columns are grouped, or use Adaptive Query Execution (AQE) (`spark.sql.adaptive.enabled=true`) so Spark dynamically coalesces post-shuffle partitions to prevent hundreds of tiny, empty partition files.
8. Real-World Engineering Scenario In retail point-of-sale (POS) systems processing millions of daily transactions, regional managers require nightly store-level performance reports. An automated pipeline ingests raw receipt items, performs `df.groupBy("store_id", "business_date").agg(sum("item_total"))`, and loads the summarized metrics into a Snowflake or Delta Lake gold data mart.
9. Common Pitfall / Anti-Pattern Calling `df.groupBy().sum()` on high-cardinality keys with extreme data skew (e.g. a single store having 80% of all company sales) can cause straggler tasks and OOM during the shuffle phase. In skewed aggregation scenarios, consider two-phase salting or broadcast maps if possible.
10. Tutorial Reference Master grouping and aggregation patterns in [PySpark Aggregation and Grouping](/tutorials/pyspark/pyspark-aggregation).
Question 45
Review Requirement: "Calculate total revenue for each product." Which aggregation function is most appropriate?
1. Core Concept In statistical and financial analysis, computing "total revenue" requires calculating the cumulative arithmetic sum of all revenue or transaction amounts associated with an entity. In PySpark, this is achieved using the **`sum()`** aggregation function.
2. Detailed Breakdown PySpark provides `sum()` through two primary interfaces: 1. Direct method on `RelationalGroupedDataset`: `df.groupBy("product_id").sum("revenue")` 2. Through `pyspark.sql.functions.sum`: `df.groupBy("product_id").agg(sum("revenue").alias("total_revenue"))` Under the hood: - The Catalyst optimizer translates `sum()` into the internal `Sum` expression. - Spark generates optimized byte-code via **Tungsten** (Whole-Stage Code Generation). - It allocates memory in Tungsten off-heap row format using `HashAggregateExec` (or `SortAggregateExec` if types are not supported). - Local partition sums are accumulated into accumulator registers, shuffled across the cluster, and added together into the final sum per product.
3. Why the Correct Answer is Correct The business requirement specifically demands calculating *"total revenue"*: - "Total" is mathematically identical to summation. - `sum("revenue")` adds up all monetary figures for each product group. - Therefore, option A is the only appropriate aggregation function.
4. Why Each Distractor is Incorrect - **B (avg()):** Calculates the mean (average) revenue per transaction (`sum / count`), not the total revenue. - **C (max()):** Returns the single highest revenue transaction for a product, ignoring all other sales. - **D (count()):** Counts the number of transactions or units sold, completely ignoring the monetary revenue values.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import sum as _sum, round as _round
spark = SparkSession.builder.appName("SumDemo").getOrCreate()
product_sales = [
("Laptop", 1200.0),
("Mouse", 25.0),
("Laptop", 1400.0),
("Keyboard", 75.0),
("Mouse", 30.0)
]
df = spark.createDataFrame(product_sales, ["product_name", "revenue"])
result = df.groupBy("product_name").agg(_round(_sum("revenue"), 2).alias("total_revenue"))
result.show()
# +------------+-------------+
# |product_name|total_revenue|
# +------------+-------------+
# | Laptop| 2600.0|
# | Mouse| 55.0|
# | Keyboard| 75.0|
# +------------+-------------+6. Key Takeaway Computing totals across dataset groups in PySpark requires the `sum()` aggregation function, which produces arithmetic cumulative totals.
7. Production Best Practice When summing currency or high-precision financial numbers, cast float or double types to `DecimalType(18, 2)` or `DecimalType(38, 4)`. Floating-point binary representation can introduce small rounding anomalies (e.g. `2600.0000000000005`), whereas `DecimalType` maintains exact arithmetic precision.
8. Real-World Engineering Scenario In an e-commerce data pipeline, raw order line items contain quantities and unit prices. The silver-to-gold aggregation step calculates total gross merchandise value (GMV) by multiplying quantity by price and running `sum()` grouped by product category to populate executive financial dashboards.
9. Common Pitfall / Anti-Pattern Using Python's built-in `sum()` function instead of PySpark's `pyspark.sql.functions.sum`. Calling Python's native `sum([col("revenue")])` causes syntax and serialization errors because standard Python cannot compile Catalyst expressions. Always import explicitly: `from pyspark.sql.functions import sum as spark_sum`.
10. Tutorial Reference Learn more about aggregations in [PySpark Aggregation and Grouping](/tutorials/pyspark/pyspark-aggregation).
Question 46
A banking company wants average account balance by branch. Which aggregation should be recommended?
1. Core Concept In data analytics, calculating the central tendency across a set of numeric observations requires computing the arithmetic mean. In PySpark, this operation is executed using the **`avg()`** function (or its synonym **`mean()`**).
2. Detailed Breakdown In PySpark: - Function: `pyspark.sql.functions.avg(col)` or `df.groupBy("branch").avg("balance")`. - Under the hood, Catalyst does not simply compute averages locally on each worker and then average those averages (which is a severe mathematical fallacy if partition sizes differ). - Instead, Spark breaks the `avg()` physical plan into two tracking accumulators during map-side aggregation: 1. `sum`: Cumulative sum of the values. 2. `count`: Total count of non-null values. - During the shuffle and final aggregation stage, Spark sums all partial sums and sums all partial counts across all workers, and finally computes: $$\text{Final Average} = \frac{\sum \text{partial\_sums}}{\sum \text{partial\_counts}}$$ - This guarantees mathematically rigorous results regardless of partition distribution.
3. Why the Correct Answer is Correct The business requirement explicitly calls for: *"average account balance by branch"*. - The arithmetic mean of account balances for each branch is calculated using `avg("balance")` or `mean("balance")`. - Therefore, option B is the correct and optimal aggregation.
4. Why Each Distractor is Incorrect - **A (max()):** Evaluates and returns only the single highest account balance in each branch, failing to provide an average. - **C (count()):** Returns the number of accounts open in each branch, not the average financial balance. - **D (sum()):** Returns the total combined holdings of all accounts in the branch rather than the per-account average.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import avg, round as spark_round
spark = SparkSession.builder.appName("AvgBalanceDemo").getOrCreate()
accounts = [
("Branch_Downtown", 15000.0),
("Branch_Downtown", 5000.0),
("Branch_Uptown", 20000.0),
("Branch_Downtown", 25000.0),
("Branch_Uptown", 80000.0)
]
df = spark.createDataFrame(accounts, ["branch_name", "account_balance"])
result = df.groupBy("branch_name").agg(
spark_round(avg("account_balance"), 2).alias("avg_balance")
)
result.show()
# +---------------+-----------+
# | branch_name|avg_balance|
# +---------------+-----------+
# |Branch_Downtown| 15000.0|
# | Branch_Uptown| 50000.0|
# +---------------+-----------+6. Key Takeaway PySpark computes group averages using `avg()`, which tracks partial sums and counts across worker partitions to ensure mathematically exact distributed calculations.
7. Production Best Practice Account for NULL values when calculating averages. In SQL and PySpark, `avg()` automatically ignores NULLs in the denominator. If your business definition treats missing balances as zero, use `coalesce(col("account_balance"), lit(0))` before passing to `avg()` to prevent skewing the average upward.
8. Real-World Engineering Scenario Bank compliance and liquidity monitoring systems compute average customer deposit balances by branch and product category every night. The resulting averages feed Federal Reserve liquidity ratios and branch performance scorecards.
9. Common Pitfall / Anti-Pattern Trying to average pre-aggregated branch averages from different sources (e.g. `avg(branch_daily_avg)`). If Branch A had 10,000 customers and Branch B had 10 customers, a simple unweighted mean treats both branches equally, destroying analytical validity. Always store and aggregate the underlying `sum` and `count`.
10. Tutorial Reference Review aggregation operations in [PySpark Aggregation and Grouping](/tutorials/pyspark/pyspark-aggregation).
Question 47
Review Requirement: "Determine how many transactions occurred for each customer." Which aggregation fits best?
1. Core Concept In database query languages and distributed data frameworks, counting the frequency or cardinality of occurrences within a group is performed using the **`count()`** aggregation function.
2. Detailed Breakdown In PySpark: - Syntax: - `df.groupBy("customer_id").count()` - Or within an expression: `df.groupBy("customer_id").agg(count("*").alias("tx_count"))` - Physical Plan Execution: - When `count("*")` or `count(1)` is called, Spark executes a **Partial Count** on each partition. Every worker computes the number of matching records in its partition without serializing row objects. - Intermediate integer counts are transmitted across the cluster during the shuffle stage. - The final aggregation stage sums the partial counts for each customer key. - `count(*)` vs `count(col)`: `count(*)` counts all rows including those with null fields, whereas `count(col)` counts only rows where `col` is non-null.
3. Why the Correct Answer is Correct The requirement asks: *"Determine how many transactions occurred for each customer."* - We need the total count of transaction records attributed to each customer ID. - `count()` tallies the number of rows representing transactions per customer. - Therefore, option C is the correct function.
4. Why Each Distractor is Incorrect - **A (sum()):** Computes the arithmetic total of numerical values (e.g. dollar amounts). Passing a non-numeric or non-additive column or trying to count transactions via `sum()` without creating synthetic 1s is inappropriate. - **B (avg()):** Computes the mathematical average of a numeric column, not the frequency of occurrence. - **D (max()):** Finds the maximum transaction value or newest date, not how many transactions occurred.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import count
spark = SparkSession.builder.appName("CountTransactions").getOrCreate()
transactions = [
(101, "TXN_001", 50.0),
(101, "TXN_002", 120.0),
(102, "TXN_003", 15.0),
(101, "TXN_004", 200.0),
(103, "TXN_005", 99.0)
]
df = spark.createDataFrame(transactions, ["customer_id", "txn_id", "amount"])
# Compute transaction count per customer
tx_counts = df.groupBy("customer_id").agg(count("txn_id").alias("transaction_count"))
tx_counts.show()
# +-----------+-----------------+
# |customer_id|transaction_count|
# +-----------+-----------------+
# | 101| 3|
# | 102| 1|
# | 103| 1|
# +-----------+-----------------+6. Key Takeaway To measure event frequency or record cardinality across grouping categories, use `count()`.
7. Production Best Practice When you only need the total number of records per group and do not have specific column nullability concerns, using the shortcut `df.groupBy("key").count()` is both idiomatic and highly optimized, as Catalyst optimizes `count()` to avoid column deserialization.
8. Real-World Engineering Scenario Credit card fraud detection engines look for velocity anomaliesโsuch as a single card executing more than 20 transactions within a 5-minute window. A streaming or batch aggregation pipeline groups events by card token and runs `count()` over rolling time intervals to flag high-frequency bursts.
9. Common Pitfall / Anti-Pattern Confusing `count()` as an aggregation function inside `groupBy()` with `DataFrame.count()`. Calling `df.count()` is an **action** that returns a single integer representing the total rows in the entire DataFrame to the driver, whereas `groupBy("key").agg(count("..."))` returns a transformed DataFrame.
10. Tutorial Reference Read about counting and summary metrics in [PySpark Aggregation and Grouping](/tutorials/pyspark/pyspark-aggregation).
Question 48
A healthcare company wants to know the highest insurance claim amount submitted by each patient. Which function should be used?
1. Core Concept In mathematical and data operations, identifying the greatest or upper-bound numerical value within an grouped observation set is achieved using the **`max()`** aggregation function.
2. Detailed Breakdown In PySpark: - Syntax: `pyspark.sql.functions.max(col)` or `df.groupBy("patient_id").max("claim_amount")`. - Under the hood: - Spark's Catalyst optimizer translates `max()` into the `Max` expression. - During the map phase on each partition, Spark computes the local maximum for each key, retaining only a single record per patient on each worker. - Shuffling transmits only these local maximum values rather than all raw claims. - The reduce phase compares the local partition maximums to yield the absolute global maximum per patient. - Works across data types: While commonly used on numbers, `max()` also operates on timestamps and dates (returning the most recent timestamp) and strings (returning the lexicographically latest string).
3. Why the Correct Answer is Correct The business requirement explicitly asks for the *"highest insurance claim amount submitted by each patient"*: - "Highest" corresponds directly to the maximum numerical value. - `max("claim_amount")` compares all claims for each patient and extracts the maximum. - Therefore, option D is the correct function.
4. Why Each Distractor is Incorrect - **A (min()):** Finds the lowest (smallest) claim amount submitted by the patient, which is the exact inverse of what is required. - **B (avg()):** Calculates the mean claim value across all visits, which does not identify the single highest claim. - **C (sum()):** Adds all claim amounts together to find the patient's lifetime total claims, failing to isolate the peak claim.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import max as spark_max
spark = SparkSession.builder.appName("MaxClaimDemo").getOrCreate()
claims_data = [
("Patient_A", 150.0),
("Patient_B", 4200.0),
("Patient_A", 8500.0),
("Patient_B", 310.0),
("Patient_A", 600.0)
]
df = spark.createDataFrame(claims_data, ["patient_id", "claim_amount"])
# Find highest claim per patient
highest_claims = df.groupBy("patient_id").agg(spark_max("claim_amount").alias("max_claim"))
highest_claims.show()
# +----------+---------+
# |patient_id|max_claim|
# +----------+---------+
# | Patient_A| 8500.0|
# | Patient_B| 4200.0|
# +----------+---------+6. Key Takeaway Use the `max()` aggregation function to extract the peak, highest numerical value, or latest timestamp within each grouping dimension.
7. Production Best Practice When applying `max()` on datasets containing potential outliers (such as claim amounts entered with extra zeros due to human error), pair `max()` with data validation checks or IQR filtering in upstream ingestion layers to prevent erroneous values from polluting downstream analytics.
8. Real-World Engineering Scenario In health insurance underwriting and risk assessment, identifying a patient's single largest historical hospital claim helps actuaries evaluate catastrophic loss exposure and set stop-loss reinsurance coverage thresholds.
9. Common Pitfall / Anti-Pattern If you need both the maximum claim amount AND the claim date or hospital name where that maximum occurred, simply doing `groupBy("patient_id").agg(max("claim_amount"))` will NOT retain the other metadata columns. To retrieve the entire row corresponding to the maximum value, use a Window function with `row_number().over(Window.partitionBy("patient_id").orderBy(desc("claim_amount")))` filtered to `row_number == 1`.
10. Tutorial Reference Explore aggregation techniques in [PySpark Aggregation and Grouping](/tutorials/pyspark/pyspark-aggregation).
Question 49
A telecom company wants the lowest monthly usage recorded for each customer. Which aggregation is appropriate?
1. Core Concept In statistical and database operations, finding the lowest or minimum numerical value across a collection of observations is performed using the **`min()`** aggregation function.
2. Detailed Breakdown In PySpark: - Syntax: `pyspark.sql.functions.min(col)` or `df.groupBy("customer_id").min("usage_gb")`. - Under the hood: - Catalyst represents this as the `Min` physical expression within `HashAggregateExec` or `SortAggregateExec`. - Spark executes two-phase aggregation: 1. **Map-side local reduction:** Calculates the minimum value per customer key within each individual partition. 2. **Shuffle and global reduction:** Merges the local partition minimums to find the global minimum across the entire cluster. - `min()` ignores NULL values. If all values for a group are NULL, `min()` returns NULL. - Can be used on numeric columns, date/timestamp columns (returning the earliest date), and string columns (returning lexicographically earliest string).
3. Why the Correct Answer is Correct The telecom requirement explicitly states: *"the lowest monthly usage recorded for each customer"*. - The lowest value corresponds directly to the minimum. - `min("usage")` extracts the lowest monthly usage recorded for each customer group. - Therefore, option A is the correct answer.
4. Why Each Distractor is Incorrect - **B (max()):** Finds the highest monthly usage recorded, which is the exact opposite of lowest. - **C (count()):** Returns the number of months recorded for each customer, not their lowest usage amount. - **D (sum()):** Computes the total combined usage across all months rather than the lowest single month.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import min as spark_min
spark = SparkSession.builder.appName("MinUsageDemo").getOrCreate()
usage_data = [
("Cust_101", 45.2),
("Cust_102", 12.8),
("Cust_101", 18.5),
("Cust_102", 34.0),
("Cust_101", 30.1)
]
df = spark.createDataFrame(usage_data, ["customer_id", "data_usage_gb"])
lowest_usage = df.groupBy("customer_id").agg(spark_min("data_usage_gb").alias("min_usage_gb"))
lowest_usage.show()
# +-----------+------------+
# |customer_id|min_usage_gb|
# +-----------+------------+
# | Cust_101| 18.5|
# | Cust_102| 12.8|
# +-----------+------------+6. Key Takeaway The `min()` aggregation function computes the lowest value or earliest timestamp within each group in a distributed PySpark DataFrame.
7. Production Best Practice Ensure columns passed to `min()` do not contain sentinel values like `-1` or `-999` that represent missing or unrecorded data in legacy telecom systems. Cleanse or replace sentinel values with `NULL` before running `min()` so they do not falsely appear as the lowest actual usage.
8. Real-World Engineering Scenario In cellular plan optimization pipelines, telecom carriers analyze customer minimum monthly data consumption over a 12-month period to identify dormant subscribers or recommend lower-tier budget plans during churn-prevention campaigns.
9. Common Pitfall / Anti-Pattern Attempting to filter records before aggregation by comparing columns against `min()` directly in a `where()` clause (e.g. `df.where(col("usage") == min(col("usage")))`). In SQL and Spark, aggregate functions cannot appear directly in a WHERE clause. You must compute the aggregate using `groupBy()` or a Window function first.
10. Tutorial Reference Learn more about aggregations in [PySpark Aggregation and Grouping](/tutorials/pyspark/pyspark-aggregation).
Question 50
Review Requirement: "Count distinct customers who placed orders." Which solution best fits?
1. Core Concept In database systems and distributed analytics, counting unique values while ignoring duplicate occurrences of the same entity requires distinct cardinality aggregation. In PySpark, this is achieved using **`countDistinct()`** (or `count(distinct col)` in Spark SQL).
2. Detailed Breakdown In PySpark: - Syntax: `from pyspark.sql.functions import countDistinct; df.select(countDistinct("customer_id"))` or `df.groupBy("store_id").agg(countDistinct("customer_id"))`. - Physical Execution Plan: - Unlike standard `count()`, `countDistinct()` cannot be easily accumulated with a simple integer counter during map-side reduction because identical keys must be deduplicated across partitions. - Spark must shuffle all distinct keys across the cluster, perform deduplication (often through a two-stage aggregation or sorting), and then count the remaining unique keys. - If distinct counting across massive datasets (e.g., billions of website visitors) causes shuffle bottlenecks, Spark provides **`approx_count_distinct()`**, which uses the HyperLogLog++ algorithm to return an approximation with negligible error (default 5% standard deviation) in a fraction of the time and memory.
3. Why the Correct Answer is Correct The requirement explicitly specifies: *"Count **distinct** customers who placed orders."* - Multiple orders placed by the same customer must only be counted once. - `countDistinct()` filters out duplicates and calculates the exact number of unique customer IDs. - Therefore, option B is the exact function required.
4. Why Each Distractor is Incorrect - **A (count()):** Counts every single order row regardless of whether the customer has ordered before, resulting in total order count rather than unique customer count. - **C (sum()):** Arithmetically adds numerical fields together; passing customer identifiers to `sum()` produces an invalid and meaningless number. - **D (avg()):** Calculates arithmetic mean, which has no relevance to counting unique entities.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import count, countDistinct
spark = SparkSession.builder.appName("DistinctCountDemo").getOrCreate()
orders = [
(1, "Cust_101"),
(2, "Cust_102"),
(3, "Cust_101"), # Repeated customer
(4, "Cust_103"),
(5, "Cust_102") # Repeated customer
]
df = spark.createDataFrame(orders, ["order_id", "customer_id"])
# Compare standard count vs countDistinct
result = df.select(
count("customer_id").alias("total_order_rows"),
countDistinct("customer_id").alias("unique_customers")
)
result.show()
# +----------------+----------------+
# |total_order_rows|unique_customers|
# +----------------+----------------+
# | 5| 3|
# +----------------+----------------+6. Key Takeaway Use `countDistinct()` to compute the exact number of unique, deduplicated values in a column or grouped partition.
7. Production Best Practice When analyzing massive web-traffic logs or IoT telemetry with millions of unique entities, exact `countDistinct()` creates huge shuffle memory overhead. If an exact count is not legally or financially mandated, replace it with `approx_count_distinct(col, rsd=0.03)` for a 10x speedup.
8. Real-World Engineering Scenario In digital marketing attribution, advertisers track Monthly Active Users (MAU) and Daily Active Users (DAU). An engineer aggregates user event logs with `countDistinct("user_id")` per month to report authentic active user figures to executive stakeholders and advertisers.
9. Common Pitfall / Anti-Pattern Running `df.distinct().count()` instead of `df.select(countDistinct("col"))`. `df.distinct()` shuffles and deduplicates every single column across the entire DataFrame before counting, whereas `countDistinct("col")` projects and aggregates only the targeted column, saving significant network I/O.
10. Tutorial Reference Learn about distinct operations in [PySpark Aggregation and Grouping](/tutorials/pyspark/pyspark-aggregation).
Question 51
A retail company has: sales_df customers_df The review note states: "Keep only customer records that have sales." Which join type should be evaluated?
1. Core Concept A **Left Semi Join** acts as a high-performance filtering join. It evaluates whether keys from the left DataFrame exist in the right DataFrame. If a match is found, the left row is preserved; if no match is found, the left row is dropped. Crucially, the output contains **only columns from the left DataFrame**, and rows from the left DataFrame are **never duplicated**, even if multiple matching records exist in the right DataFrame.
2. Detailed Breakdown In PySpark: - Syntax: `customers_df.join(sales_df, on="customer_id", how="left_semi")` (or `how="semi"`). - Catalyst Optimizer advantages: - **Early termination / short-circuiting:** When probing the right-side hash table or index, Spark only needs to find the *first* matching occurrence. Once a match is confirmed, it emits the left row immediately without scanning remaining duplicates on the right side. - **No column duplication:** Columns from the right DataFrame are not appended to the output schema. - **Zero row multiplication:** If customer 101 has 500 sales records, an inner join would duplicate customer 101's profile 500 times. A Left Semi Join emits customer 101 exactly once.
3. Why the Correct Answer is Correct The requirement states: 1. *"Keep only customer records that have sales."* 2. It does not ask to append sales columns or duplicate customer rows. 3. A **Left Semi Join** filters the `customers_df` to only those customers who appear in `sales_df`, keeping the customer schema pristine and preventing row explosion. Therefore, option C is the optimal join type.
4. Why Each Distractor is Incorrect - **A (Full Join):** Preserves all customers and all sales, including customers without sales and sales without customers, which completely violates the filtering goal. - **B (Right Join):** Preserves all sales records and appends customer data, returning sales rows rather than just customer records. - **D (Cross Join):** Multiplies every customer by every sale in the company, creating an enormous Cartesian product with no key matching.
5. Code Example
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("LeftSemiJoinDemo").getOrCreate()
customers = spark.createDataFrame([
(1, "Alice"),
(2, "Bob"),
(3, "Charlie")
], ["customer_id", "name"])
sales = spark.createDataFrame([
(101, 1, 50.0),
(102, 1, 75.0), # Alice has 2 sales
(103, 3, 120.0) # Charlie has 1 sale; Bob has 0
], ["sale_id", "customer_id", "amount"])
# Left Semi Join retains Alice and Charlie exactly once, dropping Bob
active_customers = customers.join(sales, on="customer_id", how="left_semi")
active_customers.show()
# +-----------+-------+
# |customer_id| name|
# +-----------+-------+
# | 1| Alice|
# | 3|Charlie|
# +-----------+-------+6. Key Takeaway A Left Semi Join acts as an `IN (subquery)` or `EXISTS` filter, retaining left DataFrame records that have at least one match on the right side without duplicating rows or appending right-side columns.
7. Production Best Practice Whenever you need to filter a master entity table based on the presence of events in a massive fact table (e.g. customers with orders, users with logins), always prefer a `left_semi` join over an `inner` join followed by `distinct()`. `left_semi` avoids generating duplicate rows and shuffles far less data.
8. Real-World Engineering Scenario In a CRM targeted marketing campaign, the marketing team wants to send a survey to "all registered users who made a purchase in the last 30 days." Using `users.join(purchases_last_30_days, on="user_id", how="left_semi")` returns the exact list of distinct eligible users ready for email dispatch.
9. Common Pitfall / Anti-Pattern Using an Inner Join when only left-table filtering is needed (e.g., `customers.join(sales, "id", "inner").select("customers.*").distinct()`). This forces Spark to build full join rows for every transaction, potentially blowing up memory with billions of joined rows, only to discard the sales data with an expensive post-join `distinct()` shuffle.
10. Tutorial Reference Master advanced join patterns in [PySpark DataFrame Joins](/tutorials/pyspark/df-joins).
Question 52
Review Requirement: "Identify customers that do NOT have any orders." Which join is most suitable?
1. Core Concept A **Left Anti Join** acts as an inverse filtering join (equivalent to `NOT IN` or `NOT EXISTS` in SQL). It compares keys between two DataFrames and retains **only** rows from the left DataFrame that have **no match** in the right DataFrame. The output schema contains exclusively the columns of the left DataFrame.
2. Detailed Breakdown In PySpark: - Syntax: `customers_df.join(orders_df, on="customer_id", how="left_anti")` (or `how="anti"`). - Catalyst Optimization: - During execution, Spark evaluates each left row key against the right-side join keys. - If a key from the left DataFrame exists in the right DataFrame, it is discarded immediately. - If a key from the left DataFrame has no counterpart on the right side, the left row is passed directly to the output. - No columns from the right DataFrame are ever materialized or outputted. - It handles NULLs cleanly according to SQL outer join semantics, avoiding standard SQL `NOT IN` pitfalls with NULLs.
3. Why the Correct Answer is Correct The requirement explicitly demands: *"Identify customers that do NOT have any orders."* - Customers who exist in the customer database but have zero rows in the orders database must be isolated. - A **Left Anti Join** between customers (left) and orders (right) outputs exactly and only those customer records that lack corresponding orders. - Therefore, option D is the most suitable join.
4. Why Each Distractor is Incorrect - **A (Right Join):** Retains all order records and matches customers. It does not isolate customers who lack orders. - **B (Full Join):** Retains all customers and all orders. Unmatched customers would have null order fields, but extracting them requires additional post-filtering and shuffles unnecessary data. - **C (Inner Join):** Retains only customers who DO have matching orders, which is the direct opposite of what is required.
5. Code Example
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("LeftAntiJoinDemo").getOrCreate()
customers = spark.createDataFrame([
(101, "Alice"),
(102, "Bob"),
(103, "Charlie")
], ["customer_id", "name"])
orders = spark.createDataFrame([
(5001, 101),
(5002, 103)
], ["order_id", "customer_id"])
# Left Anti Join returns Bob because he has no orders
inactive_customers = customers.join(orders, on="customer_id", how="left_anti")
inactive_customers.show()
# +-----------+----+
# |customer_id|name|
# +-----------+----+
# | 102| Bob|
# +-----------+----+6. Key Takeaway A Left Anti Join efficiently isolates records in the left dataset that have no corresponding match in the right dataset, functioning as a high-performance `NOT EXISTS` filter.
7. Production Best Practice In SQL, writing `WHERE id NOT IN (SELECT id FROM orders)` fails or performs terribly if the subquery returns even a single `NULL`. In PySpark, `left_anti` join handles NULLs safely and generates a highly optimized broadcast or sort-merge anti-join physical plan without subquery null-traps.
8. Real-World Engineering Scenario In customer churn analytics and re-engagement campaigns, marketing systems need to identify registered subscribers who have not placed an order in the last 90 days. Joining the customer master with the 90-day active orders table using `left_anti` immediately produces the target churn cohort.
9. Common Pitfall / Anti-Pattern Performing a Left Outer Join followed by filtering for nulls (e.g. `customers.join(orders, "id", "left").filter(orders["id"].isNull())`). While logically equivalent, this forces Spark to construct and shuffle right-table columns, consuming unnecessary memory compared to `left_anti`.
10. Tutorial Reference Learn more about anti-joins and optimization in [PySpark DataFrame Joins](/tutorials/pyspark/df-joins).
Question 53
A Data Engineer sees: ```python df.groupBy("department").agg(sum("salary")) ``` What business question is being answered?
1. Core Concept In relational algebra and the PySpark DataFrame API, combining a `groupBy("dimension")` clause with an aggregate expression `agg(sum("metric"))` computes the cumulative numerical total of that metric partitioned by each unique value of the dimension.
2. Detailed Breakdown Deconstructing the code snippet:
df.groupBy("department").agg(sum("salary"))1. **df**: The input DataFrame representing employee records.
2. **groupBy("department")**: Partitions the employee dataset logically by the department column (e.g. Engineering, Sales, HR).
3. **.agg(...)**: Initiates multi-argument or explicit aggregation over the grouped data.
4. **sum("salary")**: Computes the arithmetic addition of the salary column values for all employees assigned to that department.
The resulting DataFrame will contain two columns: department and sum(salary).
3. Why the Correct Answer is Correct - The grouping column is `department`. - The aggregate function is `sum()`. - The aggregated metric is `salary`. - Putting them together, the query calculates the total departmental salary expenditure ("Total salary by department"). - Therefore, option A is the exact business question answered.
4. Why Each Distractor is Incorrect - **B (Maximum salary):** Would require the `max("salary")` function, which identifies the highest individual paycheck rather than the departmental total. - **C (Employee count):** Would require `count("*")` or `count("employee_id")`, which counts headcount rather than summing dollar amounts. - **D (Duplicate records):** Identifying duplicate records involves finding rows with `count() > 1` or using `dropDuplicates()`, having nothing to do with summing salaries.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import sum as spark_sum
spark = SparkSession.builder.appName("DeptSalaryDemo").getOrCreate()
employees = [
("Alice", "Engineering", 120000),
("Bob", "Engineering", 110000),
("Charlie", "Sales", 90000),
("Diana", "Sales", 95000),
("Edward", "HR", 80000)
]
df = spark.createDataFrame(employees, ["name", "department", "salary"])
# Calculate total salary by department
result = df.groupBy("department").agg(spark_sum("salary").alias("total_department_salary"))
result.show()
# +-----------+-----------------------+
# | department|total_department_salary|
# +-----------+-----------------------+
# |Engineering| 230000|
# | Sales| 185000|
# | HR| 80000|
# +-----------+-----------------------+6. Key Takeaway Grouping by a categorical column and summing a numerical column aggregates cumulative metric totals partitioned by category.
7. Production Best Practice Always alias aggregated columns using `.alias("meaningful_name")` inside `.agg()`. Without an explicit alias, Spark assigns auto-generated names like `sum(salary)`, which can lead to parsing errors or cumbersome escaping in downstream SQL queries and reporting tools.
8. Real-World Engineering Scenario In enterprise human resource and enterprise resource planning (ERP) systems, quarterly financial audits require comparing budgeted departmental payroll against actual payroll spend. Data engineers build aggregation jobs that execute this exact transformation to feed corporate budget variance models.
9. Common Pitfall / Anti-Pattern Assuming that `groupBy("dept").agg(sum("salary"))` maintains the original row order. In PySpark, shuffle operations do not guarantee row ordering. If downstream consumers expect departments sorted alphabetically or by highest salary, explicitly append `.orderBy(col("total_salary").desc())`.
10. Tutorial Reference Read more about aggregation syntax and best practices in [PySpark Aggregation and Grouping](/tutorials/pyspark/pyspark-aggregation).
Question 54
Review Requirement: "Find average monthly sales by region." Which operation sequence is most appropriate?
1. Core Concept Calculating mean performance metrics across specific geographical or business boundaries requires partitioning the data along dimensional axes followed by applying arithmetic reduction. In PySpark, this standard workflow is executed by chaining **`groupBy()`** with **`avg()`**.
2. Detailed Breakdown To satisfy "average monthly sales by region": 1. **Grouping stage (`groupBy("region")`):** - Segregates the distributed DataFrame records into buckets where all rows with the same region identifier are mapped to the same logical group. 2. **Aggregation stage (`avg("monthly_sales")`):** - Applies the mean formula (sum of monthly sales divided by count of records) to each regional partition. - Executed through two-phase aggregation: map-side partial sum and count, network shuffle across the cluster, and final reduce-side division. 3. Chained expression: `df.groupBy("region").agg(avg("monthly_sales").alias("avg_monthly_sales"))`
3. Why the Correct Answer is Correct - The requirement has two components: "by region" (the grouping dimension) and "average monthly sales" (the statistical metric). - `groupBy()` supplies the regional grouping dimension. - `avg()` supplies the arithmetic mean calculation. - Therefore, the operation sequence **`groupBy() + avg()`** perfectly matches the business requirement.
4. Why Each Distractor is Incorrect - **A (filter() + orderBy()):** Filters rows based on conditions and sorts them; it computes neither groups nor averages. - **C (drop() + cast()):** Drops columns and converts data types; it performs zero aggregation. - **D (distinct() + count()):** Computes the count of unique rows; it cannot calculate the average of sales figures.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import avg, round as spark_round
spark = SparkSession.builder.appName("RegionalSalesDemo").getOrCreate()
sales_data = [
("North", "Jan", 50000.0),
("North", "Feb", 60000.0),
("South", "Jan", 40000.0),
("South", "Feb", 42000.0),
("West", "Jan", 80000.0)
]
df = spark.createDataFrame(sales_data, ["region", "month", "monthly_sales"])
# Sequence: groupBy region + avg monthly_sales
result = df.groupBy("region").agg(
spark_round(avg("monthly_sales"), 2).alias("avg_monthly_sales")
)
result.show()
# +------+-----------------+
# |region|avg_monthly_sales|
# +------+-----------------+
# | North| 55000.0|
# | South| 41000.0|
# | West| 80000.0|
# +------+-----------------+6. Key Takeaway Partitioning data by categorical groups with `groupBy()` followed by `avg()` is the standard, optimized method for calculating dimensional averages in PySpark.
7. Production Best Practice If calculating average monthly sales requires first summing individual daily transactions into monthly totals per region before averaging those months, structure the pipeline in two clear stages: first aggregate daily transactions by `region` and `month` using `sum()`, then aggregate that intermediate result by `region` using `avg()`.
8. Real-World Engineering Scenario National franchise retailers analyze store and regional performance to determine marketing budget allocation. Nightly ETL pipelines aggregate daily register receipts into regional monthly averages to benchmark store manager bonuses against regional standards.
9. Common Pitfall / Anti-Pattern Forgetting to filter out invalid or canceled sales records before running `groupBy().avg()`. If refunded orders or dummy test records remain in the dataset with negative or zero amounts, the calculated average will be artificially deflated.
10. Tutorial Reference Explore aggregation workflows in [PySpark Aggregation and Grouping](/tutorials/pyspark/pyspark-aggregation).
Question 55
A logistics company wants shipment statistics by country. Multiple metrics are required: - Total Shipments - Average Cost - Maximum Cost Which feature should be used?
1. Core Concept While convenience methods like `df.groupBy().sum()` or `df.groupBy().avg()` apply a single aggregation across specified columns, computing **multiple distinct aggregations** (e.g. sum, avg, max, and count) across different columns in a single grouping operation requires the **`agg()`** method.
2. Detailed Breakdown In PySpark: - Calling `df.groupBy("country").agg(...)` allows passing an arbitrary list of PySpark SQL aggregate expressions. - Syntax:
df.groupBy("country").agg(
count("shipment_id").alias("total_shipments"),
avg("cost").alias("avg_cost"),
max("cost").alias("max_cost")
)- Catalyst Optimizer Execution:
- When multiple aggregate functions are placed inside a single
agg()call, Catalyst evaluates them concurrently in a single pass over the grouped data. - Workers compute local partial counts, sums, and maximums within the sameHashAggregateExecoperator buffer. - The shuffle transmits a single combined composite accumulator row per key, avoiding multiple network shuffles.
3. Why the Correct Answer is Correct The business requirement specifies computing three distinct metrics simultaneously (*Total Shipments*, *Average Cost*, and *Maximum Cost*) grouped by country: - Applying individual methods like `.count()` and `.max()` would require running separate DataFrame queries and joining them back together. - The **`agg()`** method is specifically designed to evaluate multiple heterogeneous aggregation expressions in a single efficient execution step. - Therefore, option C is the correct feature.
4. Why Each Distractor is Incorrect - **A (union()):** Concatenates rows from two DataFrames that share identical schemas; it does not compute grouped statistical metrics. - **B (distinct()):** Deduplicates rows; it cannot calculate counts, averages, or maximums. - **D (fillna()):** Replaces null values with default constants; it performs no grouping or statistical aggregation.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import count, avg, max as spark_max, round as spark_round
spark = SparkSession.builder.appName("MultiMetricAgg").getOrCreate()
shipments = [
("USA", "SH_01", 150.0),
("USA", "SH_02", 350.0),
("USA", "SH_03", 100.0),
("Germany", "SH_04", 200.0),
("Germany", "SH_05", 500.0)
]
df = spark.createDataFrame(shipments, ["country", "shipment_id", "cost"])
# Compute multiple metrics in one pass using agg()
stats_df = df.groupBy("country").agg(
count("shipment_id").alias("total_shipments"),
spark_round(avg("cost"), 2).alias("avg_cost"),
spark_max("cost").alias("max_cost")
)
stats_df.show()
# +-------+---------------+--------+--------+
# |country|total_shipments|avg_cost|max_cost|
# +-------+---------------+--------+--------+
# | USA| 3| 200.0| 350.0|
# |Germany| 2| 350.0| 500.0|
# +-------+---------------+--------+--------+6. Key Takeaway The `agg()` method enables computing multiple heterogeneous aggregate metrics (such as count, avg, and max) in a single optimized pass over grouped data.
7. Production Best Practice Always combine related metrics into a single `agg()` invocation rather than creating multiple separate aggregated DataFrames and joining them. A single `agg()` incurs only one shuffle exchange, whereas multiple aggregations followed by joins incur repeated expensive shuffles.
8. Real-World Engineering Scenario Global supply chain platforms generate country-level daily carrier summaries. A single scheduled Spark job aggregates billions of shipment telemetry pings using `agg()` to compute on-time delivery rates, total freight weight, average fuel surcharge, and maximum transit delay in a single unified pipeline.
9. Common Pitfall / Anti-Pattern Passing a dictionary to `agg()` (e.g. `df.groupBy("country").agg({"cost": "avg", "cost": "max"})`). In Python, dictionaries cannot have duplicate keys, so the second `"cost"` entry overwrites the first! Always use explicit PySpark function arguments: `agg(avg("cost").alias("..."), max("cost").alias("..."))`.
10. Tutorial Reference Master advanced multi-metric aggregations in [PySpark Aggregation and Grouping](/tutorials/pyspark/pyspark-aggregation).
Question 56
A newly hired engineer joins two datasets. Output row count is much larger than expected. The review document shows: No join condition specified. What is the MOST likely issue?
1. Core Concept When two datasets are joined without specifying a join condition (predicate), every single row from the first dataset is matched with every single row from the second dataset. This mathematical operation is a **Cartesian Product**, known in database systems and PySpark as a **Cross Join**.
2. Detailed Breakdown In PySpark: - If DataFrame A contains $M$ rows and DataFrame B contains $N$ rows: $$\text{Output Row Count} = M \times N$$ - For example, joining a table of 100,000 customers with a table of 10,000 products without a condition yields: $$100{,}000 \times 10{,}000 = 1{,}000{,}000{,}000 \text{ (1 Billion rows)}$$ - Execution & Spark Guardrails: - Because Cartesian products are notoriously dangerous and frequently cause cluster Out-Of-Memory (OOM) crashes, Spark SQL disables implicit cross joins by default. - If a query attempts an unconditional join, Spark throws an `AnalysisException`: *"Detected implicit cartesian product for INNER join between logical plans... To enable them, set spark.sql.crossJoin.enabled = true"*. - However, calling `df1.crossJoin(df2)` explicitly bypasses this safety check and executes a `CartesianProductExec` physical plan.
3. Why the Correct Answer is Correct The problem notes: 1. *"Output row count is much larger than expected."* 2. *"No join condition specified."* Joining two tables with no join condition produces a Cartesian product (**Cross Join**), exploding the row count to $M \times N$. Therefore, option D is the most likely issue.
4. Why Each Distractor is Incorrect - **A (Broadcast Join):** An internal physical execution strategy where a small table is copied to all executors. It does not alter logical row counts; it merely optimizes execution speed for conditioned joins. - **B (Semi Join):** A filtering join that outputs at most $M$ rows (only matching rows from the left table). It can never produce more rows than the left table. - **C (Left Join):** A left join requires a join predicate; even with duplicate keys, it does not represent an unconditional join by definition.
5. Code Example
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("CrossJoinDemo").getOrCreate()
df_colors = spark.createDataFrame([("Red",), ("Blue",), ("Green",)], ["color"]) # 3 rows
df_sizes = spark.createDataFrame([("S",), ("M",), ("L",), ("XL",)], ["size"]) # 4 rows
# Unconditional cross join yields 3 * 4 = 12 rows
result = df_colors.crossJoin(df_sizes)
print("Row count:", result.count())
# Row count: 12
result.show(6)
# +-----+----+
# |color|size|
# +-----+----+
# | Red| S|
# | Red| M|
# | Red| L|
# | Red| XL|
# | Blue| S|
# | Blue| M|
# +-----+----+6. Key Takeaway Joining DataFrames without a join condition executes a Cross Join (Cartesian Product), multiplying row counts ($M \times N$) and frequently causing severe memory exhaustion.
7. Production Best Practice Keep `spark.sql.crossJoin.enabled` set to `false` in production cluster configurations. This guarantees that accidental omitted join conditions in developer scripts fail immediately during the query planning phase rather than executing an accidental billion-row shuffle that crashes the cluster.
8. Real-World Engineering Scenario A retail promotion engine needs to generate all possible combinations of active product SKUs and store locations to initialize baseline inventory forecast tables. In this specific intentional scenario, an explicit `products.crossJoin(stores)` is valid and expected.
9. Common Pitfall / Anti-Pattern Accidentally omitting the `on` parameter: writing `df1.join(df2)` instead of `df1.join(df2, on="account_id")`. In older PySpark versions or with cross-join flags enabled, this generates an accidental Cartesian explosion that exhausts worker disk space and triggers executor heartbeat timeouts.
10. Tutorial Reference Learn how to optimize and prevent bad join patterns in [PySpark Join Optimization](/tutorials/pyspark/join-optimization-pyspark).
Question 57
Review Requirement: "Display top-selling stores first." Which operation should be added after aggregation?
1. Core Concept Presenting summary reports with leading performers at the beginning of the result set requires sorting the aggregated metric in descending order. In PySpark, this is achieved by chaining **`orderBy()`** (or **`sort()`**) with the **`desc()`** function.
2. Detailed Breakdown In PySpark: - Syntax:
from pyspark.sql.functions import desc, col
df.groupBy("store_id").agg(sum("sales").alias("total_sales")).orderBy(desc("total_sales"))- Or using column object notation:
.orderBy(col("total_sales").desc()). - Execution & Sorting Mechanics:
- An unconditional
orderBy()across an entire DataFrame performs a global sort. - Spark uses a Range Partitioner to divide the data into sorted ranges across worker partitions, followed by local sorting within each partition. - Sorting in descending order (desc) places the largest numerical values at row 1 of partition 0, ensuring "top-selling" entities appear at the very beginning of the output.
3. Why the Correct Answer is Correct The requirement explicitly demands: *"Display top-selling stores first."* - "Top-selling" refers to stores with the highest sales numbers. - "First" means they must appear at the top of the dataset. - Sorting total sales from highest to lowest requires a descending sort via **`orderBy(desc())`**. - Therefore, option A is the correct operation.
4. Why Each Distractor is Incorrect - **B (cast()):** Converts data types (e.g. integer to string); it does not sort or arrange rows. - **C (drop()):** Removes columns or drops null rows; it has no effect on row sorting. - **D (fillna()):** Replaces null values with constant replacements; it does not change row order.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import sum as _sum, desc, col
spark = SparkSession.builder.appName("TopSellingStores").getOrCreate()
sales = [
("Store_A", 1200.0),
("Store_B", 4500.0),
("Store_C", 3100.0),
("Store_A", 800.0)
]
df = spark.createDataFrame(sales, ["store_id", "amount"])
# Aggregate sales and order top-sellers first
top_stores = (
df.groupBy("store_id")
.agg(_sum("amount").alias("total_sales"))
.orderBy(desc("total_sales"))
)
top_stores.show()
# +--------+-----------+
# |store_id|total_sales|
# +--------+-----------+
# | Store_B| 4500.0|
# | Store_C| 3100.0|
# | Store_A| 2000.0|
# +--------+-----------+6. Key Takeaway To rank aggregated entities and display the highest performers at the top of the output, sort descending using `orderBy(desc("column_name"))`.
7. Production Best Practice Global sorting with `orderBy()` on large DataFrames requires shuffling all records across the network to enforce total ordering. If you only need to show the top 10 or top 100 stores on a dashboard, always chain `.limit(N)` immediately after `.orderBy()`. Catalyst recognizes this pattern and plans a `TakeOrderedAndProjectExec`, which maintains a local priority queue of size $N$ per partition, avoiding a massive full-dataset global shuffle.
8. Real-World Engineering Scenario In executive business intelligence dashboards, commercial directors review monthly leaderboards for top-performing retail branches. The daily batch ETL aggregates transactions by store, sorts descending by gross margin, and outputs the top 50 stores to an operational cache for real-time app rendering.
9. Common Pitfall / Anti-Pattern Using `sortWithinPartitions()` when a global sort is required. `sortWithinPartitions()` sorts records only within each individual executor partition without performing a range shuffle. As a result, calling `.show()` on a multi-partition DataFrame will not display the true global top-selling store first.
10. Tutorial Reference Master sorting, sampling, and limiting in [PySpark Sorting, Sampling, and Limit](/tutorials/pyspark/sorting-sampling-limit-pyspark).
Question 58
A manufacturing company wants total machine downtime by plant. Which operation sequence is most appropriate?
1. Core Concept Calculating cumulative metrics (such as total downtime hours) segregated by an organizational or physical location (such as factory plant) is the foundational pattern of dimensional aggregation. In PySpark, this is executed by combining **`groupBy()`** with **`sum()`**.
2. Detailed Breakdown To satisfy "total machine downtime by plant": 1. **Grouping dimension (`plant_id`):** - The dataset contains individual machine logs, each tagged with a `plant_id` and a `downtime_minutes` figure. - Calling `df.groupBy("plant_id")` groups all machine maintenance events occurring within the same physical facility together. 2. **Aggregation metric (`sum("downtime_minutes")`):** - Accumulates the downtime values within each plant group into a single total sum. 3. Execution: - Spark executes map-side partial sums across worker nodes. - Workers exchange intermediate sums partitioned by plant hash. - The final stage computes total downtime per plant with minimal shuffle overhead.
3. Why the Correct Answer is Correct - "By plant" defines the grouping dimension -> **`groupBy("plant")`**. - "Total machine downtime" defines the mathematical summation -> **`sum("downtime")`**. - The operation sequence **`groupBy() + sum()`** directly fulfills both parts of the requirement. - Therefore, option B is the correct operation sequence.
4. Why Each Distractor is Incorrect - **A (cast() + union()):** Converts data types and combines two DataFrames vertically; it does not aggregate or calculate totals. - **C (drop() + orderBy()):** Deletes columns and sorts rows; it calculates no summary totals. - **D (filter() + distinct()):** Filters records and removes duplicate rows; it does not compute cumulative sums.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import sum as spark_sum
spark = SparkSession.builder.appName("PlantDowntime").getOrCreate()
downtime_logs = [
("Plant_Austin", "Machine_01", 45),
("Plant_Austin", "Machine_02", 90),
("Plant_Detroit", "Machine_03", 120),
("Plant_Austin", "Machine_01", 30),
("Plant_Detroit", "Machine_04", 60)
]
df = spark.createDataFrame(downtime_logs, ["plant_id", "machine_id", "downtime_minutes"])
# Sequence: groupBy plant_id + sum downtime_minutes
plant_totals = df.groupBy("plant_id").agg(
spark_sum("downtime_minutes").alias("total_downtime_minutes")
)
plant_totals.show()
# +-------------+----------------------+
# | plant_id|total_downtime_minutes|
# +-------------+----------------------+
# | Plant_Austin| 165|
# |Plant_Detroit| 180|
# +-------------+----------------------+6. Key Takeaway To calculate cumulative totals partitioned by location or category, use the canonical PySpark aggregation sequence `groupBy() + sum()`.
7. Production Best Practice When aggregating industrial telemetry containing millions of sensor readings, ensure timestamp filters are applied *before* `groupBy()` (e.g. `df.filter(col("timestamp") >= current_date() - 30)`). Pushing down filters before the aggregation shuffle minimizes the volume of raw rows that must be processed and shuffled across the network.
8. Real-World Engineering Scenario In Smart Factory IoT architectures, assembly line PLCs emit status codes and downtime increments every second. Data pipelines stream these events into Delta Lake and run hourly batch aggregations grouping by factory plant and machine cell to compute Overall Equipment Effectiveness (OEE) metrics.
9. Common Pitfall / Anti-Pattern Using Python loops or client-side pandas to group and sum distributed records. Pulling millions of factory telemetry rows into the driver via `.toPandas()` or `.collect()` crashes driver memory and eliminates distributed cluster scaling. Always perform `groupBy() + sum()` natively in PySpark.
10. Tutorial Reference Review aggregation principles in [PySpark Aggregation and Grouping](/tutorials/pyspark/pyspark-aggregation).
Question 59
A banking company aggregates billions of transaction records. Only 300 branch-level records are returned. What explains this behavior?
1. Core Concept Aggregation is a **many-to-one reduction operation**. When a dataset containing billions of detailed transactional rows is grouped by a categorical attribute (such as bank branch), the output contains exactly one row per unique value of that attribute. The drastic reduction in output row count is the expected and intended mathematical result of dimensional aggregation.
2. Detailed Breakdown In distributed data architectures: - If a retail bank has 300 branch locations, the cardinality of the `branch_id` column is exactly 300. - When executing:
df.groupBy("branch_id").agg(sum("amount"), count("*"))- Spark processes billions of records across hundreds of executor nodes.
- During map-side combine and reduce-side shuffle, all records sharing the same
branch_idare consolidated into a single summary record. - Because there are only 300 unique branches in the entire organization, the final DataFrame contains exactly 300 rows.
- Data volume is reduced from tens of gigabytes or terabytes of granular events down to a few kilobytes of aggregated metrics.
3. Why the Correct Answer is Correct - The behavior observed is completely normal: billions of transactions across 300 branches yield 300 output rows. - The fundamental mathematical and architectural purpose of aggregation is dimensionality reduction and data volume summarization. - Therefore, option C is the correct and accurate technical explanation.
4. Why Each Distractor is Incorrect - **A (Partitions were removed):** Removing or coalescing partitions changes how data files are stored, not the logical cardinality of grouped keys. - **B (Executors failed):** If executors had failed fatally without recovery, the Spark job would terminate with an exception (`FetchFailedException` or `SparkException`), producing zero rows and failing the pipeline. - **D (Data was deleted):** Aggregation is a read and transformation operation; it does not delete underlying source data in the database or storage lake.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import count, sum as _sum
spark = SparkSession.builder.appName("DataReductionDemo").getOrCreate()
# Simulating many transaction records across just 2 branches
transactions = [
("Branch_East", 100.0),
("Branch_East", 250.0),
("Branch_West", 500.0),
("Branch_East", 150.0),
("Branch_West", 300.0)
]
df = spark.createDataFrame(transactions, ["branch_id", "amount"])
print("Input detailed records count:", df.count()) # 5 rows
# Aggregation collapses rows down to unique branch count
summary = df.groupBy("branch_id").agg(_sum("amount").alias("total_amount"), count("*").alias("tx_count"))
print("Output summary records count:", summary.count()) # 2 rows
summary.show()
# +-----------+------------+--------+
# | branch_id|total_amount|tx_count|
# +-----------+------------+--------+
# |Branch_East| 500.0| 3|
# |Branch_West| 800.0| 2|
# +-----------+------------+--------+6. Key Takeaway Aggregation collapses massive granular datasets into compact summary datasets whose output row count equals the number of distinct grouping keys.
7. Production Best Practice Because an aggregation over low-cardinality keys collapses billions of rows into a few hundred rows, the default post-shuffle partition count (`spark.sql.shuffle.partitions = 200`) will result in 200 output partitions holding only 1 or 2 rows each. Use Adaptive Query Execution (`spark.sql.adaptive.coalescePartitions.enabled=true`) or explicit `.coalesce(1)` before writing the output to avoid generating hundreds of tiny output files.
8. Real-World Engineering Scenario In telecommunications data lakes, billions of daily network connection CDR (Call Detail Record) logs are ingested into bronze storage. Nightly ETL jobs roll up these logs by cell tower ID (approx. 50,000 towers nationwide). The gold reporting table shrinks from 500 GB of raw logs to 5 MB of summarized tower utilization metrics, enabling sub-second executive BI query performance.
9. Common Pitfall / Anti-Pattern Writing aggregated low-cardinality DataFrames directly to cloud storage without coalescing partitions. Writing 300 rows across 200 partition files creates severe "small files problems" on Amazon S3, Azure ADLS, or Google Cloud Storage, degrading downstream query performance.
10. Tutorial Reference Understand shuffle and aggregation internals in [PySpark Shuffle and Partitions](/tutorials/pyspark/shuffle-narrow-wide-pyspark).
Question 60
A Senior Data Engineer reviews an ETL design: 1. Join customer data 2. Join transaction data 3. Aggregate spending 4. Publish customer metrics What is the PRIMARY business purpose of these operations?
1. Core Concept In enterprise data architecture (such as the Medallion Architecture: Bronze -> Silver -> Gold), the ultimate objective of data engineering is not merely running distributed computing algorithms, but **transforming raw, disassociated operational records into enriched, trustworthy, and actionable business intelligence**.
2. Detailed Breakdown Examining the four-stage ETL pipeline: 1. **Join customer data:** Enriches disparate entities with identity, demographic, and account metadata. 2. **Join transaction data:** Links monetary behavioral events to those verified entities. 3. **Aggregate spending:** Collapses millions of raw line-item timestamps into synthesized financial KPIs (e.g. total lifetime spend, average basket size, purchase frequency). 4. **Publish customer metrics:** Exposes high-value, curated metrics to downstream consumer applications, machine learning models, executive dashboards, and marketing engines. The technical operations (joins and aggregations) serve as the mechanism to fulfill the business outcome.
3. Why the Correct Answer is Correct - Raw transaction rows (e.g. timestamp, SKU, amount) are low-level transactional telemetry. - Joining and aggregating them yields customer-level metrics (e.g., Customer Lifetime Value, loyalty tier eligibility). - This directly represents **converting detailed records into meaningful business insights**. - Therefore, option D is the primary business purpose.
4. Why Each Distractor is Incorrect - **A (Increase partitions):** Partitioning is a physical tuning tactic used to optimize parallelism; increasing partitions is never the business goal of a company. - **B (Create executors):** Executors are ephemeral compute processes allocated by cluster managers (YARN/Kubernetes); creating them is an infrastructure mechanism, not a business goal. - **C (Reduce cluster memory):** Reducing cluster memory is an infrastructure optimization or cost-saving tactic, not the fundamental business objective of an analytics pipeline.
5. Code Example
from pyspark.sql import SparkSession
from pyspark.sql.functions import sum as _sum, count, round as _round
spark = SparkSession.builder.appName("CustomerInsightsETL").getOrCreate()
# 1. Customer demographics (Master Data)
customers = spark.createDataFrame([(1, "Alice", "Premium"), (2, "Bob", "Standard")], ["cust_id", "name", "tier"])
# 2. Transaction logs (Raw Telemetry)
txns = spark.createDataFrame([(1, 150.0), (1, 350.0), (2, 45.0)], ["cust_id", "amount"])
# 3 & 4. Join and Aggregate to produce high-value business insights
customer_insights = (
customers.join(txns, on="cust_id", how="inner")
.groupBy("cust_id", "name", "tier")
.agg(
_round(_sum("amount"), 2).alias("lifetime_spend"),
count("amount").alias("total_orders")
)
)
customer_insights.show()
# +-------+-----+-------+--------------+------------+
# |cust_id| name| tier|lifetime_spend|total_orders|
# +-------+-----+-------+--------------+------------+
# | 1|Alice|Premium| 500.0| 2|
# | 2| Bob|Standard| 45.0| 1|
# +-------+-----+-------+--------------+------------+