0%
Data-Intensive Applications
Foundations of Data Systems
Distributed Data
Encoding and Evolution
Batch Processing
Stream Processing
Operational Patterns
Data Lineage and Cataloging
As organizations grow, data flows through dozens of pipelines, transformations, joins, and aggregations before reaching a dashboard or model. Without lineage, nobody can answer the simplest questions: "Where did this number come from?" "Which source table feeds this metric?" "If I change column X, what breaks downstream?"
Those questions are asked in anger in four places covered elsewhere: when a producer proposes a change and someone must say who it breaks, in schema evolution and compatibility; when a deletion request has to reach every copy of a record, in privacy and compliance; when a derived store has to be rebuilt and someone must establish what it was derived from, in derived data systems; and when an access control decided in one system has to be honored in a copy of the data, in data security and access control.
Data lineage is the record of how data moves and transforms from source to destination. It captures the full path: which systems produce the data, which pipelines transform it, which tables store intermediate results, and which dashboards or models consume the final output. Lineage turns an opaque pipeline into a traceable graph where every output can be traced back to its inputs.
Why does this matter? Because without lineage, every schema change is a gamble, every compliance audit is a scramble, and every debugging session starts from zero. Lineage is the infrastructure that makes data organizations manageable as they scale past a handful of pipelines.
Think of lineage as version control for data flow. Just as Git tracks which developer changed which line of code and why, lineage tracks which pipeline changed which data and how. A codebase without version control is manageable at 100 lines but chaotic at 10,000. Similarly, a data estate without lineage is manageable at 10 tables but ungovernable at 1,000. The investment in lineage pays off precisely when the organization scales past the point where anyone can keep the full picture in their head.
The analogy extends further: just as you would never deploy code without reviewing the Git diff to see what changed, you should never deploy a data pipeline change without reviewing the lineage graph to see what is affected. Lineage makes "code review for data" possible. Without it, pipeline changes are deployed blind, and their downstream impact is discovered only after the fact, usually when a dashboard breaks or an analyst reports wrong numbers.
There are four levels of lineage, each answering a different question at a different granularity.
Column-level lineage
Column-level lineage tracks individual fields through every transformation. If a dashboard shows gross_margin, column-level lineage tells you that gross_margin = revenue - cost_of_goods_sold, that revenue comes from sales.fact_orders.total_amount, and that cost_of_goods_sold comes from inventory.dim_products.unit_cost multiplied by sales.fact_orders.quantity.
This is the most granular and most valuable form of lineage. When a data engineer renames a column or changes a transformation, column-level lineage shows exactly which downstream reports, features, and models depend on that column. Without it, you are guessing.
Column-level lineage is hard to build automatically because it requires parsing SQL queries, Spark jobs, and custom Python scripts to understand which output columns derive from which input columns. Tools like DataHub and SQLGlot can parse SQL SELECT statements and extract column-level dependencies. For custom Python transformations, teams often annotate lineage manually or use framework-level hooks (Airflow task metadata, dbt model references).
Consider a concrete example. A dbt model computes monthly_revenue as SUM(fact_orders.total_amount) WHERE order_date >= DATE_TRUNC('month', CURRENT_DATE). Column-level lineage captures three things: the output column (monthly_revenue), the input column (fact_orders.total_amount), and the transformation applied (SUM with a date filter). If a data engineer changes total_amount to exclude shipping costs, column-level lineage immediately surfaces every dashboard and model that consumes monthly_revenue, because the change propagates through the lineage chain.
Why column-level lineage is worth the investment. The effort to build column-level lineage is significant (SQL parsing, framework instrumentation, manual annotation for non-SQL pipelines), but the return is disproportionate. A single incident where a column rename breaks a regulatory report can cost more in engineering hours, compliance fines, and executive trust than the entire lineage infrastructure. Column-level lineage also accelerates onboarding: a new data engineer joining the team can trace any metric back to its source columns without reading pipeline code or asking colleagues.
Column-level lineage and data quality testing. Column-level lineage enables targeted data quality testing. Instead of running quality checks on every column of every table (expensive and noisy), you can focus checks on the columns that feed critical business metrics. If monthly_revenue on the executive dashboard derives from exactly three source columns, you test those three columns rigorously (null checks, range validation, distribution monitoring) and skip quality checks on the hundreds of other columns that do not affect critical outputs. Lineage-driven test prioritization reduces the cost of data quality monitoring while concentrating coverage where it matters most.
Table-level lineage
Table-level lineage tracks dependencies between datasets as a directed acyclic graph (DAG). Node A depends on nodes B and C means that the pipeline producing table A reads from tables B and C. Table-level lineage answers: "If table B is late, which downstream tables are affected?" and "How many hops exist between the raw source and the final report?"
The depth of the lineage graph matters. A table that is 2 hops from the raw source is relatively easy to debug: you check two transformations. A table that is 8 hops deep has accumulated transformations, filters, joins, and aggregations across 8 stages, and tracing a data quality issue back to the root cause requires walking through all of them. Deep lineage chains are a code smell in data pipelines: they increase debugging time, reduce transparency, and make impact analysis more complex. Teams should aim to keep the maximum hop count under 5-6 for critical business metrics.
The fan-out of the lineage graph also matters. A single source table that feeds 50 downstream tables has a high fan-out, meaning a change to that source table has an outsized blast radius. High-fan-out tables should be treated as critical infrastructure: changes require careful impact analysis, coordinated rollouts, and explicit approval from downstream consumers. Monitoring the fan-out of each table helps teams identify which datasets are most critical to the organization, even if nobody explicitly labeled them as such.
Most orchestration tools (Airflow, Dagster, Prefect) produce table-level lineage automatically from DAG definitions. If Task 2 reads from the output of Task 1, the dependency is explicit in the DAG. The challenge is cross-system lineage: data flows from a Kafka topic to a Spark job to a Snowflake table to a Looker dashboard. No single tool sees the full path. Stitching lineage across system boundaries requires a centralized metadata platform.
Table-level lineage is also the foundation for scheduling and SLA management. If table C depends on tables A and B, and table A has a freshness SLA of 6 hours, then table C cannot guarantee anything better than 6 hours plus its own processing time. The lineage graph makes these cascading SLA constraints visible. A common mistake is setting aggressive SLAs on downstream tables without accounting for the latency of their upstream dependencies.
Operational lineage
Operational lineage captures runtime information: when did this pipeline last run, how long did it take, how many records did it process, did it succeed or fail? This is lineage enriched with execution context.
Operational lineage is essential for debugging. When a dashboard shows stale data, operational lineage tells you that the upstream pipeline failed at 3am with a timeout error, processed zero records, and has not recovered. Without operational lineage, you see stale data and start investigating from scratch: is the source down, is the pipeline broken, is the warehouse overloaded? With it, you jump directly to the failed pipeline run and read the error log.
Operational lineage also enables trend analysis. If a pipeline that normally processes 1 million records suddenly drops to 500,000, the lineage system can flag the anomaly even though the pipeline technically succeeded. Record count trends, processing duration trends, and error rate trends are all operational lineage signals that catch data quality issues before they reach dashboards.
Latency tracking is another dimension of operational lineage. How long does it take for a source event (a user places an order) to appear in the final dashboard? End-to-end latency is the sum of ingestion delay, pipeline execution time, and any queue wait times in between. Operational lineage measures latency at each hop in the pipeline, so when end-to-end latency increases, you can pinpoint which stage is responsible. Without this, debugging latency spikes requires manually measuring each stage, a process that takes hours for complex pipelines with many hops.
Business lineage
Business lineage maps business terms to physical tables and columns. When a product manager says "revenue," they mean a specific business concept. Business lineage maps that concept to the physical column sales.fact_orders.total_amount, the transformation that calculates it (sum of line items minus refunds, converted to USD at the daily exchange rate), and the authoritative dashboard where it is displayed.
Business lineage prevents the "which revenue?" problem. In a large organization, five different teams might have five different definitions of "revenue." One includes refunds, another excludes them. One uses the booking date, another uses the settlement date. Business lineage makes these definitions explicit, traceable, and auditable. When the CFO asks "why does revenue differ between the finance dashboard and the product dashboard," business lineage provides the answer in minutes instead of days.
Building business lineage requires collaboration between data engineers and domain experts. The data engineer knows the physical schema. The domain expert knows the business meaning. A glossary that maps "revenue" to sales.fact_orders.total_amount (with the precise calculation logic documented) bridges this gap. Tools like Atlan and DataHub support business glossaries that link terms to physical assets, creating a searchable layer of business context on top of the technical lineage graph.
How lineage is collected
Lineage can be collected in three ways, each with different trade-offs.
Parse-based lineage extracts dependencies by parsing SQL queries, dbt models, and Spark job definitions. This is accurate for SQL-based pipelines (dbt, Snowflake views, BigQuery scheduled queries) because the transformation logic is declarative and parseable. dbt is particularly well-suited for parse-based lineage because every model explicitly declares its dependencies via ref() and source() macros, making the lineage graph a first-class artifact of the codebase. Parse-based lineage struggles with imperative code (Python scripts, Java jobs) where the transformation logic is embedded in general-purpose code that is difficult to analyze statically.
Runtime-based lineage captures dependencies by observing actual data flow at execution time. The pipeline framework logs which tables each job reads from and writes to during execution. This captures dependencies that static parsing misses (dynamic SQL, conditional branches, table names constructed from variables) but only records lineage for paths that actually execute. A branch that runs only on the first of each month will not appear in lineage until that branch executes. OpenLineage is an open standard for runtime lineage collection. It defines a common event format that pipeline frameworks (Airflow, Spark, dbt) emit during execution, and lineage platforms (DataHub, Marquez) consume. Using OpenLineage avoids vendor lock-in: you can switch lineage platforms without re-instrumenting your pipelines.
Manual lineage is annotated by engineers through configuration files, metadata APIs, or UI tools. This is the fallback for systems that cannot be parsed or instrumented: legacy stored procedures, third-party ETL tools with opaque internals, and manual data entry processes. Manual lineage is labor-intensive and degrades over time as the system evolves, but it is sometimes the only option. The best practice for manual lineage is to co-locate the annotation with the code: a YAML sidecar file next to the stored procedure, or an API call embedded in the job's startup code, so the lineage is updated whenever the code is deployed.
Most organizations use a combination: parse-based for SQL, runtime-based for Spark and Python, and manual annotation for legacy systems and external data feeds.
An OpenLineage event is worth looking at directly, because the standard is small enough to read in one sitting and that is the reason it has been adopted:
Three design decisions in that payload do most of the work. The runId is minted before the job starts and repeated on the START, COMPLETE and FAIL events, so a lineage platform can record an attempted edge that failed rather than silently omitting it, which is exactly the blind spot discussed below. The columnLineage facet lives on the output rather than the job, which is what makes column-level lineage composable across tools that know nothing about each other. And everything beyond the four required fields is a facet, so a vendor can add its own without breaking a consumer that ignores it.
The event is emitted by the framework, not by your code. In practice that means one line in an Airflow or Spark configuration, and the honest consequence is that your lineage coverage is exactly the set of frameworks you have configured, which is the number the completeness section below asks you to measure.
Lineage storage and graph models
Lineage data is naturally a graph: nodes are datasets (tables, columns, dashboards) and edges are dependencies (reads from, writes to, derived from). Most lineage platforms store this graph in either a relational database with recursive queries (OpenMetadata), a graph database like Neo4j (some DataHub deployments), or an in-memory graph built from a metadata event stream (DataHub's default architecture).
The choice of storage affects query performance. Finding all downstream consumers of a table requires a graph traversal (breadth-first or depth-first from the starting node). In a relational database, this is a recursive CTE that can become slow for deep graphs (10+ hops). In a graph database, traversals are native operations and remain fast regardless of depth. For organizations with fewer than 10,000 tables, the relational approach is usually sufficient. For larger data estates, a graph database or a pre-computed lineage index provides better query latency.
The relational version of "everything downstream of this table" is a recursive CTE, and it is short enough that the graph-database argument should be made on measurements rather than on reflex:
The cycle guard is not defensive programming. Real lineage graphs contain cycles: a table that reads yesterday's version of itself, a reverse-ETL job that writes a metric back into the source system, a dbt model with an incremental self-reference. Without the path check this query does not return a wrong answer, it runs until the connection dies, and it will do so for the first time in production against the one table that has the cycle.
The performance argument has a shape worth knowing. With average fan-out the number of nodes reachable within hops is bounded by
so cost is exponential in depth, not in the size of the estate. At a six-hop traversal touches at most 1,093 nodes and a twelve-hop traversal at most 797,161. This is why the 10,000-table threshold quoted above is a poor predictor on its own: a 50,000-table warehouse of shallow marts traverses faster than a 5,000-table one with a twelve-layer dbt dependency chain. Measure your depth distribution before choosing a store.
The graph model also determines what queries are possible. A simple directed graph (table A depends on table B) supports basic impact analysis. A property graph (edges annotated with transformation type, column mappings, and freshness) supports richer queries: "Show me all datasets that derive the email column from the users table through any transformation chain." Column-level lineage requires a property graph because the column mapping information must be stored on the edges, not just the nodes.
Lineage completeness and trust
A lineage graph is only as useful as it is complete. If 80% of your pipelines emit lineage but 20% do not (legacy jobs, third-party integrations, manual data loads), the graph has blind spots. An impact analysis that misses 20% of downstream consumers gives false confidence: the engineer thinks they have identified all affected assets, but the unlisted 20% break silently after deployment.
Measuring lineage completeness requires comparing the lineage graph against the actual set of tables and pipelines in your data estate. If you have 1,000 tables in Snowflake but your lineage graph only contains 700 nodes, your coverage is 70%. The missing 300 tables represent blind spots. Tracking this coverage metric over time and targeting 95%+ coverage ensures that impact analysis results are trustworthy.
Incomplete lineage is worse than no lineage in one specific way: it creates false confidence. An engineer who runs impact analysis and sees "0 downstream consumers" might assume the table is safe to delete, when in reality the downstream consumers exist but are not captured in the lineage graph. At least without any lineage, the engineer knows they are guessing and proceeds with caution. With partial lineage, they trust the (incomplete) results and break things they did not know about. This is why lineage coverage measurement and transparency about blind spots are essential governance practices.
Mid-level engineers should understand table-level lineage and use it for impact analysis. Senior engineers build column-level lineage into their pipeline frameworks and enforce it in CI/CD. Staff engineers design the organization-wide lineage strategy, choosing between buy (DataHub, Atlan) and build, and establishing the governance process that keeps business lineage accurate as definitions evolve.