-
Notifications
You must be signed in to change notification settings - Fork 129
Add support for summary metrics in Lakeflow Declarative Pipelines / DLT #1301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
14bcddb
ab300a2
05e1cf2
1c82d8c
7e5d856
2965c1b
d534894
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| # Databricks notebook source | ||
| import dlt | ||
|
|
||
| # COMMAND ---------- | ||
|
|
||
| # MAGIC | ||
| # MAGIC %md | ||
| # MAGIC ## DQX in a Lakeflow Pipeline (formerly Delta Live Tables - DLT) — data quality with quarantine pattern | ||
| # MAGIC | ||
| # MAGIC This demo applies DQX checks and **splits** the data into a valid `silver` table and a `quarantine` | ||
| # MAGIC table. It also persists the checked layer (`bronze_dq_check`) so summary metrics can be computed as | ||
| # MAGIC a materialized view over all rows. For a simpler pipeline that reports issues as columns without | ||
| # MAGIC quarantining, see `dqx_dlt_demo.py`. | ||
| # MAGIC | ||
| # MAGIC Create new ETL Pipeline to execute this notebook (see [here](https://docs.databricks.com/aws/en/getting-started/data-pipeline-get-started)): | ||
| # MAGIC 1. Upload the notebook to a Databricks Workspace | ||
| # MAGIC 2. Go to `Workflows` tab > `Create` > `ETL Pipeline` > `Add existing assets` > select the source code path and root directory | ||
| # MAGIC 3. Add DQX library as a [dependency](https://docs.databricks.com/aws/en/dlt/dlt-multi-file-editor#environment) to the pipeline: Go to `Settings` > `Edit environment` > Add `databricks‑labs‑dqx` as dependency | ||
| # MAGIC 4. Run the pipeline | ||
| # MAGIC | ||
| # MAGIC | ||
| # MAGIC As an alternative to setting the environment as described above, you can also [install](https://docs.databricks.com/aws/en/dlt/external-dependencies) DQX directly in the notebook. Put the below commands as first cells in the notebook: | ||
| # MAGIC | ||
| # MAGIC %`pip install databricks-labs-dqx` | ||
| # MAGIC | ||
| # MAGIC `dbutils.library.restartPython()` | ||
| # MAGIC | ||
|
|
||
| # COMMAND ---------- | ||
|
|
||
| import yaml | ||
| from databricks.labs.dqx.engine import DQEngine | ||
| from databricks.labs.dqx.metrics_observer import DQMetricsObserver | ||
| from databricks.sdk import WorkspaceClient | ||
|
|
||
| # compute_summary_metrics requires an observer on the engine; it reads any custom_metrics from it. | ||
| dq_engine = DQEngine(WorkspaceClient(), observer=DQMetricsObserver()) | ||
|
|
||
| # COMMAND ---------- | ||
|
|
||
| # MAGIC %md | ||
| # MAGIC ## Define Data Quality checks | ||
|
|
||
| # COMMAND ---------- | ||
|
|
||
| # Define checks in YAML format. They can also be defined using classes or loaded from a file or a table. | ||
| checks = yaml.safe_load(""" | ||
| - check: | ||
| function: is_not_null | ||
| arguments: | ||
| column: vendor_id | ||
| name: vendor_id_is_null | ||
| criticality: error | ||
| - check: | ||
| function: is_not_null_and_not_empty | ||
| arguments: | ||
| column: vendor_id | ||
| trim_strings: true | ||
| name: vendor_id_is_null_or_empty | ||
| criticality: error | ||
|
|
||
| - check: | ||
| function: is_not_null | ||
| arguments: | ||
| column: pickup_datetime | ||
| name: pickup_datetime_is_null | ||
| criticality: error | ||
| - check: | ||
| function: is_not_in_future | ||
| arguments: | ||
| column: pickup_datetime | ||
| name: pickup_datetime_isnt_in_range | ||
| criticality: warn | ||
|
|
||
| - check: | ||
| function: is_not_in_future | ||
| arguments: | ||
| column: pickup_datetime | ||
| name: pickup_datetime_not_in_future | ||
| criticality: warn | ||
| - check: | ||
| function: is_not_in_future | ||
| arguments: | ||
| column: dropoff_datetime | ||
| name: dropoff_datetime_not_in_future | ||
| criticality: warn | ||
| - check: | ||
| function: is_not_null | ||
| arguments: | ||
| column: passenger_count | ||
| name: passenger_count_is_null | ||
| criticality: error | ||
| - check: | ||
| function: is_in_range | ||
| arguments: | ||
| column: passenger_count | ||
| min_limit: 0 | ||
| max_limit: 6 | ||
| name: passenger_incorrect_count | ||
| criticality: warn | ||
| - check: | ||
| function: is_not_null | ||
| arguments: | ||
| column: trip_distance | ||
| name: trip_distance_is_null | ||
| criticality: error | ||
| """) | ||
|
|
||
| # COMMAND ---------- | ||
|
|
||
| # MAGIC %md | ||
| # MAGIC ## Define Lakeflow Pipeline (bronze -> silver + quarantine -> metrics) | ||
|
|
||
| # COMMAND ---------- | ||
|
|
||
| # Bronze: raw input as a streaming view. | ||
| @dlt.view | ||
| def bronze(): | ||
| return spark.readStream.format("delta") \ | ||
| .load("/databricks-datasets/delta-sharing/samples/nyctaxi_2019") | ||
|
|
||
| # COMMAND ---------- | ||
|
|
||
| # Apply checks and persist the checked data as a table so the summary-metrics materialized view | ||
| # below can read the result columns (_errors/_warnings) after the pipeline writes them. | ||
| @dlt.table | ||
| def bronze_dq_check(): | ||
| df = dlt.read_stream("bronze") | ||
| return dq_engine.apply_checks_by_metadata(df, checks) | ||
|
|
||
| # COMMAND ---------- | ||
|
|
||
| # Silver: rows without errors or warnings, with the auxiliary result columns dropped. | ||
| @dlt.table | ||
| def silver(): | ||
| df = dlt.read_stream("bronze_dq_check") | ||
| return dq_engine.get_valid(df) | ||
|
|
||
| # COMMAND ---------- | ||
|
|
||
| # Quarantine: only rows with errors or warnings. | ||
| @dlt.table | ||
| def quarantine(): | ||
| df = dlt.read_stream("bronze_dq_check") | ||
| return dq_engine.get_invalid(df) | ||
|
|
||
| # COMMAND ---------- | ||
|
|
||
| # Summary Metrics: materialized view computed by aggregation over the checked table. | ||
| # One row per metric (input / error / warning / valid row counts and per-check breakdown). | ||
| @dlt.table | ||
| def dq_summary_metrics(): | ||
| df = dlt.read("bronze_dq_check") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same applies here. |
||
| return dq_engine.compute_summary_metrics(df, checks=checks) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1019,6 +1019,53 @@ To enable summary metrics programmatically, create and pass a `DQMetricsObserver | |
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| ### Enabling summary metrics in Lakeflow Pipelines | ||
|
|
||
| Inside a Lakeflow Pipeline (Spark Declarative Pipeline / DLT), do the following: | ||
|
|
||
| 1. Persist the checked data as a table (so the result columns are available downstream). | ||
| 2. Add a materialized view that calls `compute_summary_metrics` on that table, passing the same `checks` you applied. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An MV will recompute metrics over the entire checked table. There are 2 possible challenges with this:
Should we instruct users to use a foreachBatch sink in these scenarios? |
||
|
|
||
| <Tabs> | ||
| <TabItem value="Python" label="Python" default> | ||
| ```python | ||
| import dlt | ||
| from databricks.labs.dqx.engine import DQEngine | ||
| from databricks.labs.dqx.metrics_observer import DQMetricsObserver | ||
| from databricks.sdk import WorkspaceClient | ||
|
|
||
| # compute_summary_metrics requires an observer on the engine; it reads any custom_metrics from it. | ||
| dq_engine = DQEngine(WorkspaceClient(), observer=DQMetricsObserver()) | ||
|
|
||
| @dlt.view | ||
| def bronze(): | ||
| return spark.readStream.table("catalog.schema.input") | ||
|
|
||
| # 1. Apply checks and persist the checked data as a table. | ||
| @dlt.table | ||
| def silver(): | ||
| df = dlt.read_stream("bronze") | ||
| return dq_engine.apply_checks_by_metadata(df, checks) | ||
|
|
||
| # 2. Compute summary metrics as a materialized view over the checked table. | ||
| @dlt.table | ||
| def dq_summary_metrics(): | ||
| df = dlt.read("silver") | ||
| return dq_engine.compute_summary_metrics(df, checks=checks) | ||
| ``` | ||
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| This works for both batch and streaming pipelines. See the [Lakeflow pipeline demo](https://github.com/databrickslabs/dqx/blob/v0.15.0/demos/dqx_dlt_demo.py) for a complete example, or the [quarantine variant](https://github.com/databrickslabs/dqx/blob/v0.15.0/demos/dqx_dlt_demo_quarantine.py) that splits valid and invalid records into separate tables. | ||
|
|
||
| <Admonition type="info" title="Summary Metrics: Snapshot vs. history"> | ||
| The example is using a materialized view to compute the summary metrics. The view is recomputed on each pipeline update, so it holds a **current snapshot** of the metrics over all rows in the checked table. Because it aggregates the whole table, `input_row_count` is the cumulative total, not a per-run count. | ||
|
|
||
| To keep a **history** of metrics over time you need an append target: for **streaming** pipelines append per-window metrics from a windowed streaming table (recommended), and for **batch** pipelines append `compute_summary_metrics` output from a companion job. See the [Summary Metrics guide](/docs/guide/summary_metrics) for examples. | ||
| </Admonition> | ||
|
|
||
| **Why not the observer directly?** The observer (`observe()`) and the streaming metrics listener do not work inside a declarative pipeline: the pipeline runtime — not your code — triggers the write action, so the Spark Observation is never populated (`observation.get` stalls) and the streaming listener receives no events. DQX detects the pipeline runtime and automatically skips wiring `observe()` there. `compute_summary_metrics` computes the same metrics as an aggregation instead, but still reads from the engine's observer — so **an observer must be configured** on the engine (calling it without one raises `InvalidParameterError`), and if you need **custom metrics**, configure it with them (`DQMetricsObserver(custom_metrics=[...])`). See the [Summary Metrics guide](/docs/guide/summary_metrics) for details. | ||
|
|
||
| ### Enabling summary metrics in DQX workflows | ||
|
|
||
| Summary metrics can also be enabled in DQX workflows. Metrics are configured: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This defines an MV on the entire silver table with all of its historical data and does not scope the metrics to the latest run or micro-batch. Should we show a
foreachBatchsink that reads data with the streaming APIs instead? This seems more scalable.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, although
foreachBatchis more complex and is for streaming pipelines only. I guess we should document all 3 approaches:As for the demo, I'm not sure, maybe we can have a separate one with foreach_batch? the MV one will definitely be simpler.