Home / DE Professional practice test / Developing Code for Data Processing using Python and SQL

Free · 8 questions with explanations

Developing Code for Data Processing using Python and SQL: Databricks Data Engineer Professional Practice Questions

Exam-style questions on Developing Code for Data Processing using Python and SQL. Pick your answer, then open the explanation to see why it's right — and why the other options are wrong.

1 Developing Code for Data Processing using Python and SQL

A data engineer is implementing a Pandas UDF that computes a rolling 7-day weighted moving average of revenue for each store_id partition. The function requires access to multiple rows within a group simultaneously. The engineer writes the following code: ``python from pyspark.sql.functions import pandas_udf from pyspark.sql import Window import pandas as pd @pandas_udf('double') def weighted_moving_avg(revenue: pd.Series) -> pd.Series: return revenue.rolling(window=7, min_periods=1).mean() result_df = df.withColumn( 'wma_revenue', weighted_moving_avg(col('revenue')).over( Window.partitionBy('store_id').orderBy('sale_date') ) ) ` When this code runs, it raises: AnalysisException: Pandas UDF does not support 'over' with a Window spec`. What is the correct approach to implement this grouped rolling calculation using Pandas UDFs?

  1. AUse a Pandas UDF of type GROUPED_MAP (decorated with @pandas_udf(schema, PandasUDFType.GROUPED_MAP) or the modern applyInPandas pattern). Apply it using df.groupBy('store_id').applyInPandas(weighted_moving_avg_fn, schema=result_schema) where the function receives a full Pandas DataFrame for each store_id group and returns a Pandas DataFrame. This allows applying rolling window logic within each partition using native Pandas operations.
  2. BChange the UDF decorator to @pandas_udf('double', PandasUDFType.SCALAR_ITER). Iterator-based Pandas UDFs support window operations because they receive an iterator of batches that correspond to window frames, allowing the rolling computation to be applied across each window's rows.
  3. CThe @pandas_udf('double') decorator is correct, but the .over(Window.partitionBy(...)) call must be replaced with .over(Window.partitionBy('store_id').orderBy('sale_date').rowsBetween(-6, 0)) to specify an explicit 7-row frame. Scalar Pandas UDFs support window frames when the frame bounds are explicitly specified using rowsBetween or rangeBetween.
  4. DThe solution is to convert the Pandas UDF into a standard Python UDF with @udf('double'). Standard Python UDFs support .over() window expressions because they operate row-by-row and do not require the batching constraints that cause Pandas UDFs to be incompatible with Spark window operations.
  5. EPandas UDFs cannot be used for grouped window operations in PySpark. The only correct approach is to collect the DataFrame to the driver with toPandas(), apply the grouped rolling window logic using df.groupby('store_id').apply(lambda g: g.assign(wma=g['revenue'].rolling(7).mean())), and then convert the result back to a Spark DataFrame with spark.createDataFrame().
Show answer & explanation

Correct answer: A

WHY A: The correct pattern for applying group-level Pandas operations (like rolling windows per partition) is applyInPandas (the modern equivalent of GROUPED_MAP Pandas UDF). The function receives the entire Pandas DataFrame for each store_id group, allowing full Pandas rolling/window operations within the group, and returns a Pandas DataFrame. This is the idiomatic PySpark pattern for partition-level operations that require access to multiple rows simultaneously. WHY NOT B: SCALAR_ITER UDFs receive an iterator of batches for memory efficiency with scalar transformations — they do not correspond to partition groups and do not enable .over() window operations. WHY NOT C: Scalar Pandas UDFs (SCALAR type) do not support .over() window expressions, regardless of the frame specification. The AnalysisException is not resolved by changing the frame bounds. WHY NOT D: Standard Python UDFs also do not support .over() window expressions with grouping operations that require cross-row access. A standard row-at-a-time UDF cannot compute a rolling average because it receives only one row at a time. WHY NOT E: Collecting 500 million+ rows to the driver via toPandas() is an OOM anti-pattern. applyInPandas achieves the same result distributedly without moving data to the driver.

2 Developing Code for Data Processing using Python and SQL

A data engineer is writing unit tests for a PySpark transformation function apply_revenue_adjustments(df: DataFrame) -> DataFrame that performs type casting, a column computation, and a filter. The engineer wants to test the function locally without connecting to a Databricks cluster, using pytest. The test should verify both the schema of the output DataFrame and the correctness of specific row values. The engineer also wants to use DataFrame.transform() for composing multiple transformation functions in a testable pipeline chain. Which test implementation correctly uses assertSchemaEqual, assertDataFrameEqual, and DataFrame.transform()?

  1. A``python def test_revenue_adjustments(spark): input_df = spark.createDataFrame( [(1, '100.5', 'active'), (2, '200.0', 'inactive')], ['id', 'revenue_str', 'status'] ) result = input_df.transform(apply_revenue_adjustments) expected_schema = StructType([ StructField('id', LongType(), True), StructField('revenue', DoubleType(), True), StructField('status', StringType(), True) ]) assertSchemaEqual(result.schema, expected_schema) expected_df = spark.createDataFrame([(1, 100.5, 'active')], ['id', 'revenue', 'status']) assertDataFrameEqual(result, expected_df) ``
  2. B``python def test_revenue_adjustments(spark): input_df = spark.createDataFrame( [(1, '100.5', 'active'), (2, '200.0', 'inactive')], ['id', 'revenue_str', 'status'] ) result = apply_revenue_adjustments(input_df) assert result.schema == expected_schema assert result.count() == 1 assert result.first()['revenue'] == 100.5 ``
  3. C``python @pytest.mark.parametrize('input_val,expected', [('100.5', 100.5), ('200.0', 200.0)]) def test_revenue_adjustments(spark, input_val, expected): df = spark.createDataFrame([(1, input_val, 'active')], ['id', 'revenue_str', 'status']) result = df.transform(apply_revenue_adjustments) actual = result.select('revenue').first()[0] assert actual == expected ``
  4. D``python def test_revenue_adjustments(): input_data = [{'id': 1, 'revenue_str': '100.5', 'status': 'active'}, {'id': 2, 'revenue_str': '200.0', 'status': 'inactive'}] result = apply_revenue_adjustments(input_data) assert result[0]['revenue'] == 100.5 assert len([r for r in result if r['status'] == 'active']) == 1 ``
  5. E``python def test_revenue_adjustments(spark): input_df = spark.createDataFrame( [(1, '100.5', 'active'), (2, '200.0', 'inactive')], ['id', 'revenue_str', 'status'] ) result = input_df.transform(apply_revenue_adjustments) expected_df = spark.createDataFrame([(1, 100.5, 'active')], ['id', 'revenue', 'status']) assertDataFrameEqual(result, expected_df, checkRowOrder=False) assertSchemaEqual(result.schema, expected_df.schema, ignoreNullable=True) ``
Show answer & explanation

Correct answer: E

WHY E: This is the most complete and correct test using all three specified APIs. df.transform(apply_revenue_adjustments) uses DataFrame.transform() to apply the function in a composable, chain-friendly way. assertDataFrameEqual(result, expected_df, checkRowOrder=False) uses the PySpark testing API to compare DataFrames with configurable row-order sensitivity — essential for non-deterministic query execution. assertSchemaEqual(result.schema, expected_df.schema, ignoreNullable=True) compares schemas while tolerating nullability differences between inferred and explicitly defined schemas. Both assertDataFrameEqual and assertSchemaEqual are from pyspark.testing and provide detailed failure messages on mismatch. WHY NOT A: This test uses assertSchemaEqual and assertDataFrameEqual correctly, but the expected_df has the same number of rows as the result (only 1 row with active status after filtering) — the test logic appears correct but expected_schema is not defined in the test scope shown, making it incomplete. Option E is more complete and correct. WHY NOT B: Using assert result.schema == expected_schema (Python equality) and result.first()['revenue'] are not the idiomatic PySpark testing API. They lack the detailed error messages of assertDataFrameEqual/assertSchemaEqual and don't handle nullable field differences or row ordering. WHY NOT D: apply_revenue_adjustments accepts a DataFrame, not a Python list. This test would fail with a TypeError before any assertions. Unit tests for PySpark functions require a SparkSession fixture. WHY NOT C: While parametrize is a valid pytest pattern and DataFrame.transform() is used correctly, this test only checks a single column value per parametrized case and does not use assertDataFrameEqual or assertSchemaEqual as required by the objective.

3 Developing Code for Data Processing using Python and SQL

A data engineering team is configuring a production Databricks Job with the following requirements: (1) the main transformation notebook task requires 64 GB of memory per node due to large in-memory joins; (2) the pipeline must not retry on failure — a failed run should immediately alert the on-call engineer without any automatic re-execution; (3) the job runs in a production environment where the cluster must use Photon-enabled Databricks Runtime and memory-optimized instance types. Which combination of job and cluster configuration settings correctly satisfies all three requirements in a Databricks Asset Bundle YAML?

  1. ASet max_retries: -1 in the task configuration to disable all retries. Select a Standard_E64s_v3 (Azure) or r5.2xlarge (AWS) memory-optimized instance type with 64 GB RAM. Set spark_version to a Photon-enabled DBR (e.g., 14.3.x-photon-scala2.12). Use timeout_seconds: 0 at the job level to remove any execution time limit that could interfere with long-running memory-intensive joins.
  2. BSet retry_on_timeout: false and max_retries: 1 in the task configuration. The retry_on_timeout: false flag is the correct setting to prevent retries; max_retries: 1 is required as a minimum value because Databricks does not allow max_retries: 0 for production jobs. Select a memory-optimized instance type and set the Spark config spark.executor.memory=64g to explicitly allocate 64 GB per executor.
  3. CSet max_retries: 0 in the task configuration to disable automatic retries. Select a memory-optimized instance type for the job cluster worker nodes (e.g., Standard_E64s_v3 on Azure with 432 GB RAM, or r5.16xlarge on AWS with 512 GB RAM). Set spark_version to a Photon-enabled DBR. Configure a job-level email or webhook notification for the on_failure event to alert on-call engineers immediately when the job fails.
  4. DSet max_retries: 0 at the job level (not the task level) to apply a zero-retry policy across all tasks. The job-level retry setting overrides any task-level retry configuration. Memory requirements are satisfied by setting spark.driver.memory=64g in the cluster Spark config, which automatically sizes worker memory to match. Photon is enabled by adding photon: true as a top-level cluster property.
  5. ESet on_failure: skip_remaining_tasks in the task configuration to prevent downstream tasks from running after a failure, which effectively prevents retries. Set spark.memory.fraction=0.9 to maximize the memory available for joins. Select the largest available instance type and rely on Spark's adaptive memory management to allocate the required 64 GB for joins dynamically.
Show answer & explanation

Correct answer: C

WHY C: max_retries: 0 is the correct task-level configuration to prevent any automatic retry — the task fails immediately on the first error without re-execution. Memory-optimized instance types (e.g., r5.16xlarge on AWS or Standard_E64s_v3 on Azure) provide the high memory-per-core ratios needed for large in-memory joins — selecting these at the cluster worker level (not via Spark config) is the correct approach. A Photon-enabled spark_version string (e.g., 14.3.x-photon-scala2.12) enables Photon. Job-level on_failure notifications alert on-call engineers via email or webhook. WHY NOT A: max_retries: -1 means unlimited retries in Databricks Jobs — the opposite of the requirement. timeout_seconds: 0 means no timeout (the job runs indefinitely), which is unrelated to the retry requirement. WHY NOT B: max_retries: 1 allows one retry, violating the no-retry requirement. spark.executor.memory=64g sets Spark's heap size but does not guarantee the underlying instance has 64 GB — instance selection is what ensures physical memory availability. WHY NOT D: Job-level retry settings do not override task-level settings in Databricks Jobs; retry configuration is typically set at the task level. spark.driver.memory sets driver memory, not worker memory. Worker memory is determined by instance type selection. WHY NOT E: on_failure: skip_remaining_tasks controls downstream task execution, not retries of the failed task itself. spark.memory.fraction tunes Spark's internal memory allocation ratio but cannot create physical memory that doesn't exist on the instance.

4 Developing Code for Data Processing using Python and SQL

A data engineering team is implementing a CDC pipeline using APPLY CHANGES INTO in Lakeflow SDP. The source CDC stream sometimes delivers events out of order — a DELETE event for a customer can arrive before the INSERT event for the same customer due to upstream replication lag. The business requirement is that the Silver table always reflects the last known state based on event timestamp, even when events arrive out of order. The team also wants to maintain a full historical audit trail showing all versions of each customer record, not just the current state. Which APPLY CHANGES configuration correctly satisfies both requirements?

  1. AAPPLY CHANGES INTO silver_customers FROM STREAM(bronze_cdc) KEYS (customer_id) APPLY AS DELETE WHEN operation = 'DELETE' SEQUENCE BY event_ts; — The SEQUENCE BY event_ts clause automatically handles out-of-order events by buffering all events and sorting by timestamp before applying them, regardless of arrival order.
  2. BAPPLY CHANGES INTO silver_customers FROM STREAM(bronze_cdc) KEYS (customer_id) APPLY AS DELETE WHEN operation = 'DELETE' SEQUENCE BY arrival_timestamp COLUMNS * EXCEPT (operation); — Using arrival_timestamp (the Kafka message timestamp or Auto Loader file modification time) instead of the business event timestamp ensures events are always applied in the order they were received, regardless of out-of-order delays in the business event timestamp.
  3. CAPPLY CHANGES INTO silver_customers FROM STREAM(bronze_cdc) KEYS (customer_id) APPLY AS DELETE WHEN operation = 'DELETE' SEQUENCE BY event_ts STORED AS SCD TYPE 1 IGNORE NULL UPDATES COLUMNS * EXCEPT (operation); — IGNORE NULL UPDATES prevents out-of-order NULL-valued events from overwriting valid current-state data, and SCD TYPE 1 overwrites older versions while keeping only the current state.
  4. DAPPLY CHANGES INTO silver_customers_history FROM STREAM(bronze_cdc) KEYS (customer_id) APPLY AS DELETE WHEN operation = 'DELETE' SEQUENCE BY event_ts STORED AS SCD TYPE 2 COLUMNS * EXCEPT (operation); — SCD Type 2 mode maintains a full historical record of all changes by inserting new rows for each version (with __START_AT and __END_AT columns) rather than overwriting the current row, satisfying the audit trail requirement. SEQUENCE BY event_ts ensures out-of-order events are applied in the correct logical order.
  5. EAPPLY CHANGES INTO silver_customers FROM STREAM(bronze_cdc) KEYS (customer_id) APPLY AS DELETE WHEN operation = 'DELETE' SEQUENCE BY event_ts STORED AS SCD TYPE 3 COLUMNS * EXCEPT (operation); — SCD Type 3 stores both the current and previous value of each column in side-by-side column pairs (e.g., current_email, previous_email), providing a full audit trail of all changes in a single row without requiring additional join operations.
Show answer & explanation

Correct answer: D

WHY D: This is the correct configuration for both requirements. STORED AS SCD TYPE 2 is the Lakeflow SDP mechanism for maintaining a full history — each change creates a new row with __START_AT and __END_AT columns tracking validity periods, and DELETE events close the current row by setting __END_AT. SEQUENCE BY event_ts uses the business event timestamp to determine ordering, so even out-of-order arrival events are applied to the history in the correct logical sequence. This satisfies both the 'latest state reflects actual event order' and 'full historical audit trail' requirements. WHY NOT A: SCD Type 1 (default) only keeps the current state — it overwrites previous values, so there is no audit trail of all versions. While SEQUENCE BY event_ts handles ordering correctly, the audit trail requirement is not satisfied. WHY NOT C: IGNORE NULL UPDATES prevents null-valued update events from overwriting non-null current values — this is a valid option for data quality but does not provide a full audit trail. SCD TYPE 1 still overwrites history. WHY NOT B: Using arrival_timestamp defeats the purpose of SEQUENCE BY for out-of-order handling — arrival order does not equal event order, so a DELETE arriving before an INSERT would still be applied incorrectly if ordered by arrival time. WHY NOT E: SCD Type 3 is not supported in Lakeflow SDP's APPLY CHANGES INTO syntax. SCD Type 3 (current + previous value columns) is not a valid option for this API — only SCD Type 1 (current state) and SCD Type 2 (full history) are supported.

5 Developing Code for Data Processing using Python and SQL

A data engineering team is deploying a Databricks Job with a Lakeflow Spark Declarative Pipeline task. The pipeline runs in TRIGGERED mode and is expected to fully process all pending data within 30 minutes under normal conditions. The business SLA requires the pipeline to complete within 45 minutes. The team wants the pipeline to automatically stop and alert if it has not completed within 45 minutes, and they want the pipeline cluster to automatically terminate after completion to avoid idle cluster costs. The team is also aware of a known upstream data quality issue that occasionally causes transient failures, and they want a maximum of 2 automatic retries before the alert is raised. Which DAB job configuration correctly implements all three requirements?

  1. ASet timeout_seconds: 2700 (45 minutes) at the pipeline task level to stop the pipeline if it exceeds the SLA. Set max_retries: 2 at the task level to allow up to 2 retries. Pipeline clusters auto-terminate when the job finishes by default — no additional configuration is needed for cluster termination.
  2. BSet timeout_seconds: 2700 at the job level (not task level) so the entire job run is terminated if it exceeds 45 minutes total. Set max_retries: 2 at the job level to allow retries of the entire job. Pipeline clusters auto-terminate when the TRIGGERED pipeline completes. Configure a job-level on_failure notification to alert when the job fails after exhausting retries.
  3. CSet timeout_seconds: 1800 (30 minutes, matching the expected runtime) to ensure the pipeline is strictly bounded to its normal execution time. Set max_retries: 2 at the task level. Add autotermination_minutes: 10 to the pipeline cluster configuration to terminate the cluster 10 minutes after the pipeline completes, providing a buffer for any post-processing cleanup tasks.
  4. DSet timeout_seconds: 2700 at the task level. Set max_retries: 2 with min_retry_interval_millis: 30000 (30 seconds between retries) to avoid immediately hammering the transient failure source. Configure an on_failure job notification. Pipeline clusters (in TRIGGERED mode) auto-terminate when the pipeline run completes, so no explicit auto-termination config is needed.
  5. EConfigure the pipeline in CONTINUOUS mode with a timeout_seconds: 2700 at the task level. CONTINUOUS mode automatically retries internally on transient failures without the job needing to retry the task. Set max_retries: 0 at the task level since CONTINUOUS mode handles its own resilience, and configure the on_failure notification at the job level.
Show answer & explanation

Correct answer: D

WHY D: timeout_seconds: 2700 at the task level is the correct SLA enforcement — the pipeline task is terminated if it does not complete within 45 minutes. max_retries: 2 allows up to two automatic retries for transient failures before the job is considered permanently failed. min_retry_interval_millis adds a configurable delay between retries, which is a best practice for transient upstream issues (avoids immediately retrying a still-failing source). Pipeline clusters in TRIGGERED mode auto-terminate when the pipeline run completes — this is a built-in behavior of TRIGGERED mode job clusters, so no explicit termination config is needed. on_failure notification triggers after all retries are exhausted. WHY NOT A: While functionally similar, the task-level timeout without a retry interval is less correct than C because immediate retries on transient issues may repeatedly hit the same failure. Option D is more complete and operationally sound. WHY NOT B: Setting timeout and retries at the job level (rather than task level) is less precise for multi-task jobs, as the timeout would apply to the entire job's wall-clock time including scheduling overhead. WHY NOT C: Setting timeout_seconds: 1800 (the expected runtime) as the timeout means any run that takes slightly longer than normal (but still within SLA) would be killed unnecessarily. The SLA is 45 minutes; the timeout should match the SLA, not the expected runtime. WHY NOT E: CONTINUOUS mode is for ongoing, low-latency streaming. Using it for a 30-minute TRIGGERED batch pipeline is inappropriate and would keep the cluster running indefinitely rather than terminating after each batch.

6 Developing Code for Data Processing using Python and SQL

A data engineering team is operating a production Lakeflow Spark Declarative Pipeline in TRIGGERED mode that processes financial transaction data from Auto Loader. After a routine deployment, the pipeline fails with the following error in the Event Log: SCHEMA_MISMATCH: Schema of data does not match schema of streaming table: expected 'amount DECIMAL(18,2)' but got 'amount DOUBLE'. The upstream source team has confirmed they changed the amount field type from DECIMAL(18,2) to DOUBLE in a recent data export. The pipeline must resume processing as quickly as possible without data loss. What is the CORRECT remediation sequence?

  1. AIn the Lakeflow SDP pipeline settings, change cloudFiles.schemaEvolutionMode from none to addNewColumns. Restart the pipeline. The addNewColumns mode automatically handles type changes (not just new columns) by widening the schema to accommodate both type representations.
  2. BUpdate the pipeline's streaming table DDL to change the amount column type from DECIMAL(18,2) to DOUBLE to match the new source schema. Perform a full pipeline reset (clicking 'Full Refresh' in the UI or running databricks pipelines start --full-refresh) to rebuild the streaming table from scratch with the new schema. Resume normal TRIGGERED mode operation.
  3. CRun ALTER TABLE silver_transactions ALTER COLUMN amount TYPE DOUBLE on the pipeline's output Delta table directly via a Databricks SQL notebook. Then restart the pipeline without a full reset. The schema change applied directly to the Delta table will resolve the mismatch without requiring a full data reprocessing.
  4. DUpdate cloudFiles.schemaEvolutionMode to rescue in the pipeline configuration. In rescue mode, records with the new DOUBLE type for amount will be stored in the _rescued_data column as JSON strings instead of failing. The pipeline resumes immediately and rescued records can be re-processed once the permanent schema fix is deployed.
  5. EDelete the pipeline's checkpoint directory in DBFS and restart the pipeline. Deleting the checkpoint forces Auto Loader to re-scan all files from the beginning and re-infer the new schema from the latest data. The pipeline will rebuild the streaming table with the correct DOUBLE type automatically.
Show answer & explanation

Correct answer: B

WHY B: A SCHEMA_MISMATCH error where an existing column's data type has changed (not just a new column added) requires a schema-breaking change to the streaming table definition. The correct procedure is: (1) update the streaming table's DDL to reflect the new type, (2) perform a Full Refresh (pipeline reset) which truncates and rebuilds the target table from the checkpoint — ensuring the new schema is applied consistently from the beginning. This is the official Lakeflow SDP procedure for type-changing schema evolution. WHY NOT A: addNewColumns handles the addition of new fields only. It does not handle type changes to existing columns. A DECIMAL-to-DOUBLE type change is not resolved by addNewColumns. WHY NOT C: Altering the Delta table schema directly via ALTER TABLE outside the pipeline can cause the Lakeflow SDP engine to detect a schema conflict between the pipeline definition and the physical table schema, potentially causing further errors. The pipeline definition and the target table schema must be updated in a coordinated way via a Full Refresh. WHY NOT D: rescue mode captures records that don't match the current schema into _rescued_data. The amount field would land as a JSON string in _rescued_data rather than being processed correctly, and rescued records require manual reprocessing — this is not a quick resume strategy. WHY NOT E: Manually deleting the checkpoint directory corrupts the pipeline's incremental state. Databricks manages checkpoints as part of the pipeline metadata. A Full Refresh through the pipeline UI/API is the correct supported method to reset the pipeline state.

7 Developing Code for Data Processing using Python and SQL

A data engineering team needs to apply a complex data masking transformation to a pii_value STRING column in a large PySpark DataFrame. The transformation logic is implemented in a Python function mask_pii(value: str) -> str that calls an external encryption library. The team considers four implementation approaches. The DataFrame has 500 million rows. Which implementation approach offers the BEST performance for this use case, and why?

  1. ARegister the function as a standard Python UDF using @udf(returnType=StringType()) and apply it with df.withColumn('masked', mask_pii_udf(col('pii_value'))). Python UDFs are the simplest to implement and Databricks automatically parallelizes them across all executor cores, achieving performance equivalent to native Spark built-in functions for single-column string transformations.
  2. BRegister the function as a vectorized UDF using @pandas_udf and apply it with spark.sql() after registering it to the catalog with spark.udf.register('mask_pii', mask_pii_pandas_udf). SQL-invoked UDFs have lower overhead than programmatic DataFrame API UDFs because the SQL query planner can push down the UDF execution to the storage layer, avoiding a full shuffle.
  3. CConvert the PySpark DataFrame to a Pandas DataFrame using df.toPandas(), apply the masking function using pandas_df['masked'] = pandas_df['pii_value'].apply(mask_pii), then convert back to a PySpark DataFrame using spark.createDataFrame(pandas_df). This approach leverages Pandas' optimized C-backed string operations, which are faster than Spark's JVM string processing for single-column transformations.
  4. DImplement the masking logic as a Spark SQL expression using regexp_replace() and encode()/decode() built-in functions, and apply it via df.withColumn('masked', regexp_replace(col('pii_value'), pattern, replacement)). Built-in Spark SQL functions always outperform any UDF approach because they execute natively in the JVM without any Python process involvement.
  5. ERegister the function as a Pandas UDF (vectorized UDF) using @pandas_udf(returnType=StringType()) with the PandasUDFType.SCALAR type (or the modern function annotation approach). Apply it with df.withColumn('masked', mask_pii_pandas_udf(col('pii_value'))). Pandas UDFs use Apache Arrow for serialization, processing data in batches as Pandas Series instead of row-by-row, significantly reducing Python-JVM serialization overhead compared to standard row-at-a-time Python UDFs.
Show answer & explanation

Correct answer: E

WHY E: Pandas UDFs (vectorized UDFs) are the correct high-performance choice when custom Python logic cannot be expressed as native Spark functions. They use Apache Arrow for zero-copy data transfer between the JVM and Python process, serialize data in columnar batches (Pandas Series) instead of row by row, and dramatically reduce the per-row Python overhead of standard Python UDFs. For 500 million rows, this batching can be 10-100x faster than a standard row-at-a-time Python UDF. WHY NOT A: Standard Python UDFs serialize each row individually between the JVM and Python, incurring significant per-row overhead. They are the slowest UDF option for large DataFrames and are not equivalent to native Spark functions in performance. WHY NOT C: toPandas() collects the entire 500 million row DataFrame to the driver node's memory, which would almost certainly cause an OOM error and eliminates all distributed processing benefits. This is an anti-pattern for large DataFrames. WHY NOT D: Built-in Spark SQL functions are the fastest option when they can express the logic, but a complex external-library encryption call cannot be expressed as a regexp_replace() pattern. The scenario requires custom Python logic via a UDF. WHY NOT B: SQL-invoked UDFs do not get pushed down to the storage layer. UDF execution happens at the executor level, not at the storage layer, regardless of how they are invoked (SQL vs DataFrame API).

8 Developing Code for Data Processing using Python and SQL

A data engineering team is building a test suite for their production pipeline. A junior engineer proposes using the Databricks notebook's built-in debugger to step through a failing PySpark transformation during interactive development. A senior engineer also wants to ensure the team can run integration tests that validate the full pipeline's end-to-end output against a known-good fixture dataset stored in a Delta table, without requiring a separate test Databricks cluster. Which combination of testing and debugging strategies is MOST appropriate for a production-grade Databricks project?

  1. AUse the Databricks notebook debugger for step-through debugging during development, and write integration tests that load the known-good fixture Delta table using spark.read.format('delta').load(fixture_path), apply the full pipeline transformation chain using DataFrame.transform() composition, and then call assertDataFrameEqual(result, expected, checkRowOrder=False) to validate correctness. These integration tests can run in a Databricks job or locally using pyspark with a local SparkSession pointing to DBFS/Volume paths.
  2. BUse only the Spark UI's 'SQL / DataFrame' tab to debug failing transformations by examining the physical plan and stage metrics. For integration tests, deploy the pipeline to a dedicated staging Databricks workspace and compare the output Delta table against the production Delta table using EXCEPT SQL queries to identify discrepancies. This environment parity approach is more reliable than local testing for production pipelines.
  3. CUse display() calls and notebook cell outputs as the primary debugging mechanism, inserting them at each transformation step to inspect intermediate DataFrames. For integration tests, use spark.sql('SELECT COUNT(*) FROM gold_table') count assertions as the primary correctness check, because full DataFrame comparisons using assertDataFrameEqual are too computationally expensive for production-scale datasets.
  4. DUse the Databricks notebook built-in debugger (accessible via the 'Enable Debug' button in the notebook toolbar) to set breakpoints and step through PySpark transformation code line-by-line during development. For integration tests, use assertDataFrameEqual in a pytest test that creates a SparkSession via SparkSession.builder.master('local[*]').getOrCreate() and compares the pipeline output against a small locally-defined fixture DataFrame. Run these tests in CI using pytest without requiring a live Databricks workspace.
  5. EUse Python's standard pdb debugger with import pdb; pdb.set_trace() in notebook cells for step-through debugging of PySpark code. For integration tests, mock all Spark DataFrame operations using unittest.mock.patch to avoid the overhead of actually executing Spark plans, and assert that the correct transformation methods were called with the correct arguments.
Show answer & explanation

Correct answer: A

WHY A: This combines two correct and complementary strategies. The Databricks notebook built-in debugger provides visual step-through debugging for development — it is a first-class feature of the Databricks notebook environment. For integration tests, loading the fixture from a Delta table, applying the full transformation chain via DataFrame.transform() composition, and asserting correctness with assertDataFrameEqual(checkRowOrder=False) is the production-grade pattern: it validates actual end-to-end behavior, uses the official PySpark testing API for reliable schema and data comparison, and is composable and maintainable. WHY NOT D: While option D is partially correct (local SparkSession + pytest is valid for unit tests), integration tests against a locally-defined fixture DataFrame may miss real-world data issues that a Delta table fixture would catch. Option A's use of DataFrame.transform() composition is also more explicit. WHY NOT B: While staging environment testing is valuable, relying only on EXCEPT SQL queries between staging and production tables is not a unit/integration testing strategy — it is a post-deployment comparison. It also doesn't use assertDataFrameEqual. WHY NOT C: display() is for ad-hoc interactive inspection, not systematic testing. Count assertions are insufficient for correctness validation — they miss wrong values, type errors, and incorrect row contents. WHY NOT E: pdb.set_trace() in a notebook cell does not work well with Spark's distributed execution model — PySpark code executes on executors, not on the driver where pdb is set. Mocking Spark operations with unittest.mock tests that methods were called, not that the data transformations produce correct results.

Take the full DE Professional practice test →