<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>article Propagating Deletes: Managing Data Removal using Delta Live Tables in Technical Blog</title>
    <link>https://community.databricks.com/t5/technical-blog/propagating-deletes-managing-data-removal-using-delta-live/ba-p/90978</link>
    <description>&lt;H1&gt;&lt;SPAN&gt;Introduction&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;Deleting specific data from our tables may be necessary for various reasons, such as complying with regulations like &lt;/SPAN&gt;&lt;A href="https://commission.europa.eu/law/law-topic/data-protection/data-protection-eu_en" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;The General Data Protection Regulation&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; (GDPR) and &lt;/SPAN&gt;&lt;A href="https://oag.ca.gov/privacy/ccpa" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;California Consumer Privacy Act&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; (CCPA), which require companies to remove user data within a set timeframe. It is essential to ensure that this data is removed from all relevant tables, especially when dealing with sensitive information such as personally identifiable information (PII).&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;A href="https://www.databricks.com/product/delta-live-tables" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Delta Live Tables&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; (DLT), a popular Databricks product, simplifies building and running ETL pipelines, especially for streaming workloads. While some methods for propagating deletes when using DLT are covered in the blog post &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/handling-right-be-forgotten-gdpr-and-ccpa-using-delta-live-tables-dlt" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Handling "Right to be Forgotten" in GDPR and CCPA using Delta Live Tables (DLT)&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;, I will be exploring a new approach using the &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/delta-change-data-feed.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Change Data Feed &lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;(CDF) feature, recently enabled for DLT streaming tables. This feature allows us to propagate deletes more efficiently throughout the pipeline.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;In this post, I’ll show you how to delete data from an upstream table and use the Delta CDF and &lt;EM&gt;apply_changes&lt;/EM&gt; to propagate those deletes. &lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;Reference architecture&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;In this example, the pipeline starts with the &lt;EM&gt;user_bronze&lt;/EM&gt; table, which ingests data from the source. When a row is deleted from this table, we will use the CDF along with the &lt;EM&gt;apply_changes&lt;/EM&gt; function to propagate the deletion to the downstream &lt;EM&gt;user_silver&lt;/EM&gt; table, which is a streaming table and the target of &lt;EM&gt;apply_changes&lt;/EM&gt;. Finally, the &lt;EM&gt;user_gold&lt;/EM&gt; table, a materialised view that summarises the data, reflects the updates from &lt;EM&gt;user_silver&lt;/EM&gt;. The following sections break down each step in detail.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="GDPR-blog.jpeg" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/11334iCBCF0173D287E66B/image-size/large?v=v2&amp;amp;px=999" role="button" title="GDPR-blog.jpeg" alt="GDPR-blog.jpeg" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&amp;nbsp;&lt;/H2&gt;
&lt;H2&gt;&lt;SPAN&gt;Delete the data from the bronze table&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;The first table in the pipeline, where data from the source is stored, is called &lt;/SPAN&gt;&lt;EM&gt;user_bronze&lt;/EM&gt;&lt;SPAN&gt;, and it’s defined as a streaming table loading data incrementally from a Kafka topic&lt;/SPAN&gt;&lt;/P&gt;
&lt;TABLE style="border-style: hidden; width: 100%;" border="1" width="100%"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD width="100%" height="218px"&gt;&lt;LI-CODE lang="python"&gt; .table()
 def user_bronze:
    return (
      spark.readStream
        .format("kafka")
        .option("kafka.bootstrap.servers", "host1:port1,...")
        .option("subscribe", "topic1")
        .load()
    )&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;SPAN&gt;Now, we need to delete one of the rows from this table - which we want to subsequently be reflected in all downstream tables. We can run &lt;A href="https://docs.databricks.com/en/delta-live-tables/cdc.html#add-change-or-delete-data-in-a-target-streaming-table" target="_blank" rel="noopener"&gt;DML statements&lt;/A&gt;, including DELETE, on a DLT streaming table. To delete a row with a specific &lt;EM&gt;user_email&lt;/EM&gt; from the &lt;EM&gt;user_bronze&lt;/EM&gt; table, we can use this statement, which runs outside DLT (e.g., from a notebook).&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;TABLE style="border-style: hidden; width: 100%;" border="1" width="100%"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD width="100%"&gt;&lt;LI-CODE lang="python"&gt;DELETE FROM user_bronze WHERE user_email = "user1@mail.com"
&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;SPAN&gt;This type of efficient point deletion is possible thanks to &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/glossary/acid-transactions" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;ACID transactions&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; in Delta, along with optimisations like &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/deletion-vectors.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;deletion vectors&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;, &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/data-skipping.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;data skipping&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; using &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/data-skipping.html#what-is-z-ordering" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Z-ordering&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;, and, more recently, &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/clustering.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;liquid clustering&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;&lt;STRONG&gt;Note, that this command only removes the data from the table, not from the underlying parquet files.&lt;/STRONG&gt; I’ll cover how to remove data from the files in the section on Vacuum and Deletion Vectors, which is important for cases like GDPR compliance.&lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;How to propagate deletes with change data feed (CDF)&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;After deleting data from the most upstream table (&lt;EM&gt;user_bronze&lt;/EM&gt; in our example), the delete needs to be applied to all downstream tables as well.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Previously, methods discussed in the blog post &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/handling-right-be-forgotten-gdpr-and-ccpa-using-delta-live-tables-dlt" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Handling "Right to be Forgotten" in GDPR and CCPA using Delta Live Tables (DLT)&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; included performing a full refresh of downstream streaming tables or manually implementing deletes for each streaming table. While these work, they aren’t always efficient or require extra processes to remove rows from multiple locations.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Now, with Delta CDF available for DLT streaming tables, we can take a more streamlined approach for these use cases using the &lt;EM&gt;apply_changes&lt;/EM&gt; API. In cases where we cannot use this function, we still need to rely on the earlier methods. I'll provide more details on this later in the blog post, but for now, let's dive into how we can use CDF to improve on those previous approaches.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;Change Data Feed (CDF)&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;When a streaming table is updated or data is deleted, it’s impossible to incrementally retrieve those changes via streaming since we can only stream from an append-only table—any updates or deletes in the source table would &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/structured-streaming/delta-lake.html#ignore-updates-and-deletes" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;break&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; the pipeline.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/delta-change-data-feed.html" target="_self"&gt;CDF&lt;/A&gt; solves this by allowing us to &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/structured-streaming/delta-lake.html#stream-a-delta-lake-change-data-capture-cdc-feed" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;incrementally retrieve&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; all changes to a Delta table, including updates and deletes, and propagate them downstream.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;This feature is now enabled for DLT streaming tables by default. This includes the streaming tables that are the target of &lt;A href="https://docs.databricks.com/en/delta-live-tables/cdc.html" target="_blank" rel="noopener"&gt;apply_changes&lt;/A&gt; command.&lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Propagating deletes using CDF and apply_changes&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;The &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt; function in DLT can delete a row from the target table if the corresponding row is deleted from the source. For example, when processing data from Bronze to Silver, we can read CDF from the Bronze table and use &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt; on the target table with delete handling enabled. &lt;/SPAN&gt;&lt;SPAN&gt;This is done by using the &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta-live-tables/python-ref.html#change-data-capture-from-a-change-feed-with-python-in-delta-live-tables" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;apply_as_deletes&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; option, which specifies the criteria for triggering a delete. With CDF enabled on the source streaming table, deletes are marked by setting &lt;/SPAN&gt;&lt;EM&gt;_change_type&lt;/EM&gt;&lt;SPAN&gt; to &lt;/SPAN&gt;&lt;EM&gt;delete&lt;/EM&gt;&lt;SPAN&gt; in the CDF data.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;To read CDF data from a source table (e.g., the &lt;EM&gt;user_bronze&lt;/EM&gt; table), here’s the approach:&lt;/SPAN&gt;&lt;/P&gt;
&lt;TABLE style="border-style: hidden; width: 100%;" border="1" width="100%"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD width="100%"&gt;&lt;LI-CODE lang="python"&gt; .view()
 def cdf_user_bronze():
    df = spark.readStream.option("readChangeFeed", "true").table(f"LIVE.user_bronze")
    return df&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;SPAN&gt;Note, currently, we need to use &lt;EM&gt;spark.readStream&lt;/EM&gt; to read the CDF data instead of &lt;EM&gt;dlt.read_stream&lt;/EM&gt;. However, we must ensure to use the LIVE keyword so that DLT handles the dependencies between the bronze table (&lt;EM&gt;user_bronze&lt;/EM&gt;) and the CDF view (&lt;EM&gt;cdf_user_bronze&lt;/EM&gt;).&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;statement will look like this, for our target table &lt;/SPAN&gt;&lt;EM&gt;user_silver&lt;/EM&gt;&lt;SPAN&gt;:&lt;/SPAN&gt;&lt;/P&gt;
&lt;TABLE style="border-style: hidden; width: 100%;" border="1" width="100%"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD width="100%"&gt;&lt;LI-CODE lang="python"&gt;create_streaming_table("user_silver")

apply_changes(
  target = "user_silver",
  source = "cdf_user_bronze",
  keys = ["user_id"],
  sequence_by = struct('user_timestamp','_commit_version'),
  except_column_list = ["_change_type", "_commit_version", "_commit_timestamp"],
  apply_as_deletes = expr("_change_type = 'delete'"),
  stored_as_scd_type = 1
)
&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;I have included both &lt;EM&gt;user_timestamp&lt;/EM&gt; and &lt;EM&gt;_commit_version&lt;/EM&gt; so that if there are multiple rows in the CDF data for the same &lt;EM&gt;user_id&lt;/EM&gt; and &lt;EM&gt;user_timestamp&lt;/EM&gt; (such as both INSERTs and UPDATEs), the rows are applied in the correct order based on the &lt;EM&gt;_commit_version&lt;/EM&gt;.&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The &lt;EM&gt;except_column_list&lt;/EM&gt; prevents these certain columns from being stored in the target table, as including them would prevent DLT from turning on CDF for the table, resulting in this warning message:&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="Screenshot 2024-09-19 at 11.12.46 AM.png" style="width: 700px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/11335iCD59055D08F38E2D/image-dimensions/700x218?v=v2" width="700" height="218" role="button" title="Screenshot 2024-09-19 at 11.12.46 AM.png" alt="Screenshot 2024-09-19 at 11.12.46 AM.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;When using &lt;EM&gt;apply_changes&lt;/EM&gt; to delete rows, keep in mind that with SCD Type 2, the existing row is only marked as expired in the target table. &lt;STRONG&gt;So, if full removal of a row is required, such as for strict GDPR compliance, SCD Type 1 must be used to guarantee the row is completely deleted.&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;With &lt;EM&gt;apply_changes&lt;/EM&gt; out-of-order data is handled by temporarily retaining the deleted row as a tombstone in the underlying Delta table. A view is automatically created in the metastore to filter out these tombstones, so they aren't visible in queries.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;It is possible to &lt;A href="https://docs.databricks.com/en/delta-live-tables/settings.html#control-tombstone-management-for-scd-type-1-queries" target="_blank" rel="noopener"&gt;configure&lt;/A&gt; how long these tombstones are retained using the below settings:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN&gt;&lt;EM&gt;pipelines.cdc.tombstoneGCThresholdInSeconds&lt;/EM&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN&gt;&lt;EM&gt;pipelines.applyChanges.tombstoneGCFrequencyInSeconds&lt;/EM&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;&lt;SPAN&gt;Propagate deletes for append-only streaming tables&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;So far, I've discussed propagating deletes for tables designed to use the &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt; API, where &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt; was already an integral part of the pipeline for updating target tables. When working with append-only streaming tables, it's possible to use CDF and switch to &lt;EM&gt;apply_changes&lt;/EM&gt; in some cases, but not in others. The two scenarios are:&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Propagating deletes for datasets with unique keys&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;If our dataset doesn’t contain duplicate keys, we can use &lt;EM&gt;apply_changes&lt;/EM&gt; with SCD Type 1 while maintaining an append-only table (since no updates are necessary). This approach allows us to propagate deletes using the method described earlier. However, &lt;EM&gt;apply_changes&lt;/EM&gt; is more computationally expensive compared to a simple append, so it's important to consider its potential impact on data processing latency.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Propagate deletes when keys can be duplicated&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;If our dataset contains duplicate keys and we intend to append new rows without updating existing ones, &lt;EM&gt;apply_changes&lt;/EM&gt; won't work for propagating deletes, as it updates existing data rather than appending. In this case, we can follow Solution 3 from the &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/handling-right-be-forgotten-gdpr-and-ccpa-using-delta-live-tables-dlt" target="_blank" rel="noopener"&gt;&lt;I&gt;&lt;SPAN&gt;Handling "Right to be Forgotten" in GDPR and CCPA using Delta Live Tables (DLT)&lt;/SPAN&gt;&lt;/I&gt;&lt;/A&gt;&lt;SPAN&gt; blog post, either by running individual DELETEs on each streaming table or performing a full refresh.&amp;nbsp;&lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Propagate deletes for materialised views&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;The gold table in our example is a materialised view defined as below.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;TABLE style="border-style: hidden; width: 100%;" border="1" width="100%"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD width="100%"&gt;&lt;LI-CODE lang="php"&gt;CREATE MATERIALIZED VIEW user_gold AS
WITH PercentileCTE AS (
   SELECT
       user_id,
       visits,
       PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY visits) OVER () AS percentile_90
   FROM
       LIVE.user_silver
)
SELECT
   user_id,
   visits
FROM
   PercentileCTE
WHERE
   visits &amp;gt; percentile_90;&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;SPAN&gt;As mentioned in &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/handling-right-be-forgotten-gdpr-and-ccpa-using-delta-live-tables-dlt" target="_blank" rel="noopener"&gt;&lt;I&gt;&lt;SPAN&gt;Handling "Right to be Forgotten" in GDPR and CCPA using Delta Live Tables (DLT)&lt;/SPAN&gt;&lt;/I&gt;&lt;/A&gt;&lt;SPAN&gt;, deletes can be propagated using materialised views (MVs) since MVs always reflect the latest data from their upstream sources. If a delete occurs upstream, it will be reflected in the MVs during their refresh.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;If MVs and their upstream tables are part of the same DLT pipeline, DLT automatically handles MV refreshes whenever there are changes in the upstream tables. However, if MVs are in a separate pipeline, they need to be updated by scheduling&amp;nbsp;&lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/jobs/create-run-jobs.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Databricks jobs&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; to run the pipelines sequentially.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Keep in mind that it is not possible to stream or read CDF from an MV, so any downstream table of the MVs must also be an MV to propagate deletes.&lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;STRONG&gt;Remove data from storage&amp;nbsp;&lt;/STRONG&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Delta tables retain a &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/history.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;history&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; to support time travel and table restoration. This means that even after a delete or update, the underlying data is still kept for a certain period, even though it’s no longer visible in the current version of the table.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;To ensure data is fully removed within the required timeframe, especially for compliance purposes, it's necessary to configure the table's history retention period accordingly and run &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/sql/language-manual/delta-vacuum.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;VACUUM&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; to remove any files containing deleted data from object storage. The &lt;/SPAN&gt;&lt;EM&gt;VACUUM&lt;/EM&gt;&lt;SPAN&gt; operation removes files that are no longer relevant to the current state of the Delta table. For details on configuring the retention period, refer to &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/history.html#configure-data-retention-for-time-travel-queries" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;&amp;nbsp;this documentation&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;. It is recommended that the retention period be set to at least 7 days. For DLT, this can be set up using table properties when defining a table.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Even though the recommended approach for tasks like &lt;EM&gt;VACUUM&lt;/EM&gt; on Unity Catalog (UC) is to allow &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/optimizations/predictive-optimization.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;predictive optimisation&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; to manage them, this is currently not supported for DLT (see the limitations &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/optimizations/predictive-optimization.html#limitations" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;here&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;). However, &lt;/SPAN&gt;&lt;EM&gt;VACUUM&lt;/EM&gt;&lt;SPAN&gt; is performed as part of DLT &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta-live-tables/index.html#maintenance-tasks-performed-by-delta-live-tables" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;maintenance tasks&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;. Note that these maintenance tasks occur within 24 hours of a table being updated and are performed only if a pipeline update has run within the 24 hours before the maintenance tasks are scheduled. So, if a row is deleted using DML, it’s essential that the relevant DLT pipelines run within the necessary timeframe.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;When enabling &lt;A href="https://docs.databricks.com/en/delta/deletion-vectors.html" target="_blank" rel="noopener"&gt;deletion vectors&lt;/A&gt; (DVs) on a table, it's important to remember that DVs are designed to optimise writes by adding metadata that marks data as deleted rather than actually removing it from the underlying parquet files. If we need to physically remove data from cloud storage for compliance purposes, such as with GDPR, we’ll need to run &lt;A href="https://docs.databricks.com/aws/en/delta/vacuum#purge-metadata-only-deletes-to-force-data-rewrite" target="_blank" rel="noopener"&gt;REORG&lt;/A&gt; to rewrite the files and apply the deletes.&amp;nbsp;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;Conclusion&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;In this post, we have explored&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;how to efficiently propagate deletes across Delta Live Tables (DLT) pipelines using Change Data Feed (CDF) and &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt;. We can streamline the propagation of deletes using this method if the tables in our pipeline can be updated using &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt;. This includes streaming pipelines where, by design, we want to update a table using SCD Type 1 or SCD Type 2 methods. It also works for streaming tables that are append-only (no updates needed), but since there are no duplicate keys, using &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt; still results in an append-only table.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;For append-only streaming tables with duplicate keys or when performance is a concern, we must rely on the methods outlined &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/handling-right-be-forgotten-gdpr-and-ccpa-using-delta-live-tables-dlt" target="_blank" rel="noopener"&gt;&lt;I&gt;&lt;SPAN&gt;Handling "Right to be Forgotten" in GDPR and CCPA using Delta Live Tables (DLT)&lt;/SPAN&gt;&lt;/I&gt;&lt;/A&gt;&lt;SPAN&gt; to propagate deletes.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;If it is necessary to remove data from both the underlying files and the tables, we should employ SCD Type 1 and adjust the Delta history retention period and &lt;EM&gt;VACUUM&lt;/EM&gt; accordingly.&lt;/SPAN&gt;&lt;/P&gt;</description>
    <pubDate>Tue, 06 May 2025 09:54:28 GMT</pubDate>
    <dc:creator>yas_mokri</dc:creator>
    <dc:date>2025-05-06T09:54:28Z</dc:date>
    <item>
      <title>Propagating Deletes: Managing Data Removal using Delta Live Tables</title>
      <link>https://community.databricks.com/t5/technical-blog/propagating-deletes-managing-data-removal-using-delta-live/ba-p/90978</link>
      <description>&lt;H1&gt;&lt;SPAN&gt;Introduction&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;Deleting specific data from our tables may be necessary for various reasons, such as complying with regulations like &lt;/SPAN&gt;&lt;A href="https://commission.europa.eu/law/law-topic/data-protection/data-protection-eu_en" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;The General Data Protection Regulation&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; (GDPR) and &lt;/SPAN&gt;&lt;A href="https://oag.ca.gov/privacy/ccpa" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;California Consumer Privacy Act&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; (CCPA), which require companies to remove user data within a set timeframe. It is essential to ensure that this data is removed from all relevant tables, especially when dealing with sensitive information such as personally identifiable information (PII).&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;A href="https://www.databricks.com/product/delta-live-tables" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Delta Live Tables&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; (DLT), a popular Databricks product, simplifies building and running ETL pipelines, especially for streaming workloads. While some methods for propagating deletes when using DLT are covered in the blog post &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/handling-right-be-forgotten-gdpr-and-ccpa-using-delta-live-tables-dlt" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Handling "Right to be Forgotten" in GDPR and CCPA using Delta Live Tables (DLT)&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;, I will be exploring a new approach using the &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/delta-change-data-feed.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Change Data Feed &lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;(CDF) feature, recently enabled for DLT streaming tables. This feature allows us to propagate deletes more efficiently throughout the pipeline.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;In this post, I’ll show you how to delete data from an upstream table and use the Delta CDF and &lt;EM&gt;apply_changes&lt;/EM&gt; to propagate those deletes. &lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;Reference architecture&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;In this example, the pipeline starts with the &lt;EM&gt;user_bronze&lt;/EM&gt; table, which ingests data from the source. When a row is deleted from this table, we will use the CDF along with the &lt;EM&gt;apply_changes&lt;/EM&gt; function to propagate the deletion to the downstream &lt;EM&gt;user_silver&lt;/EM&gt; table, which is a streaming table and the target of &lt;EM&gt;apply_changes&lt;/EM&gt;. Finally, the &lt;EM&gt;user_gold&lt;/EM&gt; table, a materialised view that summarises the data, reflects the updates from &lt;EM&gt;user_silver&lt;/EM&gt;. The following sections break down each step in detail.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="GDPR-blog.jpeg" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/11334iCBCF0173D287E66B/image-size/large?v=v2&amp;amp;px=999" role="button" title="GDPR-blog.jpeg" alt="GDPR-blog.jpeg" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H2&gt;&amp;nbsp;&lt;/H2&gt;
&lt;H2&gt;&lt;SPAN&gt;Delete the data from the bronze table&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;The first table in the pipeline, where data from the source is stored, is called &lt;/SPAN&gt;&lt;EM&gt;user_bronze&lt;/EM&gt;&lt;SPAN&gt;, and it’s defined as a streaming table loading data incrementally from a Kafka topic&lt;/SPAN&gt;&lt;/P&gt;
&lt;TABLE style="border-style: hidden; width: 100%;" border="1" width="100%"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD width="100%" height="218px"&gt;&lt;LI-CODE lang="python"&gt; .table()
 def user_bronze:
    return (
      spark.readStream
        .format("kafka")
        .option("kafka.bootstrap.servers", "host1:port1,...")
        .option("subscribe", "topic1")
        .load()
    )&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;SPAN&gt;Now, we need to delete one of the rows from this table - which we want to subsequently be reflected in all downstream tables. We can run &lt;A href="https://docs.databricks.com/en/delta-live-tables/cdc.html#add-change-or-delete-data-in-a-target-streaming-table" target="_blank" rel="noopener"&gt;DML statements&lt;/A&gt;, including DELETE, on a DLT streaming table. To delete a row with a specific &lt;EM&gt;user_email&lt;/EM&gt; from the &lt;EM&gt;user_bronze&lt;/EM&gt; table, we can use this statement, which runs outside DLT (e.g., from a notebook).&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;TABLE style="border-style: hidden; width: 100%;" border="1" width="100%"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD width="100%"&gt;&lt;LI-CODE lang="python"&gt;DELETE FROM user_bronze WHERE user_email = "user1@mail.com"
&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;SPAN&gt;This type of efficient point deletion is possible thanks to &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/glossary/acid-transactions" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;ACID transactions&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; in Delta, along with optimisations like &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/deletion-vectors.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;deletion vectors&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;, &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/data-skipping.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;data skipping&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; using &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/data-skipping.html#what-is-z-ordering" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Z-ordering&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;, and, more recently, &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/clustering.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;liquid clustering&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;&lt;STRONG&gt;Note, that this command only removes the data from the table, not from the underlying parquet files.&lt;/STRONG&gt; I’ll cover how to remove data from the files in the section on Vacuum and Deletion Vectors, which is important for cases like GDPR compliance.&lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;How to propagate deletes with change data feed (CDF)&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;After deleting data from the most upstream table (&lt;EM&gt;user_bronze&lt;/EM&gt; in our example), the delete needs to be applied to all downstream tables as well.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Previously, methods discussed in the blog post &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/handling-right-be-forgotten-gdpr-and-ccpa-using-delta-live-tables-dlt" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Handling "Right to be Forgotten" in GDPR and CCPA using Delta Live Tables (DLT)&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; included performing a full refresh of downstream streaming tables or manually implementing deletes for each streaming table. While these work, they aren’t always efficient or require extra processes to remove rows from multiple locations.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Now, with Delta CDF available for DLT streaming tables, we can take a more streamlined approach for these use cases using the &lt;EM&gt;apply_changes&lt;/EM&gt; API. In cases where we cannot use this function, we still need to rely on the earlier methods. I'll provide more details on this later in the blog post, but for now, let's dive into how we can use CDF to improve on those previous approaches.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;Change Data Feed (CDF)&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;When a streaming table is updated or data is deleted, it’s impossible to incrementally retrieve those changes via streaming since we can only stream from an append-only table—any updates or deletes in the source table would &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/structured-streaming/delta-lake.html#ignore-updates-and-deletes" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;break&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; the pipeline.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/delta-change-data-feed.html" target="_self"&gt;CDF&lt;/A&gt; solves this by allowing us to &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/structured-streaming/delta-lake.html#stream-a-delta-lake-change-data-capture-cdc-feed" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;incrementally retrieve&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; all changes to a Delta table, including updates and deletes, and propagate them downstream.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;This feature is now enabled for DLT streaming tables by default. This includes the streaming tables that are the target of &lt;A href="https://docs.databricks.com/en/delta-live-tables/cdc.html" target="_blank" rel="noopener"&gt;apply_changes&lt;/A&gt; command.&lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Propagating deletes using CDF and apply_changes&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;The &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt; function in DLT can delete a row from the target table if the corresponding row is deleted from the source. For example, when processing data from Bronze to Silver, we can read CDF from the Bronze table and use &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt; on the target table with delete handling enabled. &lt;/SPAN&gt;&lt;SPAN&gt;This is done by using the &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta-live-tables/python-ref.html#change-data-capture-from-a-change-feed-with-python-in-delta-live-tables" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;apply_as_deletes&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; option, which specifies the criteria for triggering a delete. With CDF enabled on the source streaming table, deletes are marked by setting &lt;/SPAN&gt;&lt;EM&gt;_change_type&lt;/EM&gt;&lt;SPAN&gt; to &lt;/SPAN&gt;&lt;EM&gt;delete&lt;/EM&gt;&lt;SPAN&gt; in the CDF data.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;To read CDF data from a source table (e.g., the &lt;EM&gt;user_bronze&lt;/EM&gt; table), here’s the approach:&lt;/SPAN&gt;&lt;/P&gt;
&lt;TABLE style="border-style: hidden; width: 100%;" border="1" width="100%"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD width="100%"&gt;&lt;LI-CODE lang="python"&gt; .view()
 def cdf_user_bronze():
    df = spark.readStream.option("readChangeFeed", "true").table(f"LIVE.user_bronze")
    return df&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;SPAN&gt;Note, currently, we need to use &lt;EM&gt;spark.readStream&lt;/EM&gt; to read the CDF data instead of &lt;EM&gt;dlt.read_stream&lt;/EM&gt;. However, we must ensure to use the LIVE keyword so that DLT handles the dependencies between the bronze table (&lt;EM&gt;user_bronze&lt;/EM&gt;) and the CDF view (&lt;EM&gt;cdf_user_bronze&lt;/EM&gt;).&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;statement will look like this, for our target table &lt;/SPAN&gt;&lt;EM&gt;user_silver&lt;/EM&gt;&lt;SPAN&gt;:&lt;/SPAN&gt;&lt;/P&gt;
&lt;TABLE style="border-style: hidden; width: 100%;" border="1" width="100%"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD width="100%"&gt;&lt;LI-CODE lang="python"&gt;create_streaming_table("user_silver")

apply_changes(
  target = "user_silver",
  source = "cdf_user_bronze",
  keys = ["user_id"],
  sequence_by = struct('user_timestamp','_commit_version'),
  except_column_list = ["_change_type", "_commit_version", "_commit_timestamp"],
  apply_as_deletes = expr("_change_type = 'delete'"),
  stored_as_scd_type = 1
)
&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;I have included both &lt;EM&gt;user_timestamp&lt;/EM&gt; and &lt;EM&gt;_commit_version&lt;/EM&gt; so that if there are multiple rows in the CDF data for the same &lt;EM&gt;user_id&lt;/EM&gt; and &lt;EM&gt;user_timestamp&lt;/EM&gt; (such as both INSERTs and UPDATEs), the rows are applied in the correct order based on the &lt;EM&gt;_commit_version&lt;/EM&gt;.&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The &lt;EM&gt;except_column_list&lt;/EM&gt; prevents these certain columns from being stored in the target table, as including them would prevent DLT from turning on CDF for the table, resulting in this warning message:&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="Screenshot 2024-09-19 at 11.12.46 AM.png" style="width: 700px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/11335iCD59055D08F38E2D/image-dimensions/700x218?v=v2" width="700" height="218" role="button" title="Screenshot 2024-09-19 at 11.12.46 AM.png" alt="Screenshot 2024-09-19 at 11.12.46 AM.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;When using &lt;EM&gt;apply_changes&lt;/EM&gt; to delete rows, keep in mind that with SCD Type 2, the existing row is only marked as expired in the target table. &lt;STRONG&gt;So, if full removal of a row is required, such as for strict GDPR compliance, SCD Type 1 must be used to guarantee the row is completely deleted.&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;With &lt;EM&gt;apply_changes&lt;/EM&gt; out-of-order data is handled by temporarily retaining the deleted row as a tombstone in the underlying Delta table. A view is automatically created in the metastore to filter out these tombstones, so they aren't visible in queries.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;It is possible to &lt;A href="https://docs.databricks.com/en/delta-live-tables/settings.html#control-tombstone-management-for-scd-type-1-queries" target="_blank" rel="noopener"&gt;configure&lt;/A&gt; how long these tombstones are retained using the below settings:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN&gt;&lt;EM&gt;pipelines.cdc.tombstoneGCThresholdInSeconds&lt;/EM&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN&gt;&lt;EM&gt;pipelines.applyChanges.tombstoneGCFrequencyInSeconds&lt;/EM&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;&lt;SPAN&gt;Propagate deletes for append-only streaming tables&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;So far, I've discussed propagating deletes for tables designed to use the &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt; API, where &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt; was already an integral part of the pipeline for updating target tables. When working with append-only streaming tables, it's possible to use CDF and switch to &lt;EM&gt;apply_changes&lt;/EM&gt; in some cases, but not in others. The two scenarios are:&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Propagating deletes for datasets with unique keys&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;If our dataset doesn’t contain duplicate keys, we can use &lt;EM&gt;apply_changes&lt;/EM&gt; with SCD Type 1 while maintaining an append-only table (since no updates are necessary). This approach allows us to propagate deletes using the method described earlier. However, &lt;EM&gt;apply_changes&lt;/EM&gt; is more computationally expensive compared to a simple append, so it's important to consider its potential impact on data processing latency.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Propagate deletes when keys can be duplicated&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;If our dataset contains duplicate keys and we intend to append new rows without updating existing ones, &lt;EM&gt;apply_changes&lt;/EM&gt; won't work for propagating deletes, as it updates existing data rather than appending. In this case, we can follow Solution 3 from the &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/handling-right-be-forgotten-gdpr-and-ccpa-using-delta-live-tables-dlt" target="_blank" rel="noopener"&gt;&lt;I&gt;&lt;SPAN&gt;Handling "Right to be Forgotten" in GDPR and CCPA using Delta Live Tables (DLT)&lt;/SPAN&gt;&lt;/I&gt;&lt;/A&gt;&lt;SPAN&gt; blog post, either by running individual DELETEs on each streaming table or performing a full refresh.&amp;nbsp;&lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Propagate deletes for materialised views&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;The gold table in our example is a materialised view defined as below.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;TABLE style="border-style: hidden; width: 100%;" border="1" width="100%"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD width="100%"&gt;&lt;LI-CODE lang="php"&gt;CREATE MATERIALIZED VIEW user_gold AS
WITH PercentileCTE AS (
   SELECT
       user_id,
       visits,
       PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY visits) OVER () AS percentile_90
   FROM
       LIVE.user_silver
)
SELECT
   user_id,
   visits
FROM
   PercentileCTE
WHERE
   visits &amp;gt; percentile_90;&lt;/LI-CODE&gt;&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;&lt;SPAN&gt;As mentioned in &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/handling-right-be-forgotten-gdpr-and-ccpa-using-delta-live-tables-dlt" target="_blank" rel="noopener"&gt;&lt;I&gt;&lt;SPAN&gt;Handling "Right to be Forgotten" in GDPR and CCPA using Delta Live Tables (DLT)&lt;/SPAN&gt;&lt;/I&gt;&lt;/A&gt;&lt;SPAN&gt;, deletes can be propagated using materialised views (MVs) since MVs always reflect the latest data from their upstream sources. If a delete occurs upstream, it will be reflected in the MVs during their refresh.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;If MVs and their upstream tables are part of the same DLT pipeline, DLT automatically handles MV refreshes whenever there are changes in the upstream tables. However, if MVs are in a separate pipeline, they need to be updated by scheduling&amp;nbsp;&lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/jobs/create-run-jobs.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Databricks jobs&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; to run the pipelines sequentially.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Keep in mind that it is not possible to stream or read CDF from an MV, so any downstream table of the MVs must also be an MV to propagate deletes.&lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;STRONG&gt;Remove data from storage&amp;nbsp;&lt;/STRONG&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Delta tables retain a &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/history.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;history&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; to support time travel and table restoration. This means that even after a delete or update, the underlying data is still kept for a certain period, even though it’s no longer visible in the current version of the table.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;To ensure data is fully removed within the required timeframe, especially for compliance purposes, it's necessary to configure the table's history retention period accordingly and run &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/sql/language-manual/delta-vacuum.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;VACUUM&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; to remove any files containing deleted data from object storage. The &lt;/SPAN&gt;&lt;EM&gt;VACUUM&lt;/EM&gt;&lt;SPAN&gt; operation removes files that are no longer relevant to the current state of the Delta table. For details on configuring the retention period, refer to &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta/history.html#configure-data-retention-for-time-travel-queries" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;&amp;nbsp;this documentation&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;. It is recommended that the retention period be set to at least 7 days. For DLT, this can be set up using table properties when defining a table.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Even though the recommended approach for tasks like &lt;EM&gt;VACUUM&lt;/EM&gt; on Unity Catalog (UC) is to allow &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/optimizations/predictive-optimization.html" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;predictive optimisation&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; to manage them, this is currently not supported for DLT (see the limitations &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/optimizations/predictive-optimization.html#limitations" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;here&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;). However, &lt;/SPAN&gt;&lt;EM&gt;VACUUM&lt;/EM&gt;&lt;SPAN&gt; is performed as part of DLT &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/en/delta-live-tables/index.html#maintenance-tasks-performed-by-delta-live-tables" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;maintenance tasks&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;. Note that these maintenance tasks occur within 24 hours of a table being updated and are performed only if a pipeline update has run within the 24 hours before the maintenance tasks are scheduled. So, if a row is deleted using DML, it’s essential that the relevant DLT pipelines run within the necessary timeframe.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;When enabling &lt;A href="https://docs.databricks.com/en/delta/deletion-vectors.html" target="_blank" rel="noopener"&gt;deletion vectors&lt;/A&gt; (DVs) on a table, it's important to remember that DVs are designed to optimise writes by adding metadata that marks data as deleted rather than actually removing it from the underlying parquet files. If we need to physically remove data from cloud storage for compliance purposes, such as with GDPR, we’ll need to run &lt;A href="https://docs.databricks.com/aws/en/delta/vacuum#purge-metadata-only-deletes-to-force-data-rewrite" target="_blank" rel="noopener"&gt;REORG&lt;/A&gt; to rewrite the files and apply the deletes.&amp;nbsp;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;Conclusion&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;In this post, we have explored&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;how to efficiently propagate deletes across Delta Live Tables (DLT) pipelines using Change Data Feed (CDF) and &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt;. We can streamline the propagation of deletes using this method if the tables in our pipeline can be updated using &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt;. This includes streaming pipelines where, by design, we want to update a table using SCD Type 1 or SCD Type 2 methods. It also works for streaming tables that are append-only (no updates needed), but since there are no duplicate keys, using &lt;/SPAN&gt;&lt;EM&gt;apply_changes&lt;/EM&gt;&lt;SPAN&gt; still results in an append-only table.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;For append-only streaming tables with duplicate keys or when performance is a concern, we must rely on the methods outlined &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/handling-right-be-forgotten-gdpr-and-ccpa-using-delta-live-tables-dlt" target="_blank" rel="noopener"&gt;&lt;I&gt;&lt;SPAN&gt;Handling "Right to be Forgotten" in GDPR and CCPA using Delta Live Tables (DLT)&lt;/SPAN&gt;&lt;/I&gt;&lt;/A&gt;&lt;SPAN&gt; to propagate deletes.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;If it is necessary to remove data from both the underlying files and the tables, we should employ SCD Type 1 and adjust the Delta history retention period and &lt;EM&gt;VACUUM&lt;/EM&gt; accordingly.&lt;/SPAN&gt;&lt;/P&gt;</description>
      <pubDate>Tue, 06 May 2025 09:54:28 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/propagating-deletes-managing-data-removal-using-delta-live/ba-p/90978</guid>
      <dc:creator>yas_mokri</dc:creator>
      <dc:date>2025-05-06T09:54:28Z</dc:date>
    </item>
    <item>
      <title>Re: Propagating Deletes: Managing Data Removal using Delta Live Tables</title>
      <link>https://community.databricks.com/t5/technical-blog/propagating-deletes-managing-data-removal-using-delta-live/bc-p/93708#M315</link>
      <description>&lt;P&gt;&lt;a href="https://community.databricks.com/t5/user/viewprofilepage/user-id/1421"&gt;@yas_mokri&lt;/a&gt;&amp;nbsp;kudos, nice one.&lt;/P&gt;&lt;P&gt;One thing to clarify "&lt;STRONG&gt;&lt;EM&gt;Note, that this command only removes the data from the table, not from the underlying parquet files&lt;/EM&gt;"&amp;nbsp; -&amp;nbsp;&lt;/STRONG&gt;I think it would be better to say that delete operation creates another delta table version (adding deletion vector file or rewriting parquet if deletion vercors are not enabled) on top of existing ones.&lt;/P&gt;&lt;P&gt;You can access previous historical versions of delta table (time travel feature, with that deleted records available), so as deleted rows.&lt;/P&gt;&lt;P&gt;Of course, the way to remove the data completely (incl. parquets on storage) would be to run vacuum command agains that delta table specifying desired retention period.&lt;/P&gt;</description>
      <pubDate>Sun, 13 Oct 2024 12:53:35 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/propagating-deletes-managing-data-removal-using-delta-live/bc-p/93708#M315</guid>
      <dc:creator>radothede</dc:creator>
      <dc:date>2024-10-13T12:53:35Z</dc:date>
    </item>
    <item>
      <title>Re: Propagating Deletes: Managing Data Removal using Delta Live Tables</title>
      <link>https://community.databricks.com/t5/technical-blog/propagating-deletes-managing-data-removal-using-delta-live/bc-p/108481#M449</link>
      <description>&lt;P&gt;&lt;a href="https://community.databricks.com/t5/user/viewprofilepage/user-id/104480"&gt;@radothede&lt;/a&gt;&amp;nbsp;thanks for the comment. I'd like to point you to the elaboration on vacuum and deletion vectors in the later sections of the blog. I believe these are covered in those sections if not immediately after the sentence you're referring to.&lt;/P&gt;</description>
      <pubDate>Sun, 02 Feb 2025 23:32:54 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/propagating-deletes-managing-data-removal-using-delta-live/bc-p/108481#M449</guid>
      <dc:creator>yas_mokri</dc:creator>
      <dc:date>2025-02-02T23:32:54Z</dc:date>
    </item>
  </channel>
</rss>

