Skip to content
Good engineers know 4 min read · Delta, Iceberg 2 practice problems ↓

Constraints and generated columns

NOT NULL and CHECK constraints, generated columns and identity columns in Delta tables.

You will learn

  • How NOT NULL and CHECK constraints protect a Delta table
  • What generated columns compute and how they help partition pruning
  • How identity columns create surrogate keys
  • What Iceberg offers instead

Read first

Comfortable with these? Read on.

TL;DR Schema enforcement checks column names and types; constraints check the values. A Delta table can declare NOT NULL and CHECK (amount >= 0), and any write that violates them fails as a whole, so bad data never lands. Generated columns compute values (such as a date from a timestamp) automatically, and identity columns hand out unique ids.

Why constraints

A pipeline that writes a negative amount or a null customer id produces wrong numbers for every report built on it. Validating in each job is easy to forget. A constraint lives with the table, so every writer (Spark, SQL, streaming, another team) is checked the same way, and the failing write is rejected atomically: none of its rows are committed.

NOT NULL and CHECK

PySparkSpark SQL · Declaring constraints
spark.sql("""
    CREATE TABLE sales.orders (
      order_id    BIGINT NOT NULL,
      customer_id BIGINT NOT NULL,
      amount      DECIMAL(12,2),
      status      STRING,
      order_ts    TIMESTAMP
    ) USING delta
""")

spark.sql("ALTER TABLE sales.orders ADD CONSTRAINT amount_non_negative CHECK (amount >= 0)")
spark.sql("""
    ALTER TABLE sales.orders ADD CONSTRAINT valid_status
      CHECK (status IN ('placed', 'shipped', 'delivered', 'cancelled'))
""")
CREATE TABLE sales.orders (
  order_id    BIGINT NOT NULL,
  customer_id BIGINT NOT NULL,
  amount      DECIMAL(12,2),
  status      STRING,
  order_ts    TIMESTAMP
) USING delta;

ALTER TABLE sales.orders ADD CONSTRAINT amount_non_negative CHECK (amount >= 0);
ALTER TABLE sales.orders ADD CONSTRAINT valid_status
  CHECK (status IN ('placed', 'shipped', 'delivered', 'cancelled'));

Step 1 · the batch

A batch of 10,000 orders arrives; one has amount = -50.

Step 2 · the check

Delta evaluates every constraint on the rows being written, as part of the write job.

Step 3 · rejected

The write fails with DELTA_VIOLATE_CONSTRAINT_WITH_VALUES, naming the constraint and the bad value. Nothing is committed: the other 9,999 rows are not written either.

Step 4 · handle it

Split the batch yourself first: write rows that pass the same condition, and send the rest to a quarantine table for review. The constraint stays as the last line of defence.
  • Adding a CHECK constraint to an existing table validates all existing rows first, and fails if any violate it.
  • Constraints are stored in the table properties and in the transaction logtransaction log: The ordered list of commits that defines which files make up a Delta table at each version. Learn more → protocol: old readers that do not understand them may be blocked from writing.
  • ALTER TABLE ... DROP CONSTRAINT name removes one.
  • Delta does not enforce primary or foreign keys; on Databricks they are informational only. Uniqueness must be ensured by MERGE logic.

Generated columns

A generated column is computed from other columns on every write. You cannot write a different value into it.

PySparkSpark SQL · Partition by a date derived from a timestamp
from delta.tables import DeltaTable
(DeltaTable.create(spark).tableName("events")
    .addColumn("event_id", "BIGINT")
    .addColumn("event_ts", "TIMESTAMP")
    .addColumn("event_date", "DATE", generatedAlwaysAs="CAST(event_ts AS DATE)")
    .partitionedBy("event_date")
    .execute())
CREATE TABLE events (
  event_id   BIGINT,
  event_ts   TIMESTAMP,
  event_date DATE GENERATED ALWAYS AS (CAST(event_ts AS DATE))
) USING delta
PARTITIONED BY (event_date);

The bonus: a query that filters only on event_ts still gets partition pruning, because Delta knows how event_date is derived and adds the matching partition filter. Writers no longer need to remember to fill event_date correctly.

Identity columns

PySparkSpark SQL · Surrogate keys
spark.sql("""
    CREATE TABLE dim_customer (
      customer_sk BIGINT GENERATED ALWAYS AS IDENTITY,
      customer_id STRING,
      name        STRING
    ) USING delta
""")
CREATE TABLE dim_customer (
  customer_sk BIGINT GENERATED ALWAYS AS IDENTITY,
  customer_id STRING,
  name        STRING
) USING delta;

Values are unique and increasing, but not consecutive: each writer tasktask: The work for one partition in one stage, run on one CPU core. Learn more → reserves its own range, so gaps are normal. Identity columns make concurrent writes harder (writers must coordinate the high-water mark), and Delta disables some concurrent-write optimisations for such tables.

In Iceberg

Iceberg tables support required (NOT NULL) fields and, in format v3, default values; CHECK constraints are not part of the spec, so value checks live in the pipeline or engine. Iceberg's hidden partitioning gives the benefit of generated partition columns without any extra column: partition by days(event_ts) and filters on event_ts prune automatically.

Common mistakes

Relying on constraints as the only validation

One bad row fails the whole write. Filter and quarantine first; let the constraint catch what slips through.

Expecting primary keys to be enforced

Delta does not enforce uniqueness. Use MERGE and dedup.

Expecting identity values without gaps

They are unique, not consecutive.

Adding a CHECK to a table with existing bad rows

The ALTER fails; clean the data first.

Key takeaways

  • Constraints validate values on every write; violations fail the whole write.
  • Primary and foreign keys are not enforced.
  • Generated columns derive values and enable pruning on the source column.
  • Identity columns are unique but have gaps.

Check yourself

3 questions

1. A batch of 1,000 rows has one row violating a CHECK constraint. What is written?

Show the answer

Nothing. The write fails atomically.

2. Why does a generated event_date column help queries filtering on event_ts?

Show the answer

Delta derives a partition filter from the generation expression. Knowing event_date = CAST(event_ts AS DATE) lets Delta prune partitions.

3. Identity column values are...

Show the answer

Unique and increasing, possibly with gaps. Writers reserve ranges, so gaps are normal.

Practice it

Interview problems that use this: write the PySpark, run it, and get graded on hidden tests.

Solve: Data Quality: Null Count per Column →

Keep going

Up next · lesson 12 of 26 · 3 min read
Compaction, OPTIMIZE and Z-order
Rewrite small files into large ones and cluster data so queries skip more of it.

Related lessons

Previous: Schema enforcement and evolution

Primary sources: Delta constraints · Delta generated columns