Roadmap To Be A Data Engineer / Lesson 14

Lesson 14 GDPR erasure Fundamentals §8 Governance (+§4, §6, §7) About 10 min read

The Deletion That Came Back

A deleted customer reappears after the next full refresh.

Synthetic second-hand fashion marketplace: 90,000 customers, 360,000 listings, 187,060 sold items, €10,033,069.02 GMV over 360 days (AOV €53.64). Self-serve account deletion produces 321 erasure requests over days 200–359 (2.01/day, 0.36% of the base); nobody asks to be deleted while still trading, so every request post-dates its subject’s last activity. The erasure runbook is hand-run weekly against five systems. silver.dim_customer is fully refreshed six times in the window. All figures come from a fully deterministic generator (no RNG); the SQL in §9 was run in DuckDB and asserted against it; the generator ships inside the artifact and was extracted back out of the rendered HTML and re-run — 84 keys compared, zero differing.

Not legal advice: retention periods, lawful bases and the one-month clock are scenario inputs.


1The scene

Day 271. A “we miss you — new arrivals in your size” email lands with a woman who closed her account on day 224 and was told all her personal data had been deleted. It is her 9th such email. The shop database does not have her. silver.dim_customer does — name, email, street — in a row written at 02:14 on day 233, four days after the runbook was signed off. No human re-created her. A dbt run --full-refresh did, from a raw layer that had never been asked to forget anything.

2What “delete me” asks of a platform

Art. 17 gives the right; Art. 12(3) gives one month. In a platform that is not one statement but a list: every place a copy came to rest, and what keeps it there. 14 places here. Thirty days after the runbook reported success, the average erased subject was still held by name and email in 7.77 of them (range 3–9).

Four reasons a deletion does not arrive — the whole taxonomy, four different fixes:

Systems % of subjects still held at day+30
It worked shop.customers, BigQuery time travel 0%, 0%
It was undone by a rebuild silver.dim_customer, gold.customer_360, BI extract 71.3% each
It was never asked raw CDC log 100%, bronze.orders_snapshot 89.4%, email audience 86.6%, reverse-ETL destination 86.6%, ML training exports 86%, support desk 18.4% —
It cannot be honoured on demand nightly backups 100%, 7-year archive 100%, statutory invoice facts 89.4% —

3Failure 1 — the reversible

The runbook is correct on the day it runs, and it is a statement about derived tables. Derived tables are re-derived. Worse: the OLTP delete emits a CDC event whose before image is a complete copy of the person — the act of erasing her is the last thing that writes her name into the lake.

  • 245 of 321 (76.32%) subjects were erased and then rebuilt back into the warehouse.
  • Median survival of an erasure: 11 days (mean 12.48, max 34); 40.41% were back within a week.
  • The 76 still absent on day 359 are exactly those erased after the last full refresh.

A fifth shape of failure. 11 named the constant, 12 the drift, 13 the unwatched. This one is none: the fix was applied, verified, correct — then reversed by a scheduled job. The reversible covers every hand-made correction to something that can be rebuilt (a manual price fix in a mart, a patched dimension row, a backfilled conversion).

4Failure 2 — the copies that were never on the list

Art. 19 obliges notification of each recipient to whom the data have been disclosed — i.e. every reverse ETL destination. The marketing audience is a filtered upsert sync (Lesson 13’s exact bug), so it is the union of everyone ever active and cannot be told that someone left.

Erased subjects still in the email audience 249 / 321 = 77.57%
Marketing emails sent to them 3,079 (12.37 each)
First such email day 210 — five days after the first completed request
Sent before anyone complained (day 271) 480, to 73 people

No error was raised anywhere: the sync log is clean, and every warehouse test is green because gold.customer_vip_status is right. The only evidence lived in the one system the warehouse cannot query.

5Failure 3 — “we pseudonymised it”

Sale facts are retained under a statutory obligation with the customer id replaced by a salted hash. Recital 26’s test is re-identification by any means reasonably likely to be used; Art. 4(5) says pseudonymised data is still personal data. On a marketplace of quantity-1 items, 186,473 of 187,060 sold items (99.69%) are unique on day + brand + category + size + condition + price — every one of those attributes public on the sold-listing page.

Retained facts generalised to… uniquely re-identifiable k ≤ 4 analysts lose
nothing (as shipped) 97/97 · 100% 100% —
week + €25 buckets 79/97 · 81.44% 97.94% daily revenue, exact prices
month + brand tier + €100, no size 4/97 · 4.12% 13.4% 1,557 brand-week cells → 36 (43.2× coarser)

The residual is the point: at the strongest setting all 4 survivors sold a luxury item (28.57% of that tier, 0% of the other two). Mass-market sellers hide in a crowd; a Hermès seller has none.

6The clock

Copy gone after honest answer
BigQuery time travel + fail-safe 7 + 7 days inside the month, on a timer
Nightly backups ×35 35 days documented restore-and-re-erase
Monthly archive ×84 2,555 days 85.2× the deadline → crypto-shredding
Raw CDC log, daily snapshots never tombstones + compaction, or crypto-shredding
Statutory invoice records 10 years legal-hold vault, minimised, out of analytics

Crypto-shredding (per-subject data key; destroy the key, 32 bytes) is the only mechanism whose cost does not grow with the number of copies. Caveats: the key store’s own backups are then the one place a hard delete must really work; regulators have not uniformly accepted it as erasure; and it does nothing for free text or for the re-identification above.

7Could anyone have seen it? — and the ladder

Daily row-count delta on silver.dim_customer (signups carry a weekday pattern, so the series has real variance: every non-refresh day sits in [+44, +84]).

The expectation was wrong. Five of the six refreshes are the five largest daily increases in the table’s history: +103 (3.94σ), +112 (4.89σ), +113 (4.99σ), +102 (3.84σ), +142 (8.04σ). The exception matters most: the first refresh added only +73 (0.79σ, inside the band) because only ten subjects had been erased by then. The detector gets easier to trip the longer the bug runs — the cheapest day to catch it is the day it is hardest to see.

Check Fires Emails by then Verdict
A1 row count vs its own band day 233 74 ✓ but blind at the start, and names nobody
A2 weekly erasure verification: join the register to every registered system, expect zero day 217 16 ✓ names subjects; needs nobody to have predicted this bug
A3 read the marketing destination back, diff against the register day 217 16 ✓ the only check covering systems you don’t own
D a customer complains day 271 480 ✗ 30× worse, and what happened

8The fix — erasure is data, not an operation

  1. An erasure register: hashed id, dates, basis for anything retained. 321 rows = 10,272 bytes.
  2. Anti-join it inside every model, so a full refresh reproduces the erasure — 0 resurrections by construction rather than by vigilance.
  3. A machine-readable registry of destinations (Art. 30, made useful): the verification job iterates it, so a sync added next quarter is covered the day it is registered.
  4. Sync state, not membership (Lesson 13) so a destination can be told someone is gone.
  5. Crypto-shredding where rewriting is off the table.
  6. Minimise at ingest — tokenise at the edge; the cheapest copy to erase is the one never made.

If a correction is not represented as data the pipeline reads, it is not a correction — it is a pause.

9The SQL, run

-- 01 · erasure verification. One per system in the registry.
--      Lesson 13's boundary reconciliation, target set to zero.
SELECT count(*) AS still_present
FROM   silver.dim_customer d
JOIN   governance.erasure_register r ON r.subject_id = d.customer_id
WHERE  r.erased_day <= 217;
-- 10   (ten people confirmed as deleted, sitting in the warehouse)
-- 02 · how anonymous is the retained fact table, really?
WITH f AS (
  SELECT md5(seller_id || ':pepper') AS seller_pseudo, sold_day // 7 AS sold_week,
         brand, category, size, condition, (price_cents // 2500) * 2500 AS price_bucket
  FROM   gold.sale_facts
), k AS (
  SELECT sold_week, brand, category, size, condition, price_bucket,
         count(DISTINCT seller_pseudo) AS k
  FROM   f GROUP BY ALL
), joined AS (
  SELECT f.seller_pseudo, min(k.k) AS k_min
  FROM   f JOIN k USING (sold_week, brand, category, size, condition, price_bucket)
  GROUP  BY 1
)
SELECT count(*) AS erased_sellers,
       count(*) FILTER (WHERE k_min = 1) AS uniquely_identifiable
FROM   joined
WHERE  seller_pseudo IN (SELECT md5(subject_id || ':pepper') FROM governance.erasure_register);
-- 97 · 79   (81.44%)
-- 03 · the fix. The register is inside the model, so a full refresh
--      reproduces the erasure instead of reversing it.
CREATE OR REPLACE TABLE silver.dim_customer AS
SELECT c.* FROM bronze.customers c
WHERE  NOT EXISTS (SELECT 1 FROM governance.erasure_register r
                   WHERE  r.subject_hash = sha256(lower(trim(c.email))));
-- re-running query 01 afterwards: 0

10Ask your team

  1. When we delete a customer, which systems does that reach — and who wrote the list down?
  2. Which of our tables are rebuilt from a source that was never told about the deletion?
  3. Is there a query I can run today returning the number of people confirmed as erased who are still in the warehouse? What does it return?
  4. What do we tell someone about their data in our backups, and is it written down?
  5. The tables we keep for accounting — could an outsider work out whose row it is from the columns we left in?
  6. If we deleted an engineer’s laptop CSV export from six months ago, would anyone know it existed?

11Hands on (~60 min, Python + DuckDB)

  1. Run the generator; confirm 321 requests and 245 resurrections.
  2. Implement the runbook as three DELETEs, then --full-refresh as CREATE OR REPLACE TABLE … AS SELECT … FROM bronze. Watch the deletions vanish. There is no error to catch in that loop.
  3. Add the register, anti-join it inside the model, re-run: verification must return 0. Four lines.
  4. Write the verification job as a loop over a registry list of (system, query); fail loudly on non-zero. Add a system and watch the check cover it for free.
  5. Compute k-anonymity at the three generalisation levels; then do it for your own most sensitive fact table.
  6. Break it: implement crypto-shredding, then restore a backup of the key store and notice you have just un-erased somebody.
  7. Push to GitHub with a README stating what the verification job guarantees and which of the fourteen systems it does not cover.

12Takeaway

Deletion is not an operation you perform on a data platform; it is a property the platform has to preserve. Anything done by hand to a derived table is undone the next time something derives it — 245 of 321 here, median 11 days. So the register goes inside the models, destinations you don’t own get read back and diffed, copies you can’t rewrite get their key destroyed, and rows you must keep get generalised until the person is no longer the only one who could have sold that blouse. The number to ask for tomorrow: how many people confirmed as erased are still in the warehouse? On day 217 the answer was 10 — 16 emails instead of 480.

13Vocabulary

Term Meaning
Right to erasure (Art. 17) The right to have personal data deleted; answered within one month (Art. 12(3)), subject to exemptions such as a legal retention obligation
Erasure register A minimal table of hashed subject ids and dates, joined inside the models so every rebuild reproduces the erasure — and a re-import can be refused
Tombstone A delete marker in an append-only log or table format, so consumers learn a row is gone rather than just stop seeing it
Crypto-shredding Per-subject encryption key destroyed on erasure, rendering every copy — backups and immutable logs included — unreadable at once
Pseudonymisation (Art. 4(5)) Replacing identifiers so data can only be re-attributed with additional information. Still personal data
Anonymisation (Recital 26) The subject can no longer be identified by any means reasonably likely to be used. A property of the whole row
k-anonymity How many individuals share a row’s quasi-identifiers. k = 1 means the row identifies its subject
Quasi-identifier Not an identifier alone, but one in combination — sale date, brand, size, price
Legal-hold vault The one store that lawfully keeps what erasure would remove: minimum fields, separate grants, out of models and exports, retention clock on the row
The reversible failure A correct fix applied to a derived object, undone by the next scheduled rebuild
Back to top