Data Lineage Guide

Column-level lineage: what it actually shows, and where it stops

By the engineer who builds Datadef, from client work on data platforms · Reviewed August 21, 2026

Every lineage vendor sells column-level lineage, and almost none of them say what the extra detail buys you or where it quietly degrades. The short version: table-level lineage tells a data engineer that five tables are connected to the one being changed, column-level lineage tells them which two of the five actually read the field, and which three can be left alone. That difference is the whole value, and it disappears the moment the parser hits SQL it cannot follow.

7 min readFor data engineers deciding whether table-level lineage is enough

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

143/20003 credits left
Try:

No account needed · Editable canvas, not a picture

Table level and column level answer different questions

Table-level lineage draws the paths between datasets. It is enough to answer scoping questions: which pipelines touch this warehouse, which marts sit downstream of ingestion, where does this domain start and end. What it cannot tell you is anything about the content of the table, because it never looked inside.

Column-level lineage records the derivation of each field: which upstream columns feed it, through which operation, in which model. That turns a vague blast radius into a short list. Renaming customer_email in a staging model is a five-table problem at table grain and a two-column problem at column grain, and only one of those two numbers is worth waking anyone up for.

The practical test: if the answer you need is a list of teams to notify, table level is usually enough. If the answer you need is whether a specific change is safe to merge tonight, you need the column.

What a column-level edge should carry

An arrow between two columns is not enough on its own. The useful edge carries three facts: the source column, the target column, and the kind of operation between them. A direct copy, an aggregation, a join key, a filter predicate, and a union member all fail differently under a schema change, and a graph that renders them identically hides the part a reviewer needs.

A one-line description of the transformation is the fourth fact, and the one people actually read. "sum over the last 28 days, excluding refunds" is worth more in a review meeting than the SQL it came from, because the reviewer can spot a wrong assumption in a sentence and cannot spot it in a 200-line model.

On Datadef, table nodes carry their named, typed columns, and canvas_add_lineage records the link between two of them with a joinType and that one-line description. The joinType vocabulary is deliberately small: direct, transform, aggregation, lookup, filter, union, or a named join kind. Omit the column names and the same call records a table-level edge, which is how one canvas ends up carrying both grains.

The tool takes between 1 and 100 links in a call, and it resolves each column name against the columns already declared on the node rather than creating one. A name that does not match is refused with the list of the ones that do: Column "net_amount" not found on "stg_orders" (has: order_id, gross_amount, refund_amount). That is slower than accepting whatever it is handed, and it is why the recorded lineage matches the model rather than what someone remembers of it.

{
  "tool": "canvas_add_lineage",
  "links": [
    {
      "sourceNode": "stg_orders",
      "sourceColumn": "net_amount",
      "targetNode": "fct_revenue_daily",
      "targetColumn": "revenue",
      "joinType": "aggregation",
      "description": "sum of net_amount per day, refunds excluded"
    },
    {
      "sourceNode": "raw_events",
      "targetNode": "stg_orders",
      "joinType": "direct",
      "description": "table-level: no column detail needed on this hop"
    }
  ]
}

Where column-level lineage degrades

Automated column-level lineage is SQL parsing, so it inherits every limit of SQL parsing. SELECT star resolves only if the parser knows the schema at that moment. Dynamic SQL built as a string is opaque. Stored procedures, user-defined functions, and anything running inside a Spark job in a language other than SQL usually collapse to a table-level edge or vanish. Semi-structured extraction from JSON columns is often reported at the column that holds the blob, not the field inside it.

The second gap is the BI layer. A large share of real business logic lives in dashboard-level calculations, spreadsheet formulas, and reverse ETL mappings that no warehouse parser sees. A lineage graph that stops at the mart is accurate and still misleading, because the number the executive argues about was computed after the last node.

Neither gap is a reason to skip column-level lineage. It is a reason to know which parts of your graph are parsed, which are declared by a human, and which are simply missing, and to mark them differently rather than pretending the coverage is uniform.

Mixed grain is the honest default

Column detail on the flows that carry regulated or contested numbers, table detail everywhere else. Datadef records both grains on one canvas, so the lineage map does not have to pick one resolution for the whole warehouse.

A worked example: renaming customer_email

stg_orders.customer_email is becoming stg_orders.customer_email_address. At table grain, five models select from stg_orders, so the review opens with five owners and five test suites. That is the number most teams act on, and most of it is noise.

At column grain the list collapses. fct_orders reads order_id and net_amount. fct_order_lines reads order_line_id. dim_date is joined to the table and reads nothing from it. Two models actually touch the field: dim_customers, which passes it through as the contact attribute, and int_marketing_consent, which lowercases it and joins it to the consent table. Those two are the review.

The column view also decides the rollout order, which the table view cannot. int_marketing_consent joins on a derived value, so it is safe as long as both sides move in the same commit. dim_customers copies the field into a published dimension that a CRM sync reads, which is a consumer outside the warehouse and outside the test suite. One of those two facts is a merge order and the other is a message sent before the merge, and neither was visible at table grain.

FAQ

What is column-level lineage in one sentence?

Column-level lineage records which upstream fields produce each downstream field and through which operation, so a single value can be traced back to its sources instead of only the tables that contain it. The practical difference is the size of a change review: five tables may touch a model while only two columns carry the field you are changing.

Is column-level lineage worth it if I already have table-level lineage?

It is worth it for the flows where a wrong answer is expensive: regulated reports, revenue metrics, anything an auditor or a finance team disputes. For general orientation, table-level lineage costs far less to maintain and answers the question just as well.

Why does automated column-level lineage miss some columns?

Because it is produced by parsing SQL. SELECT star without a known schema, dynamic SQL assembled as a string, stored procedures, user-defined functions, and transformations written in Python or Scala often cannot be resolved to specific columns, so the tool falls back to a table-level edge or drops the hop.

Does Datadef harvest column-level lineage from my warehouse?

No. Datadef does not connect to a warehouse or read query logs. Lineage is drawn on the canvas, generated from a description or from the SQL you paste, or produced from a connected repository. The trade is deliberate: a curated map people read, instead of thousands of auto-harvested edges nobody opens.

Can one diagram mix column-level and table-level lineage?

Yes, and it usually should. Each link is recorded with or without column names, so the critical path carries field detail while the surrounding ingestion stays table to table. Mixed grain is what keeps a map readable: full column coverage across a warehouse produces thousands of edges nobody opens, and the audited fields get lost among them.