Datatrail
Blog / Guides 10 min read

Snowflake Data Lineage: A Practical Guide to Tracing Data Flow

Last updated June 2026 · Datatrail

Lineage map
Lineage mapped from query history. Read-only connection.
0

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

You can trace Snowflake data lineage natively using three account-level views: SNOWFLAKE.ACCOUNT_USAGE.OBJECT_DEPENDENCIES for declared object relationships, ACCESS_HISTORY for read and write lineage including column-level access, and QUERY_HISTORY for the raw SQL that built each table. Combined, these can reconstruct how data flows from stg_stripe_charges through fct_orders to your dashboards, but each has real limits around latency, retention, and column-level resolution that determine how far a build-it-yourself approach gets you.

This is a practical guide to what each Snowflake view gives you, what you can assemble from them, where that effort stops paying off, and how to get automatic column-level lineage with freshness on top.

OBJECT_DEPENDENCIES: the declared graph

The most direct starting point is SNOWFLAKE.ACCOUNT_USAGE.OBJECT_DEPENDENCIES. It records dependencies between objects that Snowflake knows about structurally, primarily views depending on their underlying tables and views. If analytics.fct_orders is a view selecting from analytics.stg_stripe_charges, that edge shows up here cleanly.

The limitation is the same one every declarative source has: it only captures relationships Snowflake can infer from object definitions. A table built by a dbt model or a scheduled task running CREATE OR REPLACE TABLE fct_orders AS SELECT ... is not a view, so the dependency on stg_stripe_charges does not appear in OBJECT_DEPENDENCIES. For warehouses where most marts are materialized tables, which is most warehouses, this view covers only a slice of the real lineage. It is a good supplement, not a foundation.

ACCESS_HISTORY: read and write lineage, including columns

SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY is the most powerful native source. For each query it records the objects accessed (base_objects_accessed), the objects written (objects_modified), and, importantly, column-level detail. The objects_modified column includes a columns array, and ACCESS_HISTORY supports column lineage that maps which source columns contributed to which written columns.

This is the closest Snowflake comes to giving you column-level lineage out of the box. By querying ACCESS_HISTORY you can, in principle, learn that the job which wrote fct_orders.revenue read from stg_stripe_charges.amount. The practical caveats:

  • Latency and retention: ACCESS_HISTORY is part of ACCOUNT_USAGE, which has latency (often up to a few hours) and a retention window of 365 days. It is not real-time.
  • Edition and access: ACCESS_HISTORY requires Enterprise Edition and the right grants to query the SNOWFLAKE database.
  • Shape of the data: the lineage lives inside nested JSON arrays. Flattening objects_modified and joining column arrays across thousands of queries into a clean graph is a meaningful SQL and modeling exercise.

QUERY_HISTORY: the raw SQL fallback

QUERY_HISTORY (available in both ACCOUNT_USAGE and the INFORMATION_SCHEMA table function) gives you the actual query text for every statement. Where ACCESS_HISTORY does not cover a case, or where you want to parse intent directly, you can read the SQL that created a table and parse it yourself.

The trade-off is that you are now writing a SQL parser. To turn SELECT SUM(amount) AS revenue FROM stg_stripe_charges GROUP BY ... into a column edge from amount to revenue, you have to resolve aliases, CTEs, joins, and SELECT * expansions against live schemas, across every quirk of Snowflake SQL. The INFORMATION_SCHEMA table function also has a limited time window, so for historical coverage you lean on ACCOUNT_USAGE.QUERY_HISTORY with its latency.

What you can build yourself

Stitching these together, a capable team can build a respectable internal lineage tool:

  1. Use OBJECT_DEPENDENCIES for view-to-table edges.
  2. Flatten ACCESS_HISTORY.objects_modified and base_objects_accessed for table-level read and write lineage on materialized tables, with column detail where present.
  3. Fall back to parsing QUERY_HISTORY SQL for cases the above miss.
  4. Schedule a job to refresh the graph and store it somewhere queryable.

This genuinely works, and for some teams it is the right call, especially if you already maintain a modeling layer and treat the lineage tables as just another mart. The honest assessment of where it stops:

ConcernBuild it yourselfWhat you have to solve
Column-level resolutionPartial via ACCESS_HISTORYParse SQL for cases it misses
FreshnessLatency up to hoursNot suitable for real-time alerting
Retention365 daysExport to keep longer history
Downstream of SnowflakeNot coveredBI and reverse-ETL invisible
Staying currentSnapshot per refreshStale until next pipeline run

Where the do-it-yourself approach runs out

Three limits tend to push teams past the homegrown version. First, freshness: ACCOUNT_USAGE latency means your lineage cannot also be your alerting system for a table that stopped loading this morning. Second, the boundary: native views see lineage inside Snowflake, but the dashboard that breaks lives in your BI tool, outside Snowflake's view entirely. Third, maintenance: the parser and the flattening logic are code you now own forever, and they drift as your SQL patterns evolve.

Automatic column-level lineage with freshness

Datatrail is built to give you the lineage that ACCESS_HISTORY hints at, without the parser and the JSON flattening. It connects to Snowflake read-only, never moving or mutating data, reads your query history, and builds column-level lineage automatically, resolving the CTEs, joins, and SELECT * expansions that make hand-rolled parsing painful. Because it rebuilds from query logs, the lineage map stays current as your models change instead of going stale between refresh jobs.

On top of lineage, it adds the pieces ACCOUNT_USAGE latency makes awkward to do yourself: freshness monitoring for the table that quietly stopped loading, and impact analysis so you can see every downstream column and dashboard before you rename orders.revenue or refactor stg_stripe_charges.

If you run a Snowflake warehouse and have hit the wall where ACCESS_HISTORY and QUERY_HISTORY get you most of the way but not to a current, column-level graph, you can see Snowflake lineage in Datatrail and map your own account on a live platform.

See how your data flows, end to end

Connect your warehouse read-only and map lineage, freshness, and downstream impact before a change breaks a dashboard. Transparent pricing, no card to start.