The dashboard is wrong. Someone notices on Tuesday. The root cause turns out to be a column that started arriving as NULL three weeks ago. The pipeline didn’t fail. The data landed. The table grew. Nothing broke – except every metric downstream.
Silent data quality regressions are the most expensive kind of bug. They don’t crash anything, so they don’t trigger alerts. They corrupt analytics gradually, and by the time someone notices, you’re explaining to leadership why last month’s board deck used bad numbers.
The fix is defense-in-depth: row-level validation that catches known-bad data at ingestion time, plus statistical monitoring that catches unknown-bad data by detecting distributional drift. Neither layer alone is sufficient. Expectations catch rows that violate explicit rules. Monitoring catches shifts that no rule anticipated.
Two Layers of Quality #
flowchart TB
subgraph Layer1["Layer 1: Row-Level (DLT Expectations)"]
R["Incoming rows"]
E1["expect: email is valid"]
E2["expect_or_drop: amount > 0"]
E3["expect_or_fail: customer_id NOT NULL"]
R --> E1 & E2 & E3
end
subgraph Layer2["Layer 2: Table-Level (Lakehouse Monitoring)"]
T["Materialized table"]
P["Statistical profiling"]
D["Drift detection
(vs. baseline)"]
T --> P --> D
end
subgraph Alert["Alerting"]
S["Slack / PagerDuty"]
DB["Quality Dashboard"]
end
E1 & E2 & E3 --> T
D --> S
D --> DB
style R fill:#3498db,color:white
style T fill:#2ecc71,color:white
style D fill:#e74c3c,color:white
style S fill:#f39c12,color:white
Layer 1: DLT Expectations (Row-Level Validation) #
Expectations are constraints defined inline with your Declarative Pipeline tables. They fire on every row, every micro-batch. Three modes:
| Mode | Behavior | Use Case |
|---|---|---|
expect |
Log violation, keep the row | Soft validation, metrics tracking |
expect_or_drop |
Silently drop violating rows | Filtering bad data without failing |
expect_or_fail |
Fail the pipeline | Hard constraint, data must be correct |
Setup: Orders Pipeline with Expectations #
|
|
Generate sample data with intentional quality issues:
|
|
Define the Pipeline with Expectations #
|
|
What happens at runtime:
valid_email: Rows with invalid emails pass through but are logged as violations. You see the count in the pipeline event log.positive_amount: Rows with negative or zero amounts are silently dropped. They never reachorders_validated.has_customer_id: If any row has a NULLcustomer_id, the entire pipeline fails. This is your hard constraint – orders without customers are corrupted data.valid_postal_code: Soft check, logged but not enforced. Use this to track data quality trends without disrupting the pipeline.
Querying Expectation Results #
After the pipeline runs, the event log records every expectation evaluation:
|
|
Layer 2: Lakehouse Monitoring (Statistical Drift Detection) #
Expectations catch rows that violate rules you wrote. But what about quality issues nobody anticipated? A region column that suddenly has 90% of values as “UNKNOWN” when it used to be evenly distributed. An amount column whose mean shifts from $200 to $50 because of a pricing bug upstream.
Lakehouse Monitoring profiles your table statistically and compares current distributions against a baseline. No rules to write – it detects anomalies in the data itself.
Enable a Monitor #
|
|
Configuration breakdown:
output_schema_name: Where profiling results are stored (auto-created tables)schedule: How often to re-profile. Hourly is aggressive for most tables – daily is more typicalslicing_exprs: Profile per-slice, not just globally. Drift in a single region gets detected even if the global distribution looks finebaseline_table_name: The “known good” snapshot to compare against. If omitted, the monitor uses the first profile as the baseline
Create the Baseline #
|
|
Query Drift Results #
After the monitor runs, it populates profiling and drift tables:
|
|
|
|
|
|
Custom Metrics #
Built-in profiling covers standard statistics (mean, stddev, null ratio, distinct count). For business-specific quality metrics, define custom metrics:
|
|
Integrating Alerts #
Webhook Alerts to Slack #
Set up a notification destination in the Databricks workspace, then link it to the monitor:
|
|
|
|
Quality Dashboard #
Build a dashboard over the monitoring system tables to visualize quality trends:
|
|
Putting Both Layers Together #
The two layers complement each other:
| Aspect | DLT Expectations | Lakehouse Monitoring |
|---|---|---|
| Scope | Row-level | Table-level / column-level |
| When | Every micro-batch (streaming) or batch | Scheduled (cron) |
| What it catches | Known violations (rules you write) | Unknown drift (statistical) |
| Response | Drop/fail/log per row | Alert, drift report |
| False positives | None (rules are exact) | Possible (statistical thresholds) |
| Setup effort | Inline decorators | SDK call + baseline |
A complete quality gate looks like this:
- Expectations block obviously bad data at ingestion: null keys fail the pipeline, negative amounts get dropped, invalid formats get logged
- Monitoring catches subtle regressions after the data lands: distribution shifts, unusual null spikes, column cardinality changes
- Alerts notify the right people: Slack for the data team, email for governance, PagerDuty for critical pipelines
- Dashboard gives the historical view: quality trends over weeks and months, per-slice breakdowns
Gotchas #
Monitor refresh frequency vs. cost. Profiling wide tables (100+ columns) is not free. Each monitor run reads the entire table (or the incremental window) and computes statistics per column per slice. Running hourly on a billion-row table will cost real compute. Start with daily or weekly and increase frequency only for critical tables.
Expectation naming collisions. If two expectations have the same name on different tables, the event log becomes confusing. Name them descriptively: orders_valid_email, not just valid_email. There’s no enforcement of unique names across tables.
Baseline staleness. Your baseline represents “what good data looks like.” If the business genuinely changes (new regions, different pricing), the baseline becomes outdated and the monitor flags everything as drift. Refresh the baseline periodically after validating that the new distribution is correct.
expect_or_fail in streaming pipelines. If a single bad row triggers a pipeline failure, you need to fix the source data and restart. For high-volume streaming, this can mean hours of reprocessing. Use expect_or_fail only for constraints that truly indicate corrupt data (null keys), not for soft quality issues. Use expect_or_drop for fields where dropping bad rows is acceptable.
Slicing expressiveness. slicing_exprs accepts column expressions, not arbitrary SQL. You can slice by region or DATE(order_date), but complex slicing logic needs to be materialized as a column first.
Conclusion #
Data quality is not a single check. It’s two systems working at different granularities.
DLT Expectations are your first line – they enforce what you know must be true about every row. Null keys, positive amounts, valid formats. These are deterministic, zero-false-positive validations baked into your pipeline.
Lakehouse Monitoring is your second line – it catches what you didn’t anticipate. Distribution shifts, cardinality changes, unusual null rates. These are statistical signals that something changed upstream, even if every individual row passes your explicit rules.
Neither layer replaces the other. A row can pass every expectation and still be part of a distributional anomaly. A statistical drift can be real and intentional (new market launch) rather than a bug. Both layers together give you confidence that bad data doesn’t silently corrupt your analytics.