skip to content

You are redesigning a schema for a system that has been running for years, and the only artefacts available are production data and conversations with domain experts. How do you establish which functional dependencies genuinely hold, and what goes wrong if you get them wrong?

level: principalimportance: nice to knowfreq 25%

answer

  1. Data filters candidates; domain rules confirm them
  2. Trap 1: value allowed to change over time
  3. Trap 2: dependency holds only within a tenant or scope
  4. Trap 3: single write path creates accidental consistency
  5. Encode as a constraint - creation is the real test

basics

~20 s

Mine the data for candidate dependencies, then confirm each with a domain rule - data can only rule candidates out. Watch for time-varying values, tenant scope, nulls and small samples. A wrong dependency yields a wrong key and an unsafe split that silently discards rows.

solid answer

~1 min

Treat data as a **filter**, not a source of truth. Run grouping queries that count distinct right-hand values per left-hand value; any group above one kills the candidate. Survivors are hypotheses to take to domain owners. The traps that produce false dependencies: - **Time.** `product_id -> price` may hold across today's rows and be false over the table's history. Ask whether the value is allowed to change, not whether it has. - **Scope.** A dependency may hold within a tenant or region but not globally, so the real determinant includes the scoping column. - **Nulls.** Grouping treats nulls inconsistently and can hide violations, so test them separately. - **Sample size and coverage.** Rare types, legacy imports and soft-deleted rows are where exceptions hide; test against full history, not a recent slice. - **Enforced by accident.** A dependency that holds only because one code path always writes both columns will break the first time another path writes one. When you have decided, **encode it**: uniqueness and check constraints turn the assertion into something the engine defends. An unenforced dependency degrades. A wrong one gives a wrong candidate key, hence an unsafe decomposition, deduplication that deletes real rows, or a unique constraint that rejects legitimate business data in production.

code

sql · 9 lines
sql
SELECT tenant_id, order_number,
       count(DISTINCT customer_id) AS distinct_rhs,
       count(*) FILTER (WHERE customer_id IS NULL) AS null_rhs
FROM   orders
GROUP  BY tenant_id, order_number
HAVING count(DISTINCT customer_id) > 1
    OR count(*) FILTER (WHERE customer_id IS NULL) > 0
ORDER  BY distinct_rhs DESC
LIMIT  50;

go deeper

for a junior

Know that dependencies come from business rules and that a query can only find counterexamples.

for a middle

Write the profiling query correctly, including scope and null handling, and know to check full history rather than a recent slice.

for a senior

Drive the confirmation conversation with adversarial questions, and use constraint creation as the real validation step.

for a principal

Own the risk model: sequence assertion, enforcement and structural change so a false dependency is caught before it approves an unrecoverable decomposition, and require independent confirmation for irreversible steps.

## Why this is a judgement problem Functional dependencies are constraints over every legal state, and no dataset is a complete record of legal states. So dependency discovery is inference under uncertainty: data narrows the space of candidates, domain knowledge fixes them, and constraints defend them going forward. ## Step 1 - mine candidates from data For each plausible pair, count distinct right-hand values per left-hand value; more than one is a counterexample. Automated profilers do this exhaustively over attribute pairs and small combinations, but combinatorics limit exhaustive search to modest left-hand sizes, so guided testing of business-plausible candidates beats brute force on wide tables. Run the profiling over **full history**, including archived partitions and rows produced by long-dead import paths, since those are where violations concentrate. ## Step 2 - subject each survivor to the traps **Temporal validity.** The most frequent false positive. A price, an address, a tax rate or a display name can each hold a dependency at a single point in time and fail across history. The clarifying question is never "has this changed?" but "is this allowed to change, and if so, do we keep old rows?" If old rows are kept, the dependency belongs to a *versioned* determinant such as `(product_id, valid_from)`, not to `product_id`. **Scope and multi-tenancy.** `order_number -> customer_id` may be true within a tenant and false globally, meaning the true dependency is `(tenant_id, order_number) -> customer_id`. Any dependency mined from a single-tenant-dominant dataset is suspect, because one large tenant can mask the collision. **Nulls.** Standard SQL FD semantics are awkward with nulls, and grouping treats nulls as equal in some contexts and not others. Nullable columns should be tested with explicit predicates, and the answer to "does this determine that?" usually differs for the null and non-null cases - which is itself a signal that the column is optional for a reason and probably belongs in a different relation. **Accidental enforcement.** A dependency may hold because exactly one service writes both columns in one statement. That is not a domain rule; it is an implementation coincidence. The question to ask is who else writes these columns - backfills, admin tools, support scripts, other services - and whether they are all bound by the same rule. **Selection bias.** Filters silently applied to the export (recent rows only, active records only, one region) hide exactly the violations you need. Soft-deleted and cancelled rows are frequent sources. ## Step 3 - confirm with domain owners, precisely Interviews fail when the question is vague. "Does a customer have one address?" gets "yes" from someone thinking of the common case. Better questions are adversarial and concrete: "Can two active orders ever carry the same external reference? What happens today if a partner resubmits one? Has support ever had to merge two records because of it?" Incidents and support workarounds are the best evidence, because they are where exceptions surfaced. Write each confirmed dependency down with its owner and the rule that justifies it. A dependency without a named business rule behind it is a guess. ## Step 4 - encode and let the database prove you right An asserted dependency should become a declared constraint - typically uniqueness on a determinant that is a candidate key, or a check that pins the scoping column. Two benefits: violations become impossible rather than merely unobserved, and the act of adding the constraint to existing data is itself the strongest test you will ever run. If the constraint cannot be created because existing rows violate it, the dependency was false and you found out before the redesign, not after. Where the volume makes a blocking validation impractical, add the constraint in a non-validating mode where the engine supports it - new writes are checked immediately - and validate the historical backlog separately. ## What failure looks like - **Wrong candidate key.** Keys derive from dependencies, so a false dependency yields a key that is not unique. Everything built on it - references from other tables, caches, idempotency logic - inherits the error. - **Unsafe decomposition.** The lossless-join test is a closure computation over the asserted dependencies. Assert a dependency that does not hold and the test approves a split that is genuinely lossy; the fragments are then written independently and the original combinations become unrecoverable. This is the failure that destroys data rather than merely annoying people. - **Production rejections.** A uniqueness constraint derived from a false dependency starts rejecting legitimate writes, usually for the rarest and most important customer. - **Wrong deduplication.** "These rows have the same determinant, so they are duplicates" deletes real, distinct records. ## How to hedge Stage the risk. Assert the dependency, add the constraint, run it in production for a period, and only then perform the structural change that depends on it. Keep the original wide table readable until the fragments have been reconciled. For anything irreversible, require a second, independent confirmation of the dependency - one from data, one from a named domain owner.

  • A profiling tool reports that column A determines column B over 400 million rows with zero violations. Is that enough to design on?
    It is strong evidence that no violation has occurred, not that none can. Ask whether the value is allowed to change over time, whether every write path is bound by the rule, and whether the export was filtered. The decisive step is adding the constraint: if the engine accepts it, future writes are protected, and that protection - not the historical count - is what makes the design safe.
  • How would you sequence a redesign that depends on a newly asserted functional dependency?
    Assert the dependency, declare the corresponding constraint, and let it run in production long enough to cover the real write mix, including monthly and quarterly batch paths. Only then perform the structural change - the decomposition or key change - that assumes it. Keep the original structure readable until the new one has been reconciled, because a false dependency approves a lossy split whose damage is not recoverable afterwards.

saying these in an interview costs you the question

  • Treating a clean data profile as proof that a dependency holds
  • Ignoring that the determined value may legitimately change over time
  • Missing a scoping column such as tenant, so a per-tenant rule is asserted globally
  • Profiling only recent or active rows and missing legacy and soft-deleted data
  • Asserting a dependency without ever declaring the constraint that would defend it

context