Constraints and generated columns
NOT NULL and CHECK constraints, generated columns and identity columns in Delta tables.
On this page
Show code in
Every code block on the page follows this.
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
- Schema enforcement and evolution · 3 min read
- The Delta transaction log · 8 min read
Comfortable with these? Read on.
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
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
amount = -50.Step 2 · the check
Step 3 · rejected
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
- 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 nameremoves 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.
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
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
Expecting primary keys to be enforced
Expecting identity values without gaps
Adding a CHECK to a table with existing bad rows
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 questions1. 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.
Keep going
Up next · lesson 12 of 26 · 3 min readCompaction, OPTIMIZE and Z-order
Rewrite small files into large ones and cluster data so queries skip more of it.
Related lessons
Schema enforcement and evolutionData lake & lakehouse · 3 min read
Hidden partitioning and partition evolutionData lake & lakehouse · 4 min read
MERGE INTO and upserts
Previous: Schema enforcement and evolution
Primary sources: Delta constraints · Delta generated columns