explore profile no longer offers composite keys that are artifacts of a near-unique column or of a continuous measure - #470
Merged
Merged
Conversation
…of a near-unique column or of a continuous measure
# Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #292.
Summary
A profile's grain is acted on: callers write deduplication and joins from it, and
transform plan --scaffoldkeys generated models oncandidate_keys[0]. On a2,037-row orders table where
order_idheld 1,927 distinct values, the profilereturned five candidate keys and elected a grain from them. Four were one fact
wearing four hats, that
order_idis unique on all but 110 rows so any widercolumn completes it.
(SUBTOTAL, order_id)is not a grain; it is the observationthat two rows sharing an order id happened to differ in their subtotal. The list
carried no order, so a caller could not tell the real key from the filler, and the
one thing worth saying, that this table has duplicate order ids, was the one thing
the profile did not say.
The same bug shipped in the demo warehouse and was quoted in a committed reference
doc:
order_itemsreported a grain of(unit_price, order_id), aDECIMAL(10,2)money column paired with a foreign key, on a table whose actual story is 1,000
duplicate
order_item_idrows from a double-loaded batch.Two exclusions now apply before any pair is priced,
graincomes backnullrather than wrong when nothing survives, and a new
key_evidencefield carries thereasoning per key.
Decisions worth a reviewer's attention
The near-unique rule is a test on the pair, not on the column. The obvious
version, "drop any pair containing a column that is already 90% unique", deletes a
real grain. On a parent-line table where most orders have a single line,
ORDER_IDis near-unique and
(ORDER_ID, LINE_NO)is still exactly the grain: a two-valuepartner separates the duplicate parents on purpose, where a wider one separates
them by accident. So the rule drops an anchored pair only when its partner's domain
is not bounded by the fanout. That exception is verified live on Snowflake in the
dogfood, and it is the single most load-bearing detail in the change.
A near-unique timestamp gets no such exception. The enumeration escape assumes
the anchor is a parent whose rows a position column enumerates, which a per-row
event time is not. Without this,
(status, created_at)comes back as a key on anytable with a 98%-unique timestamp and a four-value status column, which the new
fixture demonstrated before it was fixed.
The measure vocabulary is not decoration, it is the Snowflake and BigQuery
fallback.
adapters/base.py:is_integer_typealready documents that Snowflake'sSHOW COLUMNSrendersNUMBER(38,0)andNUMBER(10,2)identically asFIXED;BigQuery
NUMERICcarries no scale either. A type-only measure rule wouldtherefore either miss the money on both connectors or, if written as "numeric but
not integer", delete every non-id numeric key member across all of Snowflake. So
the type test requires an explicit nonzero scale and goes deliberately silent
where a dialect erases it, and the name vocabulary covers exactly that gap. A name
never decides alone:
quantity,amountandtotalare legitimatelylow-cardinality members of real fact grains, so anything with a domain bounded by
the fanout is an enumeration before it is ever considered a measure.
A fixed 100-row floor, and it earns its place. Below it every column looks
near-unique and every measure looks continuous, so both rules would suppress grains
rather than junk, and the probes are nearly free at that size anyway. Without the
floor,
test_composite_probe_fills_the_cap_rather_than_discarding_the_grain(6rows) fails:
amount DOUBLEat 4 of 6 distinct would leave three candidatesagainst an asserted cap of five. A fixed floor rather than a config knob, because
it changes what gets measured rather than how loudly a finding reads.
grainisnull, never the near-unique column. Returningorder_idwould bemore directly useful and is wrong:
maintain/drift.pyfolds a single-column graininto its proven-key baseline, so it would report
key_lost_uniquenesson every runforever for a key that never held, and
scaffoldwould key a model on a columnwith duplicates.
Exclude before probing, not probe then disclaim. The exact answer cannot move a
decision either way, the pair would have to be reported and immediately qualified,
and dropping it frees a capped slot for a pair that might be the real grain. On the
demo that is visible: the pairs actually probed fall from five to two, and the two
that get asked are the ones that could have been a grain.
One new field, and the restraint applied where it holds.
data_qualityis aflat
list[str]with no per-key association, it is capped per object inexplore map, and prose naming a suppressed key's columns drags them back into theserialized
columnsthroughcolumns_with_findings. Sokey_evidenceexists. Butthere is no flag (a spurious key being worse than nothing is the issue's premise, so
there is nothing to opt out of), no
probedboolean (the probe's budget notesalready say when it did not run), and
key_evidenceis not inexplore map'spayload.
reasonis prose rather than a code enum because the set of causes is openand a new one should not need a contract change and an exhaustive match in every
consumer.
CACHE_SCHEMA_VERSIONmoves to 4, and heals itself. An older engine reads aversion-4 cache fine. The direction that breaks is a current engine reading a
version-3 one, where a suppressed combination still reads as a ranked candidate and
keeps
maintain grainpaying to re-probe it. Rather than document a caveat, theprofile freshness gate treats a pre-4 profile as stale, so the first
explore profileorexplore mapafter upgrading re-scans.The duplicate figure is phrased as rows to remove. The issue asks for "110
duplicate order ids", but
non_null_rows - distinctcounts surplus rows, not valuesthat repeat, and the two differ: one id appearing 111 times is 110 surplus rows and
exactly one repeated value. The removal phrasing is exactly true in every case,
needs no new scan, and says what to fix. Two long-standing imprecisions in that
sentence are fixed alongside it: the surplus was computed against the total row
count rather than the non-null count, and it carried a
~even when derived fromtwo exact numbers.
Found by dogfooding, not by the suite
explore diagramwas marking a money columnPKin committed output.unit_price PKis gone andorder_idreadsFKrather thanPK.the table,
order_item_idlost its role and dropped out ofexplore map'scolumns, so the map reported "order_item_id is not unique" while not listing
order_item_id.notable_columnsnow keeps a column named by a suppressedentry, with no role, because it is not a key.
probe note is gone and its one distinct fact folded into the grain-unknown
sentence; the two always co-occur.
The demo's map counts moved with it: 7 objects, 5 joins and 6 PII columns unchanged,
data-quality findings 5 to 6. Both READMEs are updated.
Dogfood
DuckDB (
dex demo, free, and the reproduction itself):grain["unit_price", "order_id"]nullcandidate_keys[]BigQuery (
exmergo-viz.dex_ci): all four artifact pairs measured live atexactly 2,037 distinct combinations over 2,037 rows, so every one would have been
reported. After:
candidate_keys: [], andorder_id is the closest thing to one at 94.6% unique (1927 distinct over 2037 rows, 110 rows would have to be removed), theissue's numbers exactly.
NUMERICmoney was excluded by name, since the typecarries no scale. Estimate 41,943,040 bytes, and it held.
Snowflake (
DEX_CI.DEX292), the connector the design turns on, where everycolumn reports the bare token
FIXED: money excluded, and on a second table(ORDER_ID, LINE_NO)reported as the grain withORDER_IDat 90.9% unique. Bothhalves proven on one connector in one run. Estimate 2.0s, settled at 1.55s + 0.29s.