In a warehouse dimension table, what do not-null and accepted-values tests each catch?
answer
- two column tests, two different failures
- one is about presence
- one is about a bounded list
- the source added a code nobody mapped
- a blank bar appears in the dashboard
basics
~20 sA not-null test asserts a column is always populated — keys, effective dates, the default unknown member. An accepted-values test asserts the column only ever contains a listed set of codes, so a new source value nobody mapped fails loudly instead of appearing in a report.
solid answer
~50 sThey guard two different failure modes. **Not-null** protects columns whose absence breaks something structural: the surrogate key, the natural key, the validity dates on a versioned row, and any attribute a report groups by — a NULL there silently creates a phantom "blank" category in every dashboard. **Accepted-values** pins a low-cardinality column to its documented domain: status codes, country codes, channel, segment. Its real job is catching change in the source, not bad data as such. When a product team adds a `PENDING_REVIEW` status, the enumeration test fails on the first load, which is exactly when someone can decide how it maps. Without it, the value flows through, falls outside every `CASE` branch downstream, and quietly lands in an "other" bucket or gets dropped by a filter. I treat the accepted-values list as documentation that cannot drift, because it is executed.
code
sql · 7 lines-- accepted-values: anything outside the documented domain
-- the explicit NULL branch matters: `not in` against NULL is unknown
select status_code, count(*) as row_count
from dim_order
where status_code not in ('NEW','PAID','SHIPPED','CANCELLED')
or status_code is null
group by status_code;go deeper
Know what each test asserts and be able to write the SQL for both. The memorable point: a NULL in a grouping column shows up as a blank category in a dashboard rather than as an error.
Explain that accepted-values is really a source-change detector, and that its value list must stay aligned with the CASE expression consuming the column. Mention the NULL trap in not in.
Show judgment about severity — which assertions block a run and which warn — and about placement, testing at the staging boundary so a failure names the source change rather than the symptom.
Own the convention across teams: which columns are contractually non-nullable, how enumerations are versioned when a source adds a code, and how warnings stay actionable instead of accumulating into noise.
## Two assertions, two different failures Column-level tests are the cheapest tests in a warehouse and the ones most often written carelessly. Two carry most of the weight on a dimension table: not-null and accepted-values. ## Not-null: which columns actually need it A not-null assertion says the column is populated on every row. The mistake is applying it either to everything or to nothing. Columns that genuinely need it: - **The surrogate key.** It is the join target for every fact; a NULL there means fact rows can never resolve to that dimension row. - **The natural key** (the source system's identifier). Without it you cannot match a source record to its dimension row on the next load, so history breaks. - **Validity dates on a versioned dimension.** A row whose effective-from is NULL cannot be placed in time, and every point-in-time lookup will skip or double-count it. - **Attributes reports group by.** A NULL in a `country` or `segment` column does not error — it creates a blank row in every chart, and stakeholders read that blank as a real category. Columns that should stay nullable: genuinely optional attributes, where NULL carries a meaning the business accepts. The important discipline is to decide which of "unknown", "not applicable" and "not yet collected" a NULL represents and write that down; a column where NULL means three different things is a defect no test can fix. A distinct and very common convention is to avoid NULL keys altogether by pointing unmatched facts at a reserved default dimension row instead. Where that convention is in force, the not-null test on the foreign key becomes enforceable rather than aspirational, and unmatched rows stay visible in reports rather than vanishing from inner joins. ## Accepted-values: an executable enumeration An accepted-values test lists the legal contents of a low-cardinality column and fails when anything else appears: ```sql select status_code, count(*) as row_count from dim_order where status_code not in ('NEW','PAID','SHIPPED','CANCELLED') or status_code is null group by status_code ``` Note the explicit NULL branch: `not in` against a NULL yields unknown rather than true, so a NULL would otherwise slip past. That is the single most common bug in a hand-written enumeration test. What it is really for is **detecting source change**. Upstream systems add statuses, rename channels, split a segment in two. None of those changes announce themselves. Downstream, your model almost certainly has a `CASE` expression that maps codes to labels or to a boolean, and an unmapped code silently falls to the `ELSE` branch, gets bucketed as "other", or is dropped by a `WHERE` filter that lists the statuses it wants. The number stays plausible and becomes wrong. Because of that, an accepted-values test's list should be kept in sync with the `CASE` expression that consumes it. If they disagree, the test is enforcing a domain the model does not actually handle. ## Severity: warn or fail? This is where judgment enters. A new enumeration value is usually *unexpected* rather than *corrupt*, and failing the whole pipeline over a new status code can be disproportionate — the existing rows are still fine. A common posture: not-null on keys and validity dates blocks the run, while accepted-values raises a warning that is triaged the same day, escalating to blocking on columns where an unmapped value produces a materially wrong number (revenue recognition status, refund flag). What you must not do is set every test to warn and then stop reading warnings. A test nobody acts on is worse than no test, because it manufactures the feeling of coverage. ## Cardinality is the sanity check Accepted-values only makes sense on genuinely bounded columns — a dozen statuses, two hundred countries. Applying it to a column with thousands of distinct values produces a list nobody maintains, and it will be silenced within a month. For high-cardinality columns the useful assertions are different in kind: not-null, a format or length check, or a distinct-count range that fails when cardinality jumps unexpectedly. ## Why these belong on every load Both tests are cheap — a single scan, often on a small dimension — and both catch problems at the boundary where the data enters, before anything joins to it. Running them only on the published mart tells you a number is wrong; running them on the staging model tells you the source changed, which is the actual news.
- Which columns on a dimension would you deliberately leave nullable?Genuinely optional attributes, where NULL carries a meaning the business has agreed to — an unset second address line, a segment that has not been assigned. The rule I apply is that NULL must mean exactly one thing in that column; if it is standing in for unknown, not-applicable and not-yet-collected at once, I split it out rather than test it.
- Should an accepted-values failure block the pipeline or just warn?It depends on what the unmapped value does downstream. If a `CASE` expression silently buckets it as other and a revenue figure shifts, block. If it only affects a descriptive label, a warning triaged the same day is proportionate. The failure mode to avoid is setting everything to warn and then not reading warnings.
- Why is an accepted-values test a poor fit for a high-cardinality column?The list stops being maintainable — nobody keeps a thousand-value enumeration current, so the test gets silenced. For high-cardinality columns I assert different properties instead: not-null, a format or length pattern, or a distinct-count range that fires when cardinality jumps in a way the business cannot explain.
saying these in an interview costs you the question
- Applying accepted-values to a high-cardinality column
- Writing not in without handling NULL, so NULLs pass
- Treating NULL as three different meanings in one column
- Letting the test list drift from the CASE expression downstream
- Setting every test to warn and never reading warnings