Datatrail
Blog / Guides 8 min read

Great Expectations vs dbt Tests: Which Data Quality Approach Fits

Last updated July 2026 · Datatrail

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

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

dbt tests and Great Expectations solve the same problem from different places. dbt tests are SQL assertions that live next to your models and run with your build, so they are the natural choice when your transformations are already in dbt. Great Expectations is a standalone Python framework with a much richer library of checks, and it validates data anywhere: pandas and Spark DataFrames, files, and warehouses, including at ingestion before dbt ever sees the rows. Most teams should start with dbt tests and add GX only where dbt genuinely cannot reach. Neither one gives you lineage, so neither can tell you what a failure or a schema change actually breaks downstream.

The one-line difference

A dbt test is a query that returns failing rows. If it returns none, the test passes. That is the whole model, and its simplicity is the point: tests are declared in YAML next to the model they protect, versioned in the same repo, and executed by dbt build in dependency order. Four generic tests ship in the box (unique, not_null, accepted_values, relationships), packages like dbt_utils and dbt_expectations add dozens more, and anything else is a singular test you write as a SQL file.

Great Expectations inverts the emphasis. An Expectation is a declarative assertion object drawn from a large gallery of prebuilt checks, grouped into an Expectation Suite, bound to data by a Validation Definition, and executed by a Checkpoint that can fire Actions such as a Slack alert. It is a Python library, so it runs wherever Python runs, and it reads data through pandas, Spark, or SQLAlchemy rather than requiring a warehouse.

Great Expectations vs dbt tests, side by side

 dbt testsGreat Expectations
What it isSQL assertions inside your dbt projectStandalone Python data validation framework
Where checks liveYAML and SQL in your dbt repoPython code plus a Data Context configuration
What it can validateWarehouse tables dbt builds or sourcesWarehouse tables, pandas and Spark DataFrames, files
When it runsDuring dbt build, in DAG orderAnywhere in a pipeline, via a Checkpoint
Breadth of built-in checksFour generic, many more via packagesLarge prebuilt Expectation gallery
Validates before transformationOnly via source freshness and source testsYes, at ingestion, before dbt runs
Setup costNear zero if you already run dbtA real integration project
ReportingRun results, or Elementary for historyData Docs, or GX Cloud
Data lineageModel-level DAG only, no column lineageNone

When dbt tests are the right answer

For most teams running dbt, most of the time. If the data you need to validate is already a dbt model or a declared source, a dbt test is cheaper in every dimension that matters. It costs one block of YAML, it runs automatically in the right order because dbt already knows the dependency graph, it fails the build before bad data propagates, and every analytics engineer on the team can read it without learning a second framework.

The coverage argument against dbt tests is weaker than it used to be. Between dbt_utils and dbt_expectations, which deliberately ports much of the GX gallery into dbt-native form, you can express distribution checks, row count comparisons between models, column type assertions, and regex matching without leaving dbt. If someone is proposing Great Expectations mainly for check variety, check whether dbt_expectations already covers the specific assertions you need. It usually does.

The real constraint is scope. dbt tests run against the warehouse, in dbt, as part of a build. Data that never becomes a dbt model is outside their reach.

When Great Expectations earns its place

Three situations make GX worth the integration cost.

  • Validation before the warehouse. If you want to reject a bad file or a malformed API response at ingestion, dbt is too late in the chain. GX validates a pandas or Spark DataFrame in flight, so the bad batch never lands. This matters most when the upstream source is outside your control, which is the usual case for data pulled from third-party APIs and structured data extracted from external web sources, where a vendor can change a field without telling anyone.
  • Data that is not in the warehouse at all. Spark jobs, ML feature pipelines, files in object storage. dbt has no opinion about any of it. GX does.
  • Complex custom logic in Python. Some business invariants are awkward to express as a SQL query that returns failing rows. If your rule needs real procedural logic, writing it as a custom Expectation is more honest than contorting it into a singular test.

Outside those cases, be skeptical. GX brings a Data Context to configure, Checkpoints to orchestrate, a second place for checks to live, and a second thing to keep in sync when a schema changes. That maintenance is the cost people forget when they compare a free framework to a free framework.

Can you use both together?

Yes, and the split that works is by pipeline stage rather than by preference. Use Great Expectations at the edges, validating raw data at ingestion before it lands and validating outputs that leave the warehouse for an ML pipeline. Use dbt tests in the middle, covering every model your project builds, because that is where they are nearly free.

The common failure is running both across the same warehouse tables. You end up with two sets of checks asserting overlapping things, two alerting paths, and two things to update when a column is renamed. Draw the boundary once and hold it. If you want dbt test results with history, trends, and alerting rather than just pass or fail in the logs, Elementary is built for exactly that and keeps everything inside the dbt project.

What neither of them can tell you

Both frameworks answer the same narrow question: is this data as I asserted it should be, right now? Both are blind to two questions that decide how much a failure costs you.

What does this column feed? When a test fails on a staging table, the useful next question is which downstream models and dashboards consume it. dbt knows its own model-level DAG, so it can tell you which models depend on which models, but it does not resolve dependencies column by column, and it stops at the edge of the project. GX has no lineage graph at all; it can emit OpenLineage events through integrations such as the Airflow operator, but the graph lives in some other system.

What will break if I change this? Tests are backward looking by design. They validate data that already exists. If you are about to drop a column, rename a field, or change a type, no test suite will hand you the list of downstream models, exposures, and dashboards that read it. That list is a lineage question, and it is the one that prevents the incident instead of catching it afterward.

There is also a coverage ceiling that is easy to miss. Both tools only protect what someone wrote a check for. The tables added last quarter, the raw landing tables nobody modeled, the field an analyst quietly started depending on: none of them are covered until a human notices. That is why automated data quality monitoring that baselines behavior from history has become the standard complement to assertion-based testing rather than a replacement for it.

Adding the missing layer

Datatrail sits underneath whichever testing approach you pick. It connects to Snowflake, BigQuery, Redshift, Databricks, or Postgres with a read-only role, parses the query history your warehouse already records along with your dbt manifest, and resolves the result into column-level lineage. Because it starts from query logs rather than the dbt project, the graph includes the raw landing tables and ad hoc queries that never became models, which is precisely the territory your tests do not cover.

That graph is what makes a failure actionable. A failing test becomes a failing test plus the named list of models, exposures, and dashboards downstream of it. Before you ship a change, impact analysis gives you the blast radius up front, and schema change alerts catch the upstream drift no assertion anticipated. If you are weighing the frameworks themselves, our Great Expectations alternative comparison covers where an assertion framework stops, and dbt lineage covers what dbt's own DAG does and does not give you.

The short version

If you run dbt and the data lives in your warehouse, use dbt tests, and reach for dbt_expectations before reaching for a second framework. Add Great Expectations where dbt structurally cannot go: validating raw data at ingestion, DataFrames in Spark or pandas, and custom logic that needs real Python. Split the two by pipeline stage rather than running both over the same tables, and if you are still choosing a framework, our comparison of data quality tools covers where each one fits. Then accept that neither gives you lineage, and pair whichever you choose with column-level lineage and impact analysis so a failing check comes with its blast radius and a risky change gets caught before it ships.

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.