CertSafari
    Databricks Certified Data Engineer Associate· Lessons

    Domain 3 · Lesson 10/33

    Reading Bronze Tables and Cleaning Nulls for Silver

    Implement data cleaning

    10 min read
    3.14% of exam
    5 sources
    Published 2 Oct 2026
    Docs as of 30 Sep 2026

    What you will be able to do

    • Explain why bronze data has to be cleaned before it reaches silver tables, and how to read bronze data with PySpark or SQL
    • Drop rows containing nulls with dropna, using how, thresh and subset
    • Fill or replace missing values with fillna, coalesce and a CASE WHEN placeholder rewrite
    • Send invalid records to a quarantine table with WHERE filters, as separate writes

    Key concept

    Silver layer as the cleaning boundary — Bronze tables keep source data raw and barely validated. All the cleanup work (null handling, type casting, quality filtering) happens on the way into silver: you read from bronze and write the cleaned result to a separate silver table.

    1.What bronze hands you, and how to read it

    Cleaning starts with knowing the state of the input. Very little validation happens in bronze. To avoid losing data when a source changes shape, Databricks recommends storing most fields as string, VARIANT, or binary. That means a silver job usually gets text where it needs numbers and dates, plus nulls and placeholder values the source system never resolved. Bronze also isn't meant for analysts. It exists to feed the workloads that build silver.

    Bronze compared with silver
    LayerWhat happens hereTypical state of the data
    BronzeRaw data ingestionMinimal validation; most fields stored as string, VARIANT, or binary
    SilverData cleaning and validationSchema enforcement, handling of null and missing values, type casting, data quality checks

    The pattern is always read, then transform, then write: read from one or more bronze (or silver) tables, and write to silver tables. Databricks advises against writing to silver straight from ingestion, because schema changes and corrupt source records then break the silver load. If the bronze sources are append-only, make most bronze reads streaming reads, and keep batch reads for small datasets such as small dimension tables. In PySpark, spark.read.table loads a table as a DataFrame. In SQL, a plain SELECT ... FROM bronze_table inside an INSERT INTO silver_table statement does the same job, as the SQL examples later in this lesson show.

    Reading a table into a DataFrame in PySparkpython
    spark.read.table("tblA").sort("age").show()

    Checkpoint 1 of 5· Check yourself

    A large bronze table is append-only and feeds a silver table. How does Databricks recommend reading it?

    Sources1

    2.Dropping rows with dropna

    The bluntest fix is to remove incomplete rows. The medallion example does exactly that for customer and transaction data in silver, dropping nulls and quarantining invalid records. In PySpark, DataFrame.dropna and df.na.drop are aliases. Both return a new DataFrame without rows that contain null or NaN values. Here is the sample DataFrame the examples below use:

    Sample DataFrame with nulls and NaNpython
    from pyspark.sql import Row
    df = spark.createDataFrame([
        Row(age=10, height=80.0, name="Alice"),
        Row(age=5, height=float("nan"), name="Bob"),
        Row(age=None, height=None, name="Tom"),
        Row(age=None, height=float("nan"), name=None),
    ])
    dropna parameters
    ParameterDefaultEffect
    how'any''any' drops a row containing any null; 'all' drops a row only if every value is null
    threshNoneDrops rows with fewer than thresh non-null values; overrides how
    subsetNoneLimits the check to the listed column names
    thresh=2 keeps rows with at least two non-null valuespython
    df.na.drop(thresh=2).show()
    # +---+------+-----+
    # |age|height| name|
    # +---+------+-----+
    # | 10|  80.0|Alice|
    # |  5|   NaN|  Bob|
    # +---+------+-----+

    Pay attention to the default. A bare df.na.drop() uses how='any', which leaves only Alice, because Bob's height is NaN. That is usually far more than you meant to drop. Use subset when only certain columns, such as keys, must be populated.

    Checkpoint 2 of 5· Fill the gap

    This call drops only the row whose values are all null or NaN. Which value completes it?

    df.na.drop(how=' ? ').show()
    # +----+------+-----+
    # | age|height| name|
    # +----+------+-----+
    # |  10|  80.0|Alice|
    # |   5|   NaN|  Bob|
    # |NULL|  NULL|  Tom|
    # +----+------+-----+

    Sources21

    3.Filling and replacing missing values

    Dropping a row throws away its good fields as well. Often a default value is the better choice. DataFrame.fillna and df.na.fill are aliases. They accept either one scalar or a dict that maps column names to replacement values. With a scalar, only columns of a matching type are filled. In the docs example, df.na.fill(50) fills the numeric age and height columns and leaves the string name null. With a dict, you set a value per column, and any subset you pass is ignored.

    Per-column defaults with a dictpython
    df.na.fill({'age': 50, 'name': 'unknown'}).show()
    # +---+------+-------+----+
    # |age|height|   name|bool|
    # +---+------+-------+----+
    # | 10|  80.5|  Alice|NULL|
    # |  5|  NULL|    Bob|NULL|
    # | 50|  NULL|    Tom|NULL|
    # | 50|  NULL|unknown|true|
    # +---+------+-------+----+

    Checkpoint 3 of 5· Check yourself

    In the docs example the columns are age (int), height (float), name (string) and bool (boolean). After df.na.fill(50), which columns can still contain nulls?

    In SQL, coalesce returns its first non-null argument, which makes coalesce(col, default) the SQL counterpart of a fill. The result type is the least common type of the arguments. The arguments are evaluated left to right, and evaluation stops at the first non-null value.

    The opposite problem also comes up: a value that should be null but isn't. If an upstream system can't encode NULL, it may write a placeholder such as -1. Instead of making every downstream query filter out -1, convert it back to NULL once, during the bronze-to-silver write:

    Replacing a -1 placeholder with NULL while writing silversql
    INSERT INTO silver_table
      SELECT
        * EXCEPT weight,
        CASE
          WHEN weight = -1 THEN NULL
          ELSE weight
        END AS weight
      FROM bronze_table;

    Checkpoint 4 of 5· Exam question

    A bronze `orders` table has nulls in `email` (expected for guest checkouts) and nulls in `order_total` (caused only by an upstream parsing bug). Which approach correctly cleans this bronze table into a silver table while keeping valid guest rows?

    Sources3452

    4.Quarantining invalid records instead of losing them

    Some rows are invalid rather than incomplete, such as a negative quantity or a timestamp in the future. Use filters and WHERE clauses to keep them out of silver. Write the complementary predicate to a quarantine table, so the rows can still be inspected later. Each destination gets its own INSERT:

    Splitting bronze into silver and quarantine with complementary predicatessql
    DECLARE current_time = now()
    
    INSERT INTO silver_table
      SELECT * FROM bronze_table
      WHERE event_timestamp <= current_time AND quantity >= 0;
    
    INSERT INTO quarantine_table
      SELECT * FROM bronze_table
      WHERE event_timestamp > current_time OR quantity < 0;

    The two writes are deliberately separate. Databricks recommends processing filtered data as a separate write operation, especially with Structured Streaming, because using .foreachBatch to write to several tables can produce inconsistent results. In PySpark the same split is a pair of filter calls, using column predicates such as isNotNull().

    A PySpark null filterpython
    df.filter(df.name.isNotNull()).show()

    Checkpoint 5 of 5· Check yourself

    A streaming bronze-to-silver job must send valid rows to silver and invalid rows to quarantine. What does Databricks recommend?

    Sources4

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 1.df.na.drop() with no arguments removes only rows that are entirely null.Why is that wrong?

      The default is how='any', so a single null or NaN anywhere in a row drops it. Only how='all' limits the drop to fully null rows.

      Covered in Dropping rows with dropna

    2. 2.Passing subset together with a dict to fillna restricts the fill to the subset columns.Why is that wrong?

      When value is a dict, subset is ignored. The dict keys alone decide which columns are filled.

      Covered in Filling and replacing missing values

    Sources

    Every claim above is drawn from one of these pages, quoted as it was written on the date shown.

    1. 1.
      “Databricks recommends storing most fields as string, VARIANT, or binary to protect against unexpected schema changes.”
      ↩︎ What bronze hands you, and how to read it
      “To build the silver layer, read data from one or more bronze or silver tables, and write data to silver tables.”
      ↩︎ What bronze hands you, and how to read it
      “Is intended for consumption by workloads that enrich data for silver tables, not for access by analysts and data scientists.”
      ↩︎ What bronze hands you, and how to read it
      “Batch reads should be reserved for small datasets (for example, small dimensional tables).”
      ↩︎ What bronze hands you, and how to read it
      “Data about customers and transactions is cleaned by dropping nulls and quarantining invalid records.”
      ↩︎ Dropping rows with dropna
      “Data cleanup and validation are performed in silver layer.”
      ↩︎ Key concept
      “Assuming all sources are append-only, configure most reads from bronze as streaming reads.”
      ↩︎ Checkpoint
    2. 2.
      “Returns a new DataFrame omitting rows with null or NaN values.”
      ↩︎ Dropping rows with dropna
      “optional list of column names to consider.”
      ↩︎ Filling and replacing missing values
      “If 'any', drop a row if it contains any nulls. If 'all', drop a row only if all its values are null.”
      ↩︎ Exam trap 1
      “If specified, drop rows that have less than thresh non-null values. This overwrites the how parameter.”
      ↩︎ Prediction
    3. 3.
      “coalesce evaluates arguments left to right until a non-null value is found. If all arguments are NULL, the result is NULL.”
      ↩︎ Filling and replacing missing values
    4. 4.
      “the placeholder value -1 is used to represent missing data”
      ↩︎ Filling and replacing missing values
      “CASE WHEN ... OTHERWISE clauses allow you to define conditional logic to gracefully apply business logic”
      ↩︎ Filling and replacing missing values
      “You can use filters and WHERE clauses to define custom logic that quarantines bad records and prevents them from propagating to downstream tables.”
      ↩︎ Quarantining invalid records instead of losing them
      “Using .foreachBatch to write to multiple tables can lead to inconsistent results.”
      ↩︎ Quarantining invalid records instead of losing them
      “Databricks recommends always processing filtered data as a separate write operation, especially when using Structured Streaming.”
      ↩︎ Checkpoint
    5. 5.
      “Columns specified in subset that do not have matching data types are ignored.”
      ↩︎ Filling and replacing missing values
      “If the value is a dict, then subset is ignored and value must be a mapping from column name (string) to replacement value.”
      ↩︎ Exam trap 2

    Continue to page 2 of 2

    Standardizing Data Types and Writing Silver Tables

    Spotted a mistake, or was something unclear? Tell us.