TL;DR
dbt data quality means writing down what good data looks like for each dataset, then making dbt check it on every build. Data tests check values, unit tests check SQL logic, contracts check column shape and freshness rules check timing. Each check needs a severity that decides whether a failure blocks the build, raises a warning or only logs. Each critical check also needs a named owner who knows what to do when it fails. Because dbt only sees your data when it runs, it catches the problems you declared in advance. So add anomaly monitoring for the volume drops and drift that nobody predicted between runs.
Key Takeaways
Start from quality dimensions and the question each dataset must answer, then map each one to a single dbt control. Data tests check data, unit tests check logic, contracts check shape and freshness checks time, and none replaces another. Severity, warn_if and error_if thresholds and stored failures decide whether a failed check blocks, warns or only logs. Every critical check needs a named owner and a response rule, or it turns into noise people learn to ignore. dbt only sees data when it runs, so anomaly monitoring covers volume and distribution drift between runs. Measure coverage by the risks a test suite covers, never by the number of tests in the project.
Watch on YouTube
Snowflake Cortex for Data Quality: What ETL Tools Can’t Do (Demo)
Kanerika demos data quality checks that run inside Snowflake with Cortex, a useful look at what warehouse-side validation catches next to your dbt tests.
The Revenue Number That Passed Every Test
At 8:40 on a Monday, a finance lead opens the weekly dashboard and sees revenue up 9%. The overnight dbt build finished green. Every test passed, so nobody questions the number until the board deck has already gone out.
Two days later, an analyst traces it. A payment loader retried after a timeout and wrote Friday’s batch twice. The revenue model had a not_null test on almost every column and no uniqueness check on payment_id, so the duplicates went straight through.
That confident, wrong number is the problem dbt data quality work exists to solve. A passing build only proves that the checks you wrote passed.
If you need the background first, start with what dbt is and how it works . Fixing the gap starts with deciding which failures matter for each dataset, who hears about them and what dbt cannot see on its own.
What Does dbt Data Quality Actually Cover?
dbt data quality covers the expectations you can write down about a dataset and check while dbt builds it. That includes unique keys, allowed values, valid relationships between tables, column types and how recent the data is. It reaches accuracy only when you compare a model against a trusted source, and it never tells you whether a dataset is worth having.
Since dbt runs the data transformation step inside the warehouse, its checks see data after it lands and before people read it. That position is useful, because it sits between every source and every dashboard. It also means dbt sees nothing that happens outside a run.
Start With the Question Each Dataset Must Answer
Quality is easier to define from the consumer’s side. A finance mart answers “how much did we bill yesterday?”, so duplicates and late loads matter far more than a missing marketing attribute. A churn feature table answers a different question, and so it fails in different ways.
Writing that question down first keeps the test suite honest. Then every check traces back to a decision someone makes with the data, which also makes it easier to delete checks that protect nothing. An enterprise data quality framework sets these expectations across the company, and dbt is where many of them get enforced.
Map Each Quality Dimension to One dbt Control
dbt Labs groups data quality into seven data quality dimensions , namely usefulness, accuracy, completeness, consistency, uniqueness, validity and freshness. dbt Labs notes that most taxonomies leave usefulness out. Keeping it in is worth it, because a perfectly tested model that nobody reads is pure cost. The table below maps each dimension to the dbt control that fits it best and to the gap that control leaves.
Dimension Question it asks dbt control that fits What the control cannot see
Usefulness Does anyone rely on this dataset? Exposures and named owners, decided before any test Whether the numbers are right
Accuracy Does it match the real world? Singular reconciliation test against a source of truth Errors that already exist in the source
Completeness Did every expected row and field arrive? not_null tests, row-count comparisons, volume anomaly tests Partial loads that still look normal in size
Consistency Do related datasets agree? relationships tests, equal_rowcount, cross-model reconciliation Two tables agreeing on the same wrong value
Uniqueness Is each entity recorded once? unique or unique_combination_of_columns tests Near-duplicates that carry different keys
Validity Do values follow the business rules? accepted_values, expression_is_true, contracts with data types Valid-looking values that are still wrong
Freshness Is it recent enough for the decision? Source and model freshness Fresh data that is incomplete
Why Accuracy Is the Hardest Dimension to Test
Accuracy is the dimension dbt reaches least. A test can confirm that an amount is positive and carries a valid currency code.
Only a comparison with the billing system shows it is the correct amount. That is also where data integrity versus data quality matters, since integrity rules protect structure while quality rules protect fitness for a decision.
Before writing a single threshold, it also helps to look at real distributions. A quick round of data profiling shows which columns are truly never null and which codes actually appear. It also shows how much a daily row count normally moves.
The Five dbt Controls and the Risk Each One Covers
dbt gives you five ways to express an expectation, and each one runs at a different moment. Mixing them up is a common reason a project has hundreds of tests and still ships bad numbers. The comparison below sets them side by side, and then each one gets a short section of its own.
Control When it runs What it proves What it misses Typical failure it catches
Data tests (generic and singular) After a model builds, in dbt build or dbt test Rows meet a stated rule Rules nobody wrote Duplicate payment IDs
Unit tests Before the model is materialized, usually in CI SQL logic turns known inputs into expected outputs Problems in real production data A CASE branch that drops refunds
Contracts and constraints Before and during the model build Column names and data types match the agreement The values inside those columns A renamed or retyped column
Freshness Before the build or on a schedule Data has loaded recently enough Whether the load was complete A loader that stopped at 2 a.m.
Anomaly tests (package) Whenever dbt runs the test Volume or values sit inside a learned range Anything that happens between runs A sudden drop in daily rows
Data Tests: Generic and Singular
Generic tests are reusable assertions you attach to a model in YAML. Out of the box, there are four of them, namely unique, not_null, accepted_values and relationships.
The dbt data tests documentation calls all of them data tests. Their basic YAML, plus a table for deciding which models need them, lives in our guide to dbt best practices for production projects .
Singular tests are SQL files that return failing rows. They suit rules that only make sense for one model, such as a refund that should never exceed its original charge.
-- tests/assert_refund_not_above_charge.sql
select
r.refund_id,
r.amount as refund_amount,
c.amount as charge_amount
from {{ ref('stg_payments__refunds') }} as r
join {{ ref('stg_payments__charges') }} as c
on r.charge_id = c.charge_id
where r.amount > c.amount
If the query returns any rows, the test fails. For broader patterns that catch orphaned rows and broken references, see these SQL data integrity checks .
When the same singular test keeps reappearing with only a column name changed, the dbt docs recommend a custom generic test instead. You define it once in a {% test %} block, which works like a macro with arguments, and attach it by name in YAML.
Unit Tests Check Your SQL Logic
Unit tests arrived in dbt v1.8. They feed a model small, static input rows and compare its output with the rows you expect, before the model is built in the warehouse. That makes them the right tool for tricky CASE statements, date math and window functions.
They have limits, though. According to the dbt unit test docs , they cover SQL models in the current project only. Models that use materialized views, recursive SQL or introspective queries are not supported either.
Because they test logic against fixed inputs, run them in development and CI and exclude them from production builds. In the dbt CI setup guide , each pull request runs dbt build --select state:modified+, which builds and tests only changed models and their children.
Contracts and Constraints Check Shape
A model contract makes dbt verify column names and data types before it builds the model. Constraints such as not_null go one step further and ask the warehouse to enforce a rule on insert. The next section covers both, because what your warehouse enforces differs more than many teams expect.
Freshness Checks Time
Freshness rules compare the latest loaded timestamp against a warning window and an error window. They catch a loader that stopped, which no row-level test can see. After all, a test on yesterday’s data passes happily against a stale table.
Anomaly Tests Check Behaviour Over Time
Anomaly tests come from packages such as Elementary. Instead of a fixed rule, they learn a normal range for row counts or column values and fail when today falls outside it. They are still dbt tests, though, so they only run when dbt runs, and that detail shapes the whole observability question later in this guide.
Data Contracts in dbt: Agree on Shape Before the Data Arrives
A dashboard team builds on fct_revenue, and a month later an upstream change turns amount from a number into text. Without a contract, the model rebuilds happily and the breakage surfaces in someone else’s report. With an enforced contract, dbt stops the build because the shape no longer matches what was promised.
What an Enforced Contract Guarantees
Once a contract is enforced, the YAML must list every column with its name and data type, as the dbt model contracts guide explains. Before building, the model’s SQL output is compared against that list.
Any mismatch then fails the build. Contracts do not work on Python models, materialized views, ephemeral models or custom materializations. They usually guard the fct_ and dim_ marts that other teams query. Our guide to dbt data modeling shows how those marts are built from staging.
models:
- name: fct_revenue
config:
contract:
enforced: true
columns:
- name: revenue_id
data_type: varchar
constraints:
- type: not_null
- name: amount
data_type: number(18,2)
- name: revenue_date
data_type: date
A contract proves columns and types. It says nothing about the values inside them, so a contracted model still needs data tests for keys and business rules. The idea is close to the producer and consumer agreements behind a data mesh , scaled down to a single model. When a contracted column has to change type or disappear, publish a new major model version so consumers can move on their own schedule.
Constraints: What Your Warehouse Actually Enforces
Constraints look the same in YAML on every platform, yet each warehouse treats them differently. The dbt constraints reference lists the support per platform. On Snowflake , for example, a primary_key constraint is metadata, so it does not stop a duplicate row from landing.
Constraint Snowflake BigQuery Postgres
not_null Enforced Enforced Enforced
primary_key Defined, not enforced Defined, not enforced Enforced
foreign_key Defined, not enforced Defined, not enforced Enforced
unique Defined, not enforced Not supported Enforced
check Not supported Not supported Enforced
So keep the unique test on any Snowflake or BigQuery key, even when the contract declares a primary key. Teams picking platforms for a modern data stack should add this enforcement gap to their comparison, since it changes how many tests each model needs. The same applies when you weigh a data lakehouse versus a data warehouse for the layer dbt builds on.
Kanerika Service
Data Engineering Services
Automated tests on every build, data contracts enforced across workflows and quality checks on every pipeline, delivered through audit, build or ongoing support engagements.
Explore Data Engineering
Freshness: The Quality Dimension Many Teams Check Last
A table can pass every test and still be a day old. Freshness is the check that notices, and teams often add it only after their first stale-dashboard escalation.
Configure Freshness Against the Right Timestamp
Since v1.9, freshness settings sit under config on a source, per the dbt freshness reference . Each window takes a count and a period, and loaded_at_field names the column dbt reads to find the latest load. A filter also limits how much of a large table dbt scans.
sources:
- name: billing
config:
loaded_at_field: _loaded_at
freshness:
warn_after: {count: 4, period: hour}
error_after: {count: 8, period: hour}
filter: "_loaded_at >= current_date - 3"
tables:
- name: payments
- name: ledger_entries
Pick the timestamp the loader writes, rather than the business event time. An order placed last week but loaded an hour ago is fresh data, and confusing the two produces false alarms.
Set the windows from the consumer’s deadline as well. If finance needs numbers by 7 a.m., the error should fire early enough to fix the load before then.
In dbt v2, the dbt freshness command checks every source and model with freshness configured. Meanwhile, dbt source freshness remains as a legacy command that covers sources only. For checks that run in the warehouse itself, our guide to Snowflake data quality checks covers the native options.
Fresh Is Not the Same as Complete
Freshness only reads the newest timestamp. A loader that writes 200 rows instead of 20,000 at 6 a.m. looks perfectly fresh.
For that reason, pair freshness on critical sources with a volume check. That can be a row-count comparison against the source or an anomaly test that knows what a normal day looks like.
The same logic applies upstream of dbt. Teams that invest in data pipeline automation often have loaders that retry quietly, and a retry that half-succeeds is exactly the case freshness cannot see.
Severity, Thresholds and Ownership: Make Every Failure Mean Something
A test result is only useful if someone acts on it. Severity decides whether a failure stops the pipeline, and ownership decides who picks it up. Get both wrong and the project fills with warnings nobody reads, which is how a green build loses its meaning.
Error, Warn and the Thresholds in Between
Every data test has a severity of error or warn, and error is the default. The error_if and warn_if settings turn a single pass or fail into a range.
Both use conditions on the failure count, such as “>10” or “between 5 and 10”. As the dbt severity docs note, dbt checks error_if first and then warn_if, and with severity set to warn it skips error_if entirely.
models:
- name: fct_orders
columns:
- name: status
data_tests:
- accepted_values:
arguments:
values: ['placed', 'shipped', 'completed', 'returned']
config:
severity: error
error_if: ">100"
warn_if: ">10"
where: "order_date >= current_date - 3"
store_failures: true
Here, up to 10 bad statuses pass, 11 to 100 raise a warning and more than 100 fail the test. Thresholds belong on checks where a few bad rows are expected, such as late-arriving reference data. On keys, by contrast, keep the default of zero tolerance.
The choice has real consequences inside dbt build. A failing error-level test on a model makes everything downstream of it skip, as the dbt build command reference describes.
That is exactly what you want for a finance mart and painful for an exploratory model. So use warn when a failure should not stop the rest of the build.
Store Failing Rows for Triage, Carefully
With store_failures turned on, dbt writes the failing rows to an audit schema with a dbt_test__audit suffix by default. Each run replaces the previous results for that test, so treat the table as a triage view and keep history somewhere else. Failing rows can also contain customer or payment details, so restrict access to that schema the same way you restrict the source.
Name an Owner and a Response for Each Critical Check
dbt groups give models an owner with a name or email, plus optional meta such as data_owner, as the dbt groups documentation shows. That turns “who owns this?” from a chat thread into a field an alert can read. Deciding who gets named in the first place belongs to your data governance framework and to the people doing data stewardship day to day.
Response policy Blocks the build? Who hears about it Expected response
Error: a key, contract or reconciliation check fails on data people decide with Yes, downstream models skip Owning team’s on-call channel and the data owner Same business day, before the next consumer refresh
Warn: a value rule fails where a few bad rows are tolerable No Owning team’s channel Next working day, after reviewing stored failures
Warn with an error threshold on reference data Only above the error threshold Owning team’s channel Fix at the source within the sprint
Log only: a check that informs but protects no decision No Nobody is paged Review in a weekly quality check-in
Write the response next to the check, in meta or in the model description. Then the person who receives the alert does not have to guess.
Where ownership itself is unclear across teams, Kanerika’s data governance services start from that exact gap. Without an owner and a response rule, a check is just noise with extra steps.
Checklist
Enterprise Data Governance Checklist
A practical checklist for clear data ownership, consistent policies and governance processes, the groundwork that gives every critical dbt check a named owner.
Get the Checklist →
dbt Tests vs Data Observability: Where Tests Stop and Monitors Start
dbt tests answer a narrow question very well. Did the rules you declared hold at the moment dbt ran? Data observability answers a wider one, namely whether anything unusual happened to the data at any time, including things nobody thought to declare. The concepts are compared in data observability vs data quality , so this section stays with the practical question of where a dbt project hands off.
Known Failure Modes vs the Ones Nobody Predicted
A unique test exists because someone predicted duplicates. Plenty of incidents, however, come from failures nobody predicted. A vendor changes a code list, a partner sends half a file or a column’s distribution shifts after an app release.
Problem Caught by a dbt test? Caught by anomaly or observability monitoring?
Duplicate keys Yes, with a unique test Sometimes, as a volume spike
Schema drift on a source Partly, through contracts and schema-change tests Yes
Row-count drop Only with a volume or row-count test Yes
Distribution shift in a column Rarely, it needs a range or anomaly test Yes
Late data between scheduled runs No, dbt is not running Yes
Breakage in an upstream system Only when the next run reads it Yes, often earlier
Which dashboards are affected Partly, through exposures Yes, through lineage
That last row is easy to skip, and it shouldn’t be. Knowing that a test failed is half the job, and the other half is knowing which reports and teams sit downstream. That is what data lineage adds, whether it comes from dbt exposures or from a separate tool.
Anomaly Tests Inside dbt Still Run on dbt’s Schedule
According to the Elementary volume anomalies docs , the volume_anomalies test splits rows into daily buckets. By default, it learns from the last 14 days and checks the last 2, and both windows can be changed.
That is real anomaly detection , configured as an ordinary dbt test with arguments, severity and tags. Even so, it runs only when dbt runs. With a nightly build, a broken load at 9 a.m. waits until the next night to be noticed.
You can narrow that gap without a new tool. Tag the freshness and anomaly checks on critical sources. Then run them in a small separate job every hour or two, for example dbt test with a tag selector, while the full build stays nightly. The checks still only see data when that job runs, but a broken morning load is now noticed before lunch rather than the next day.
Warehouse-Native Checks and When an Observability Layer Pays Off
Some checks can live in the warehouse itself. Snowflake’s data metric functions monitor tables on their own schedule, although the Snowflake data quality documentation says they need Enterprise Edition.
An observability platform goes further, with automated monitors, lineage and incident history across sources dbt never touches. This round-up of data observability tools compares the main options.
Three signals usually justify that extra layer. Sources arrive from systems you do not control, consumers spot problems before the data team does, and incidents trace back to failures no test covered.
Until those signals appear, a well-owned dbt test suite plus a few anomaly tests covers most teams.
Packages That Extend dbt Data Quality Checks
Packages fill the gaps in the built-in tests. Choose them by the capability you are missing. Also check the Package Hub’s “v2 compatible” label before adding one to a project that plans to move to dbt v2.
Package What it adds Example tests Maintenance signal
dbt_utils (dbt-labs) General-purpose generic tests expression_is_true, unique_combination_of_columns, recency, equal_rowcount Version 1.4.1, marked v2 compatible on the Package Hub
dbt_expectations (metaplane/dbt_expectations) Range, pattern and distribution tests in the style of Great Expectations expect_column_values_to_be_between Version 0.10.10, marked v2 compatible; the original calogica repository has had no updates since December 2024
Elementary dbt package Anomaly detection and schema-change tests, plus a store for test results volume_anomalies, schema_changes Apache 2.0 and under active development in November 2026
If an existing project still references calogica/dbt_expectations, point it at the metaplane/dbt_expectations package instead. Also try any package test on a small model first, because some distribution tests scan whole tables and get expensive on large fact tables.
Worked Example: A Data Quality Plan for a Revenue Mart
Back to the revenue number from the start of this guide. Here is how a team might protect fct_revenue, a daily mart that feeds the finance dashboard and the board pack.
The Requirements, Dimension by Dimension
The finance team needs one row per payment, no negative amounts, data loaded by 6 a.m. and daily totals that match the billing ledger. Each requirement maps to one control. Uniqueness becomes a unique test, validity becomes a range rule, freshness goes on the billing source shown earlier and accuracy becomes a reconciliation test.
The YAML and One Reconciliation Test
The YAML below puts the mart in a finance group with a named owner, enforces its contract and attaches three tests. Notice that test inputs sit under arguments, while severity and store_failures sit under config.
groups:
- name: finance
owner:
email: [email protected]
config:
meta:
data_owner: Finance analytics
models:
- name: fct_revenue
config:
group: finance
contract:
enforced: true
columns:
- name: revenue_id
data_type: varchar
constraints:
- type: not_null
data_tests:
- unique
- name: amount
data_type: number(18,2)
data_tests:
- dbt_utils.expression_is_true:
arguments:
expression: ">= 0"
config:
severity: warn
store_failures: true
- name: revenue_date
data_type: date
data_tests:
- elementary.volume_anomalies:
arguments:
timestamp_column: revenue_date
config:
severity: warn
Accuracy needs its own singular test, because no YAML rule knows what the ledger says. This one compares daily mart totals with the source ledger over the last week.
-- tests/assert_revenue_matches_ledger.sql
with mart as (
select revenue_date, sum(amount) as mart_total
from {{ ref('fct_revenue') }}
where revenue_date >= current_date - 7
group by 1
),
ledger as (
select ledger_date, sum(amount) as ledger_total
from {{ source('billing', 'ledger_entries') }}
where ledger_date >= current_date - 7
group by 1
)
select m.revenue_date, m.mart_total, l.ledger_total
from mart as m
left join ledger as l
on m.revenue_date = l.ledger_date
where l.ledger_total is null
or abs(m.mart_total - l.ledger_total) > 0.01 * abs(l.ledger_total)
The test allows a 1% gap per day, which leaves room for refunds that are still settling. Once the team understands how the two systems differ, a tighter tolerance makes sense. This kind of comparison is the core of data reconciliation , and it is the one check in the plan that speaks to accuracy.
Kanerika Service
Klarity360: Data Validation AI Agent
Klarity360 automates data validation after migration with audit-ready reconciliation reports, the same source-to-target comparison as the ledger test above.
See Klarity360
What Happens When a Check Fails
Suppose the payment loader double-writes again. The unique test on revenue_id fails at error severity, so dbt build skips every model downstream of fct_revenue.
As a result, the dashboard keeps showing yesterday’s correct numbers. The finance group’s owner then gets the alert and can query the duplicates directly.
Other failures play out more quietly. Negative amounts raise a warning and land in the audit schema for a finance analyst to review.
A sudden drop in daily rows trips the volume anomaly test as a warning. The job also runs dbt freshness as its first step. A stale billing source fails there before dbt build starts, which saves compute and avoids a half-updated mart.
Compare that with the opening story. This time, the duplicate never reaches the board deck, and the first person to see the failure is the one who can fix it.
Common dbt Data Quality Mistakes
Test suites tend to go wrong in predictable ways. Each of these shows up in mature projects as well as new ones.
Tests that cannot fail. A not_null test on a column the loader always fills with a default value proves nothing.not_null on every column. It inflates the test count and buries the few checks that matter.Warnings nobody reads. When the warning channel gets hundreds of messages a week, every warning is effectively switched off.Full-table tests on huge fact tables. Use the where config to test recent partitions instead. In dbt v2, the batch-tests flag also groups default unique and not_null tests on the same model.Freshness on the wrong timestamp. Event time instead of load time creates false alarms or hides real delays.Counting tests instead of covered risks. Two hundred tests that miss duplicate payments are worth less than twenty that catch them.No owner. An alert that lands in a shared channel with no name attached is an alert nobody fixes.
The common thread is treating tests as a number to grow. The cost of bad data quality shows up in decisions made on wrong numbers. So measure coverage by the risks a suite actually covers.
Checklist
Data Engineering Checklist for Enterprise Teams
Pipeline, testing, orchestration and monitoring checks to review alongside your dbt test suite, so coverage follows real risks instead of test counts.
Get the Checklist →
How Kanerika Builds Data Quality Into Data Pipelines
Kanerika’s data engineering services treat quality as part of the pipeline rather than a later audit. In practice, quality checks run on every pipeline, lineage is tracked across data movements and issues are flagged before users notice.
The DataOps side matches the controls in this guide. Every pipeline change is reviewed, automated tests run on every build and data contracts are enforced across workflows.
Teams can engage Kanerika in three ways. Pipeline Audit and Design delivers a build-ready technical blueprint, while Build and Modernize rebuilds legacy ETL as tested, cloud-native pipelines. Ongoing Support extends your team with dedicated engineers who monitor pipeline health and data quality.
For migrations, Klarity360 automates data validation after migration with audit-ready reconciliation reports. That covers the step our guide on how to ensure data quality during data migration treats as the riskiest.
Validation at the point where data enters has worked for clients before. For Trax, a transportation spend management company that runs freight audit and payment services, Kanerika standardized invoice data exchange through data integration on Informatica B2B with automated validation.
Manual errors fell by 54% and invoice processing time by 39%. That was an Informatica project rather than a dbt one, yet the lesson carries over to any dbt project. Check data where it enters, and give every failure an owner.
Case Study
54% Fewer Errors With Informatica Data Integration
Trax standardized invoice data exchange on Informatica B2B with automated validation, cutting manual errors 54% and invoice processing time 39%.
Read the Case Study →
Running dbt Data Quality as a Discipline
Good dbt data quality is a habit more than a test count. Decide what each dataset must get right, and pick the one control that checks it.
Then give each failure a severity and an owner, and watch for what happens between runs. Teams that work this way still see failures. The difference is that a failure stops the right models, reaches the right person and never shows up first in a board deck.
Frequently Asked Questions
What is dbt data quality?
dbt data quality is the practice of writing down what good data means for each dataset and checking it while dbt builds your models. It combines data tests, unit tests, model contracts and freshness rules, gives each failure a severity and an owner, and adds anomaly monitoring for problems nobody declared in advance.
What is the difference between dbt unit tests and data tests?
Unit tests check SQL logic. They feed a model small, fixed input rows and compare the output with expected rows before the model is built, usually in CI. Data tests check real data after the model builds, for example that keys are unique or values are allowed. Most production projects need both kinds.
How do warn_if and error_if work in dbt?
Both are conditions on the number of failing rows a test returns, such as greater than 10. dbt evaluates error_if first and then warn_if, so one test can pass, warn or fail depending on the count. When a test has severity set to warn, dbt skips error_if and only evaluates warn_if.
Do dbt model contracts replace data tests?
No. A contract checks that a model returns the declared column names and data types before dbt builds it. It says nothing about the values inside those columns. Keys, allowed values and business rules still need data tests, and on Snowflake or BigQuery a declared primary key is not enforced, so keep the unique test.
Can dbt detect data anomalies?
Not on its own. Packages such as Elementary add anomaly tests that learn a normal range for row counts or column values and fail when new data falls outside it. These still run as dbt tests, so they only check data when dbt runs. Problems that start between scheduled runs need separate monitoring.
What is the difference between dbt tests and data observability?
dbt tests check rules you declared, at the moment dbt runs. Data observability watches data continuously for unusual volume, freshness, schema or distribution changes, including failures nobody predicted, and adds lineage and incident history. Teams usually start with owned dbt tests and add observability when many uncontrolled sources feed the warehouse or users spot issues first.
Which dbt version should a team use for data quality work in 2026?
dbt v2 has been generally available since 16 September 2026, and the free dbt distribution is the recommended v2 build. The Apache 2.0 dbt OSS build of v2 is still in alpha. Teams staying on v1 can use 1.12.6, the latest stable release, and should put test inputs under arguments either way.
How many tests should a dbt model have?
There is no fixed number. Start from the questions consumers ask of the model and the quality dimensions that matter for them, then add one control per real risk. A finance mart may need keys, ranges, freshness and reconciliation tests, while an exploratory model may need only a uniqueness check on its grain.
Where does dbt store failing test rows?
When store_failures is enabled, dbt writes failing rows to an audit schema with a dbt_test__audit suffix by default, and you can change the schema in config. Each run replaces the previous results for that test, so the table shows only the latest failures. Restrict access, because failing rows can contain sensitive data.