Data Quality and Validation Questions
Ensuring correctness and trust in data: validation rules, constraints, completeness/accuracy/timeliness checks, and quality frameworks. Covers designing validation into pipelines, quality gates before publishing, and handling edge cases and real-world dirty data. Central to any data engineering or analytics role.
Define idempotency in the context of data pipelines and describe three practical patterns for achieving idempotent ingestion under retries: unique message/idempotency keys, idempotent upserts (merge semantics on a natural or surrogate key), and a dedicated dedupe/staging table. For each pattern, describe a pitfall (a non-idempotent side effect that retries can trigger even when the write itself is idempotent) and how you would test idempotency under a simulated retry/failure scenario.
Sample Answer
Direct answer
Idempotency in a data pipeline means that processing the same input more than once (because of a retry after a timeout, a crash-and-restart, or an at-least-once delivery guarantee) produces the same end state as processing it exactly once, rather than duplicating or corrupting data on the second attempt.
Structured elaboration
- Unique/idempotency keys: attach a deterministic key to each unit of work (a message ID, a request ID) and check, before applying it, whether that key has already been processed; the pitfall is a non-idempotent side effect that happens before the idempotency check itself, for example sending a notification or calling an external API as a side effect of processing, which still fires twice on a retry even if the core database write is correctly deduplicated.
- Idempotent upserts: write using merge/upsert semantics on a natural or surrogate key (INSERT ... ON CONFLICT UPDATE, or a MERGE statement) so that reapplying the same write twice leaves the row in the same final state; the pitfall is an upsert whose UPDATE branch is not itself idempotent, for example one that increments a counter rather than setting an absolute value, which silently double-counts on retry even though the statement itself uses correct upsert syntax.
- Dedicated dedupe/staging table: land every incoming unit of work into a staging table keyed by its unique ID first, then only promote genuinely-new rows from staging into the target table; the pitfall is a staging table with no retention/cleanup policy, which grows unbounded and eventually becomes a performance problem in its own right.
Worked example
Testing idempotency under a simulated retry: replay the exact same batch of records through the pipeline twice in a row (simulating a retry after a false-positive timeout, where the first attempt actually succeeded but the caller did not receive confirmation) and assert that the target table's row count and aggregate sums are identical after the second run as after the first, not doubled. Concretely, replay a batch of 500 orders totaling $48,210. After the first run, the target table has 500 rows summing to $48,210. Replaying that exact same batch again, a correctly idempotent pipeline still shows 500 rows summing to $48,210, not 1,000 rows and $96,420. The assertion differs by pattern because the failure mode differs by pattern: for the idempotency-key pattern, assert the row count stays at 500 and additionally assert the key-tracking table (or equivalent dedupe log) still shows exactly one recorded processing per key, since a bug here often manifests as the core write staying correct while an out-of-band side effect like a notification fires twice; for the idempotent-upsert pattern, assert both the row count AND the summed amount column stay at $48,210, because an upsert with an incorrectly-written UPDATE branch (one that increments a running total instead of setting it) will pass a naive row-count check while silently doubling the sum; for the dedupe/staging-table pattern, assert the staging table itself still contains exactly 500 rows after the second run (not 1,000), since a bug here typically means staging accepted the duplicate batch outright and the promotion step is masking it downstream. This is a cheap, mechanical test to add to a CI suite, and it reliably catches the "upsert that increments instead of sets" class of bug that a code review alone easily misses.
Trade-offs and pitfalls
The recurring, easy-to-miss pitfall across all three patterns is a side effect that happens outside the idempotency-protected write itself, sending an email, calling a billing API, incrementing an external counter, triggered as part of processing a record. The core data write can be perfectly idempotent while this side effect still fires twice on every retry, and because it lives outside the database transaction the idempotency key protects, it is invisible to a test that only checks the target table's final state. Auditing every side effect a pipeline stage triggers, not just its primary data write, is what actually closes this gap.
Explain the difference between a schema mismatch (a field's structure or presence changed, for example a JSON field sometimes arriving as an array and sometimes as a scalar) and a simple data-type inconsistency (a numeric value arriving as text). Give a concrete example of each and describe the downstream consequences for analytics: a failed load, a silently broken join, or a miscalculated aggregate.
Sample Answer
Direct answer
A schema mismatch is a structural change, a field's presence, nesting, or shape has changed, for example a JSON field that sometimes arrives as an array and sometimes as a scalar. A data-type inconsistency is narrower: the field's structure is the same, but the type of the value itself is wrong, for example a numeric field arriving as the text string "42" instead of an integer.
Structured elaboration
Schema mismatches tend to cause hard failures: a strict loader that expects a scalar and receives an array will typically error out and refuse to load the row at all, which is disruptive but at least visible. Data-type inconsistencies are more dangerous precisely because they often do not fail loudly: many query engines will silently coerce a numeric-looking string during a comparison or aggregation, producing a technically-valid but wrong result rather than an error, which is how a type inconsistency turns into a silently miscalculated aggregate rather than a visible load failure.
Worked example
A structural schema mismatch: an events payload's properties field is sometimes {"tags": ["a","b"]} (an array) and sometimes {"tags": "a"} (a bare scalar) depending on which client SDK version sent it; a strict schema loader rejects the second form outright, a visible, fail-fast failure. A data-type inconsistency: a discount_pct column is declared numeric but a small fraction of rows arrive as the string "10%" instead of 0.10; depending on the query engine, SUM(discount_pct) might silently coerce the strings that happen to parse and skip or error on the ones that don't, producing an aggregate that is quietly wrong rather than an obvious failure, since nothing about the query itself errored. A silently broken join is the third named consequence, and it usually comes from the same type-inconsistency root cause rather than the schema-mismatch one: an order_id column is stored as an integer (42) in the orders table but arrives and is stored as the text string "42" in a newer order_events table after an upstream change. An equi-join ON orders.order_id = order_events.order_id does not error, most query engines simply find zero matches for rows where the compared types don't line up as expected, so the join silently returns fewer rows than it should (or, depending on the engine's coercion rules, matches inconsistently for some rows and not others). The downstream report quietly under-counts, with no failed load and no error anywhere in the pipeline to point at.
Trade-offs and pitfalls
The practical consequence of this distinction is where you invest detection effort: schema mismatches are largely self-reporting because they tend to break something loudly, so a basic schema-validation-at-load-time check catches most of them for free. Type inconsistencies need active, targeted detection (a cast-and-flag check, or a profiling pass looking for unexpected non-numeric values in a numeric-typed column) precisely because the default behavior of most systems is to silently coerce rather than fail, which is exactly the behavior that makes them dangerous.
Design an approach to detect schema drift arriving from an upstream source at validation-gate time: added, removed, renamed, or changed-type columns, and unexpected new categorical values in a value-set field. Cover detection (comparing incoming batch schema against a registered expectation), alerting and routing of the drift event, and how the pipeline should react by default (quarantine the affected batch vs coerce and continue with a logged warning) when a drift is detected but has not yet been triaged.
Sample Answer
Direct answer
Detecting schema drift at validation-gate time means checking every incoming batch's actual schema against a registered expectation, catching added, removed, renamed, or type-changed columns as well as unexpected new categorical values, and reacting with a default policy (quarantine the affected batch, or coerce and continue with a logged warning) that is chosen deliberately per table rather than applied uniformly everywhere.
Structured elaboration
- Structural drift detection: compare the incoming batch's column set and types against a stored expected schema; flag added columns (usually lower risk, can often pass through with a warning), removed columns (usually higher risk, likely to break a downstream consumer expecting that field), and type changes (highest risk, often the most silently dangerous since many systems will coerce rather than error). A renamed column is the trickiest case: naively it looks identical to an unrelated removed column plus an unrelated added column landing in the same batch, so a detector that only checks "is every expected column present" will misclassify a rename as a drop-and-add. A practical heuristic is to compare each candidate removed/added pair by ordinal position, declared type, and a sample of actual values (near-identical value distributions or overlapping value sets between the "removed" and "added" column are a strong rename signal); a high-confidence match should be treated and routed as a rename, not as two independent structural events, since the correct reaction (update the schema registry's column mapping) is different from the reaction to a genuine drop.
- New categorical values: for a value-set-constrained field, a new value arriving is not automatically a bug; it may be a legitimate new category (a new product line, a new region) that the schema simply has not caught up with yet, so the default reaction should route it to review rather than silently reject or silently accept it.
- Default reaction policy: quarantine-and-alert is the safer default for a high-stakes table, since it stops the bad batch before it reaches consumers while preserving it for investigation; coerce-and-continue-with-a-logged-warning is reasonable for a lower-stakes table where availability matters more than perfect strictness, provided the coercion behavior itself is deliberate and tested, not an accidental side effect of whatever the query engine happens to do by default.
- Alerting and routing: a drift event needs a named recipient and channel, not just a log line nobody reads. A practical routing rule: every drift event (any severity) posts to a dedicated data-quality Slack or Teams channel with the table name, drift type, and affected column names, so there is always a visible record; a high-severity event specifically (a type change, an unregistered removed column, or an unresolved rename) additionally pages the on-call data engineer directly through the team's paging tool (e.g. PagerDuty), since these are the drift types most likely to silently break a downstream consumer if left until someone happens to notice the channel post; a low-severity event (an added column, or a new categorical value) is posted to the channel and filed into a review queue/ticket for the owning team to triage during business hours, without paging anyone.
Worked example
A production model's input pipeline detects that a categorical feature column gained several new category values and, separately, a numeric column was accidentally cast to string by an upstream change. The structural-drift check flags the type change on the numeric column as high-severity (this table feeds a model whose feature-encoding pipeline is not built to handle a string where it expects a float) and quarantines that batch immediately; the new categorical values are flagged as medium-severity and routed to a review queue rather than blocking the batch, since a new category is plausibly a legitimate business change rather than a bug.
Trade-offs and pitfalls
Applying the same severity and reaction policy to every kind of drift, treating a new category value exactly like a silent type change, either over-blocks (rejecting perfectly valid new business data as if it were corruption) or under-protects (letting a genuinely dangerous type change through with only a warning). Differentiating severity by drift TYPE, not just by "did the schema change," is what makes this gate useful in practice rather than either too noisy to trust or too permissive to catch real problems.
Design a deduplication strategy for a high-throughput streaming pipeline (hundreds of thousands of events per second) where duplicates arrive due to producer retries, out-of-order delivery, and multiple event sources. Compare exact windowed stateful dedup (with watermarking and TTL-bounded state) against approximate approaches (Bloom filters), discuss the false-positive/negative trade-offs of the approximate option, and explain how you would size and checkpoint state so the job recovers correctly after a restart.
Sample Answer
Direct answer
For a high-throughput streaming pipeline (hundreds of thousands of events per second) with duplicates from retries, out-of-order delivery, and multiple producers, exact windowed stateful dedup (tracking seen keys within a bounded time window, evicting old keys via a watermark and TTL) is the correct default; approximate Bloom-filter-based dedup is a fallback only when the exact approach's memory footprint becomes genuinely prohibitive at your specific scale and retention window.
Structured elaboration
- Exact windowed dedup: maintain a keyed state store (an event ID or a composite fingerprint) with a time-to-live matching your maximum expected out-of-order lateness; a watermark advances the window and evicts state for keys old enough that a duplicate is no longer expected to arrive. This is exact (zero false positives or negatives within the window) but its memory cost scales directly with the number of unique keys held in the window at once.
- Approximate Bloom-filter dedup: a Bloom filter answers "have I seen this key before" using a fixed, much smaller memory footprint than storing every key explicitly, at the cost of a tunable false-positive rate (it can wrongly say "seen before" for a genuinely new key, causing you to drop a legitimate event; it never produces a false negative, so it will never let a true duplicate through).
- Sizing worked example: for 10 million active keys in the current window and a target false-positive rate of 0.1%, the standard Bloom-filter sizing formulas give roughly 144 million bits (about 17 MiB) and 10 optimal hash functions, verified by execution against the actual formula, which reproduces a 0.1% false-positive rate exactly at those parameters, a genuinely compact footprint for that many keys.
- Checkpointing for recovery: the state (exact key set or Bloom filter bits) must be checkpointed periodically to durable storage so that after a restart, the job resumes from the last checkpoint rather than either replaying duplicates it had already deduped, or losing its dedup state entirely and starting from an empty filter.
Trade-offs and pitfalls
The consequence of choosing the approximate approach is real and specific: a Bloom filter false positive silently drops a legitimate, never-before-seen event, treating it as a duplicate. For a metric where undercounting is more tolerable than double-counting, this can be an acceptable trade; for a use case (like a payments pipeline) where every legitimate event must be processed, false-positive event loss is unacceptable and the memory cost of exact dedup, even if higher, is the correct choice. Checkpointing frequency itself is also a trade-off: checkpointing too rarely risks reprocessing a large window of already-deduped events after a crash (duplicating work, and briefly reintroducing duplicates downstream until the state catches back up); checkpointing too frequently adds sustained I/O overhead to every processing cycle.
You must deduplicate several billion customer records at national or multi-region scale, where full pairwise comparison and even a naive ROW_NUMBER-partition dedup are computationally infeasible (a job using that pattern now takes 12+ hours on a petabyte-scale table). Propose a scalable architecture using blocking plus locality-sensitive hashing (MinHash/LSH), discuss the space/time trade-offs against exact full-sort deduplication, and explain how you would validate that the approximate approach's precision and recall are acceptable before relying on it in production.
Sample Answer
Direct answer
At several-billion-row scale, both full pairwise comparison and even a straightforward ROW_NUMBER() PARTITION BY key dedup become infeasible (the latter can take 12+ hours on a petabyte-scale table because it still requires a full sort of every partition). The scalable approach combines blocking (grouping records into buckets so only records in the same bucket are ever compared) with locality-sensitive hashing (LSH, specifically MinHash for text similarity), which reduces the comparison workload from quadratic to roughly linear in the number of records.
Structured elaboration
- MinHash/LSH mechanics: represent each record as a set of shingles (overlapping character or token n-grams), compute a compact MinHash signature that approximates Jaccard similarity (the fraction of shared elements between two sets: the number of shingles the two sets have in common, divided by the total number of distinct shingles across both) between the original sets, then bucket records by bands of their MinHash signature (LSH banding) so that records likely to be similar are very likely to land in at least one shared bucket, while dissimilar records almost never do.
- Space/time trade-off against exact full-sort dedup: a full sort and exact comparison guarantees perfect precision and recall but scales as O(n log n) sort cost plus O(n) comparison, which is what makes it slow at petabyte scale; MinHash/LSH trades a small, tunable amount of recall (some true duplicates in different bands are missed) for a dramatic reduction in comparison work, at the cost of extra memory for the signatures and bucket structures.
- Validating precision and recall before production reliance: hold out a labeled sample of known duplicate and known distinct pairs (built manually or from a smaller, exactly-deduplicated reference subset), measure the approximate approach's precision and recall against that labeled sample, and only promote it to production once both are within an agreed tolerance of the exact method's results on the same sample.
Worked example
Take two near-duplicate customer name records, jon smith and john smith (a one-character typo), and represent each as its set of 3-character shingles (a sliding window of 3 consecutive characters, including spaces): jon smith gives the 7 shingles {jon, on_, n_s, _sm, smi, mit, ith} (using _ to show a space), and john smith gives the 8 shingles {sm, hn, ith, joh, mit, n_s, ohn, smi}. These sets share 5 shingles (n_s, _sm, smi, mit, ith) out of 10 distinct shingles across both, so the exact Jaccard similarity, computed directly, is 5/10 = 0.5.
A toy MinHash signature using just 3 hash functions (a real system uses dozens to hundreds; 3 is small enough to trace by hand) shows how MinHash estimates that 0.5 without ever computing the full intersection. Assign each of the 10 distinct shingles an ID from 1 to 10 (jon=1, on_=2, n_s=3, sm=4, smi=5, mit=6, ith=7, joh=8, ohn=9, hn=10), so jon smith = {1,2,3,4,5,6,7} and john smith = {3,4,5,6,7,8,9,10}. Define each hash function as a fixed random permutation of the 10 IDs; the MinHash value for a set is the ID that appears earliest in that permutation among the IDs the set actually contains:
| Hash fn | Permutation order (rank 1 to 10) | argmin, jon smith | argmin, john smith | Match? |
|---|---|---|---|---|
| h1 | 8,3,10,1,6,2,9,4,7,5 | 3 | 8 | No |
| h2 | 5,9,2,7,3,8,1,10,4,6 | 5 | 5 | Yes |
| h3 | 4,10,6,9,1,3,8,5,2,7 | 4 | 4 | Yes |
The resulting signatures are (3, 5, 4) for jon smith and (8, 5, 4) for john smith. 2 of the 3 slots match, giving a MinHash-estimated similarity of 2/3, approximately 0.67, in the right neighborhood of the true 0.5 computed directly above but not exact, because 3 hash functions is a very small sample; a real deployment uses far more to tighten this estimate toward the true value.
For LSH banding, split the 3-value signature into 3 bands of 1 row each: jon smith and john smith land in the same bucket for band h2 and band h3 (since those slots match), so any bucketing rule that only requires a match in ONE band places them together as a candidate pair for a full comparison. Contrast this with a genuinely distinct third record, mary jones: its shingle set shares only one coincidental shingle with jon smith (the substring jon, from maryJONes), out of 14 distinct shingles across the two records, giving a true Jaccard of 1/14, approximately 0.07. With that little overlap, mary jones would need to match jon smith on a band purely by coincidence to land in the same bucket, which is exactly the rare-collision behavior LSH banding parameters (more bands, or more rows per band) are tuned to make unlikely, so in practice it would not be flagged as a candidate duplicate.
Trade-offs and pitfalls
The parameter choices (number of hash functions per signature, number of bands, rows per band) directly trade recall for compute cost, more bands catch more true duplicates but also increase false-positive comparisons and total compute; this is not a set-once decision, it needs to be re-tuned as the underlying data's duplicate rate and record-length distribution shift over time. The pitfall most teams hit is validating the approach once at launch and never re-validating it as data characteristics drift, silently degrading recall (missing more true duplicates) without anyone noticing, since a missed duplicate produces no visible error, only a slowly inflating unique-count metric nobody is actively watching for this specific cause.
Unlock Full Question Bank
Get access to all Data Quality and Validation interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.