How does column-level lineage differ from table-level lineage for impact analysis in a data platform?
answer
- same graph, different resolution
- one over-reports, one pinpoints
- the schema-change question separates them
- some columns influence rows, not values
- parsing, not runtime, produces field edges
basics
~20 sTable-level lineage says table B was built from table A; column-level says B.total came from A.amount and A.qty. Table-level over-reports impact — every downstream consumer looks affected — while column-level narrows a schema change to the handful of fields that actually break.
solid answer
~50 sBoth are the same graph at different resolutions. **Table-level** edges say a job read dataset A and wrote dataset B. That is enough for incident blast radius ("A is stale, so B and everything below it is stale") and for orchestration questions. **Column-level** edges say which output fields were derived from which input fields, and often *how*: a direct projection, an expression, or an indirect dependency because the column appeared in a `WHERE`, `JOIN` or `GROUP BY` clause without landing in the output. The difference bites on schema change. Ask "we are dropping `A.legacy_status`, what breaks?" and table-level lineage answers with every consumer of A — for a busy staging table, hundreds of models and dashboards, most of which never touched that column. Column-level lineage answers with the three that did. The cost is that column-level lineage has to be derived by parsing and resolving the query, which `select *`, dynamic SQL and opaque user-defined functions all defeat.
code
sql · 7 linescreate table mart.orders_daily as
select o.order_id,
o.gross_amount - o.discount as net_total,
c.region
from raw.orders o
join raw.customers c on c.customer_id = o.customer_id
where o.status = 'PAID';go deeper
Know the distinction in one sentence — dataset-to-dataset edges versus field-to-field edges — and that the finer one exists for questions about specific columns.
Explain why table-level lineage over-reports on a schema-change question, and where field edges come from: parsing and resolving the query, not observing the run.
Demonstrate awareness of the failure modes — indirect dependencies, wildcard projections, opaque transforms, the BI layer — and how you would report partial coverage honestly.
Own the coverage decision: which parts of the estate justify the cost of field-level lineage, and how tagging and propagation policy rides on top of it.
## The same graph at two resolutions Lineage is a derivation graph. **Table-level (or dataset-level) lineage** records edges between whole datasets: this job read `raw.orders` and `raw.customers` and wrote `mart.orders_daily`. **Column-level lineage** records edges between fields: `mart.orders_daily.net_total` was derived from `raw.orders.gross_amount` and `raw.orders.discount`, while `mart.orders_daily.region` came from `raw.customers.region`. Everything table-level lineage supports, column-level supports too — you can always collapse field edges up to their datasets. The reverse is not true, and that asymmetry is the whole subject. ## Where table-level is enough Operational questions are usually dataset-shaped: - **Staleness propagation.** The load into `raw.orders` failed. Everything transitively downstream is stale, regardless of columns. - **Blast radius during an incident.** Which dashboards and feeds sit below the failure, and who owns them. - **Deprecation of a whole table.** Nothing reads it, so it can be deleted. - **Cost attribution and orchestration checks.** Which pipelines feed the ten most expensive marts. For all of these, field-level detail adds nothing and costs effort to maintain. ## Where table-level fails and column-level pays The question column-level lineage exists for is **schema change impact**: a source column is being renamed, retyped, dropped, or its semantics are changing. Ask that of a table-level graph and it over-reports catastrophically. A staging table with three hundred downstream models produces three hundred "affected" answers, so the team either ignores the analysis or spends days manually reading SQL to find the real consumers. The signal-to-noise ratio is so bad it changes behaviour: people stop asking, and ship the change blind. Column-level lineage turns the same question into a precise list. It also enables things table-level cannot express at all: - **Sensitive-data tracking.** Where did `customer.email` end up? A PII tag on a source column can be **propagated** down the column graph, so a mart that derives from it inherits the tag automatically instead of relying on someone to remember. - **Explaining a single metric.** A user asks how `net_revenue` is computed; the column path plus the expressions on each hop is the answer. - **Precise regression scoping in review.** A pull request changes one expression; the reviewer sees exactly which downstream fields shift. - **Column-level cleanup.** A wide table where two thirds of columns have no downstream reader is a real and common finding. ## Direct versus indirect dependencies Good column-level lineage distinguishes two edge kinds, and candidates who have used it know the difference: - **Direct**: the input column's value flows into the output column, via projection or an expression. - **Indirect**: the input column influenced *which rows* exist without contributing a value — it appeared in a `WHERE` predicate, a `JOIN` condition, a `GROUP BY`, or a window's `PARTITION BY`. ```sql select o.id, o.gross_amount - o.discount as net_total, -- direct: gross_amount, discount c.region -- direct: c.region from orders o join customers c on c.id = o.customer_id -- indirect: customer_id, c.id where o.status = 'PAID'; -- indirect: o.status ``` Dropping `status` breaks this model even though no output column carries it. Lineage that records only direct edges would report it as safe — a false negative, which is a far worse failure mode than the false positives of table-level lineage. ## Where column-level lineage comes from, and where it breaks Field edges cannot be observed from the outside; a job that reads a table and writes a table looks identical at runtime whichever columns it touched. So column-level lineage is **derived by analysing the query or code**: parse the SQL into a syntax tree, resolve identifiers against real catalog schemas, and walk the projection through subqueries and CTEs. That pipeline has known blind spots: - **`select *`** — resolvable only if the parser knows the upstream schema at that moment, and stale if the upstream gains a column later. - **Dynamic or generated SQL** — the text does not exist until runtime, so static analysis sees nothing. - **Opaque code paths** — a user-defined function, a Python transform, a stored procedure. Parsers typically degrade to "all inputs may affect all outputs" or drop the edge entirely. - **The reporting layer** — a BI tool's own calculated fields form a second graph that the warehouse parser never sees, so lineage frequently stops at the last table. Because of these, treat column coverage as partial by construction. The right posture is to know which parts of the estate have trustworthy field-level edges and which do not, rather than presenting a graph that silently omits a dependency. ## Choosing Start table-level: it is cheap, obtainable from runtime events, and covers the incident-shaped questions that dominate day to day. Add column-level where change is frequent and expensive — core source tables, published marts, anything carrying regulated fields — and accept coarse edges elsewhere. Being explicit about the resolution you have is more useful than pretending to uniform coverage.
- Why can't column-level lineage be captured by observing a job at runtime the way table-level lineage can?From outside, a job that reads a table and writes a table looks the same whichever fields it touched — the read is at table or file granularity. Field edges live in the logic, so they must be derived by parsing the query or code and resolving identifiers against catalog schemas.
- What is an indirect column dependency, and why does missing it matter?A column that shapes which rows exist without contributing a value: it appears in a filter, join condition, grouping or partition clause. Omitting those edges makes lineage report that dropping such a column is safe when it will break the model outright — a false negative, which is worse than the over-reporting of table-level lineage.
- How does column-level lineage help with sensitive-data governance?Tag a source column as personal data once, then propagate the tag along field edges so every derived column inherits it automatically. That turns "which tables contain personal data" from a manual survey that decays into a derived property of the graph, and makes deletion and access-review scoping tractable.
- Where does column-level lineage most often stop being trustworthy?At `select *`, dynamically generated SQL, and opaque transforms such as user-defined functions or Python steps, where a parser cannot resolve the mapping. It also stops at the BI layer, whose calculated fields form a second graph the warehouse parser never sees. Coverage should be treated as partial and its gaps documented.
saying these in an interview costs you the question
- Says column-level lineage is just table lineage with more rows
- Assumes runtime events can emit field-level edges
- Ignores columns used only in filters, joins or grouping
- Presents parsed column lineage as complete despite select * and UDFs
- Uses table-level lineage for schema-change impact and calls the result precise