Datatrail
LINEAGE - COLUMN LEVEL

Column-Level Data Lineage: Trace Any Column Back to Its Source

Datatrail traces a single column through every join, CASE statement, and aggregation between its source and the report that uses it. When someone asks where orders.revenue comes from, you have the exact answer in one click.

See pricing
Read-only Never moves your data
Lineage map
Lineage mapped from query history. Read-only connection.
0

Read-only connection. Datatrail never moves or mutates your data.

In short

Column-level lineage traces a single column through every transformation between its source and the place it lands, including renames, joins, and aggregations. Datatrail parses the SQL of each dbt model and warehouse query to show that fct_orders.revenue derives from stg_stripe_charges.amount minus stg_refunds.amount, not just that the tables are related. This field-level resolution lets you answer where a number comes from and what feeds it without reading every transformation by hand. Datatrail reads SQL and metadata only, so the connection stays read-only by design.

// CAPABILITY

What you get

Column-level lineage, built for data engineers and analytics teams

Field-level resolution

See that fct_orders.revenue is built from stg_stripe_charges.amount and stg_refunds.amount, with each derivation step shown in order.

SQL parsed, not guessed

Datatrail reads the actual SQL of each model and query, so renamed and casted columns like gross_amount to revenue are tracked correctly.

Answer "where does this come from"

When a stakeholder questions a metric, trace orders.revenue back to its raw source columns in seconds.

Pinpoint blast radius

Know exactly which columns depend on a source field before you change a type or drop it.

// 4 STEPS

How it works

From connected to saving in four steps

01

Connect read-only

Datatrail connects to your warehouse with a read-only role and pulls SQL definitions and query history. No data is moved or altered.

02

Datatrail maps columns

Each model and query is parsed to resolve column-level edges, so derivations are tracked field by field.

03

Trace a column

Pick orders.revenue and walk upstream to the exact source columns, or downstream to every report it feeds.

04

You act safely

Refactor or deprecate a column knowing precisely which fields and dashboards depend on it.

Why table-level lineage stops being enough

Table-level lineage tells you fct_orders depends on stg_stripe_charges. That is a useful sentence right up until you have a decision to make.

You are dropping stg_stripe_charges.currency_code. Table-level lineage says every one of the 40 downstream models is potentially affected, so you either notify all 40 owners and lose credibility, or you check by hand and lose a day. Column-level lineage says three models read that field and names them. That is the difference between a graph you look at and a graph you can act on.

The same gap shows up when someone questions a number. If revenue moved 4% overnight and the graph only resolves to tables, you are reading SQL to find out why. If it resolves to columns, you can see that fct_orders.revenue is stg_stripe_charges.amount minus stg_refunds.amount, and that the refunds source loaded twice.

How the column graph gets built, and where it gets hard

Datatrail parses the SQL, it does not infer from names. Every statement in your query history and every dbt model is parsed into an abstract syntax tree, and each output column is resolved back to the input columns that produced it. Renames, casts, joins, CASE expressions, and aggregations are all tracked as derivation steps, so gross_amount becoming revenue three models later is one continuous chain rather than two unrelated fields.

It is worth being honest about where column-level lineage is genuinely hard, because any vendor claiming 100% resolution is overselling:

  • SELECT * resolves only if the schema at parse time is known. Datatrail uses warehouse metadata to expand it, but a star select over a table that has since been dropped is unresolvable.
  • Dynamic SQL built by string concatenation in a stored procedure or Python job is not statically analyzable. Nobody can parse SQL that does not exist until runtime.
  • UDFs are opaque. If a Java or Python UDF transforms a value, the edge is recorded but the logic inside is not.
  • External transformations. If data leaves the warehouse, gets transformed in a Spark job, and comes back, the middle of that path is outside the query log.

The right question to ask any vendor, including us, is not whether they do column-level lineage. It is what happens to these four cases.

Where teams use it in practice

Three jobs come up constantly:

Before a refactor. Feed the column into impact analysis and get the list of models, exposures, and dashboards that read it, before you merge rather than after someone Slacks you.

During an incident. When a freshness alert or a schema drift alert fires, the column graph turns it into a root cause: not this table is stale, but this upstream field stopped loading and here are the six dashboards showing wrong numbers right now.

For deprecation. The column nobody reads is only safe to drop if you can prove nobody reads it. Query history plus the column graph is that proof.

If you want to see how this is drawn, the lineage diagram renders the same graph visually. For the wider landscape, see our comparison of data lineage tools or how we compare to OpenMetadata.

// FAQ

Questions people ask

Column-level lineage, answered

What is column-level lineage?

Column-level lineage traces an individual field, rather than a whole table, through every transformation between its source and where it lands. It resolves that fct_orders.revenue is derived from stg_stripe_charges.amount minus stg_refunds.amount, following renames, casts, joins, and aggregations along the way. Table-level lineage only says the two tables are related, which is not enough to decide whether a specific column is safe to change.

What is the difference between table-level and column-level lineage?

Table-level lineage records dependencies between tables: fct_orders reads from stg_stripe_charges. Column-level lineage records dependencies between fields: fct_orders.revenue reads from stg_stripe_charges.amount. The practical difference is precision. When you change one column, table-level lineage flags every downstream table as possibly affected, while column-level lineage names the handful that actually read that field.

How is column-level lineage generated?

By parsing SQL. Each statement in the warehouse query log and each dbt model is parsed into a syntax tree, and every output column is resolved back to the input columns that produced it. Some tools instead infer lineage from naming patterns or metadata fingerprinting, which is faster to build but weaker: it guesses at relationships rather than reading the code that created them. Datatrail parses the SQL that actually ran.

Does dbt support column-level lineage?

dbt Core does not. Running dbt docs generate gives you a model-level DAG built from ref and source calls, which is table-level. Column-level lineage is available in dbt Cloud on paid tiers via the dbt Explorer, scoped to your dbt project. Columns in raw landing tables that were never modeled, or produced by queries running outside dbt, fall outside that graph. Datatrail reads the dbt manifest and the warehouse query log together, so both are covered.

Can column-level lineage handle SELECT *?

Usually, but not always. Resolving a star select requires knowing the schema of the source table at the time the query ran, which Datatrail pulls from warehouse metadata to expand into the real column list. It breaks down when the source table has since been dropped or altered beyond recognition. Dynamic SQL assembled at runtime and logic inside opaque UDFs are the other genuine limits, and any tool claiming perfect resolution on those is overstating it.

See it map your lineage

Connect your warehouse read-only and get a full lineage map in minutes. Datatrail never moves or mutates your data.