What this guide is for
Reconciliation answers one question: does the target hold what the source says it should? It sounds like one test. In practice it is four questions, and they have to be asked in order:
- Can the two sides be compared at all? Same columns, a key that identifies one row, no null keys.
- Is everything there? The same rows, counted overall, per segment and per key.
- Is anything extra? Rows in the target that the source never sent.
- Did anything change? Rows present on both sides whose values differ.
Asking them out of order produces noise. A full row compare on a table whose key is not unique reports thousands of mismatches that are really duplicate rows paired at random. A compare on a target that dropped a column fails with a SQL error instead of a finding. This guide walks through setting up the Key-Based Reconciliation pack so each question is asked at the right moment, and through reading the answers.
The pack works for any pair of tables whose column names match: staging and warehouse, an old platform and a new one, production and a restored copy, a nightly extract and its source. Keys can span several columns.
What the pack contains, template by template, is in Key-Based Reconciliation Test Pack. How to choose a key is in Reconciliation Key Finder.
How the templates work
Every template in the pack is a metadata-driven template test. It picks up the source tables you mark for reconciliation in the catalog, and materializes one child test per table. For each table, the template reads the column list from the catalog and sorts each column into one of three roles:
| Role | Meaning | Where it comes from |
|---|---|---|
| Key | Part of the row's identity. Rows are matched on all key columns together | The column's Reconciliation Role field set to Key, or, when the field is blank, a declared primary key |
| Value | Compared between the two sides | Reconciliation Role set to Value, or blank on a column that is not part of a declared primary key |
| Ignore | Left out of every comparison | Reconciliation Role set to Ignore |
The template then writes the same SQL against the source table and the target table. The target is the table named by the source table's Reconciliation Target Table field, in the schema named by its Reconciliation Target Schema field, or by the template's Target Schema variable when that field is blank.
So the keys and targets are catalog data, not template code. Change a table's key in the catalog, re-materialize, and every template picks it up. The SQL never needs editing for a new table.
Which tables are reconciled
No template lists tables. Each one selects with a metadata filter on the catalog:
- Fields version: every table whose Reconciliation Target Table is set.
- Tags version: every table with the Table Tag
Reconciliation Needed.
A table joins all fourteen templates as soon as it is marked, and the next materialization creates its child tests. A freshly imported pack selects nothing until you mark tables. Because the pack names no tables, it imports onto any data source without missing-metadata warnings.
Note: Child test SQL is written when you materialize. After you change a Reconciliation Role or Target value, materialize the templates again before running them, or the children keep the old key.
Two versions: fields or tags
Adding catalog custom fields needs Govern Edition. So the pack ships twice, with the same fourteen templates in each. They differ only in where they read roles and targets:
| Key-Based Reconciliation | Key-Based Reconciliation (Tags) | |
|---|---|---|
| Edition | Govern | Any, including QA |
| Column role | Reconciliation Role field: Key, Value or Ignore | Column Tag Reconciliation Role: Key, Reconciliation Role: Value or Reconciliation Role: Ignore |
| Tables reconciled | Every table whose Reconciliation Target Table is set | Every table with the Table Tag Reconciliation Needed |
| Target table | Reconciliation Target Table field. Required: it is also what marks the table | Table Tag Reconciliation Target Table: DIM_ACCOUNT. Optional: without it, the target has the source table's name |
| Target schema | Reconciliation Target Schema field | Table Tag Reconciliation Target Schema: STAR_SCHEMA |
| Also import | Reconciliation - Catalog Custom Fields | Nothing: Table Tags and Column Tags are standard fields |
| Template names | Reconciliation - Row Count per Key | Reconciliation - Row Count per Key (Tags) |
| Child test names | Source and target, for example Row Count per Key: S_DIM_ACCOUNT_10 -> STAR_SCHEMA.DIM_ACCOUNT |
Source only, Row Count per Key: S_DIM_ACCOUNT_10. The target is in the child test's description |
Choose the fields version on Govern Edition: dedicated fields are easier to read and to filter on, and they leave your tags alone. Choose the Tags version on QA Edition, or wherever adding catalog fields is not an option. On the sample warehouse the two produce identical results, template for template.
The rest of this guide says "set Reconciliation Role to Key". In the Tags version, read that as "add the Column Tag Reconciliation Role: Key".
Rules for the tags
- Opt in with
Reconciliation Needed. Every template selects the tables that carry this Table Tag, spelled exactly so. A table without it is not reconciled, whatever other reconciliation tags it has. Validatar's tag filters match whole tags, which is why the opt-in is its own tag and not part of the target tag. - Exact text.
Reconciliation Role:followed byKey,ValueorIgnore, with the capital letters and the single space after the colon as shown. A tag spelled any other way is not read. - One role tag per column, one target tag of each kind per table. If a column carries more than one role tag, Ignore wins, then Key.
- Other tags are fine. A column tagged
PIIandReconciliation Role: Keyis a key column. - No role tag means the fallback. Key when the column is part of a declared primary key, otherwise Value.
Before you start
- Edition. On Govern Edition, use the fields version with the Reconciliation Catalog Custom Fields. On QA Edition, use the Tags version. Neither version needs column roles on tables that declare a primary key: the declared key becomes the key and every other column a value. The tables still need marking (Step 2). SQL Server, PostgreSQL and Databricks ingest declared primary keys. Snowflake does not.
- Data sources. Each template runs its test side on a source data source and its control side on a target data source. The source data source's catalog must hold the source tables, because that is where the templates read columns, roles and targets. The target data source only has to reach the target tables, and it can be on a different platform. When one database holds both sides, use the same data source for both. The Key Finder runs both of its sides on the source.
- Metadata. Ingest metadata on the source data source so the catalog has the current columns.
- Permissions. Creating tests in the target project, and editing catalog metadata on the source data source to set roles.
Step 1: Import the fields and the pack
For the fields version:
- In Validatar, open Marketplace, find Reconciliation - Catalog Custom Fields, choose Get, then Import Directly. On Settings > Catalog Custom Fields, import the panel's section at the Table level and at the Column level (Import All once at each, then Save). Reconciliation Catalog Custom Fields has the detail.
- Back in Marketplace, find Key-Based Reconciliation and choose Import. Pick the project and map the pack's two data sources: Reconciliation Source to the data source that holds your source tables, and Reconciliation Target to the one that reaches your targets. Map both to the same data source when one database holds both sides. Choose a folder and commit.
For the Tags version, skip step 1, and in step 2 import Key-Based Reconciliation (Tags).
The templates arrive selecting nothing. Each one picks up the tables you mark in Step 2. To follow this guide on a training instance, mark the six SAMPLE_DW staging tables listed under What the sample shows.
Step 2: Mark the tables and point at the targets
Marking a source table is the whole selection. Every template picks it up at the next materialization.
- Fields version: open the source table in the catalog and set Reconciliation Target Table to the target's name, even when it matches the source's. Set Reconciliation Target Schema too when the target lives outside the template's Target Schema.
- Tags version: add the Table Tag
Reconciliation Needed. AddReconciliation Target Table: <name>when the target's name differs from the source's, andReconciliation Target Schema: <name>when its schema differs from Target Schema.
In the sample, STAGING.S_DIM_ACCOUNT_10 loads STAR_SCHEMA.DIM_ACCOUNT. In the fields version its Reconciliation Target Table is DIM_ACCOUNT. In the Tags version it carries Reconciliation Needed and Reconciliation Target Table: DIM_ACCOUNT. To mark many tables at once, use the Bulk Edit, Excel import or script described for roles in Step 4.
Then, on each template's Test Definition tab:
- Variables: set Target Schema to the schema the targets live in.
- Control data source: the import set it to the data source you mapped Reconciliation Target to. Change it only to point one template somewhere else.
Leave Object Selection and Definition as it is: its filter is what reads the mark. To narrow one template further, add a filter beside it, for example on Schema Name. Filters combine with AND.
Step 3: Find the keys
A keyed comparison is only as good as its key. Before any other template, run the Reconciliation Key Finder. For every marked table, it scores each way of choosing a key on the table's own data: the Reconciliation Role field, the declared primary key, the first column, and column names containing ID, KEY or the table name. Each strategy reports the columns it picked, how many key values repeat, and how many rows have a null in the key.
The Key Finder child test passes when the key the other templates will use is unique and never null. Before any key is set, every child fails with NO CANDIDATE on its first row. That is the expected starting point: it means no key is defined yet.
Read the candidate rows and pick the key that identifies the same business row on both sides. Three rules cover most decisions:
- Prefer the business key over a load surrogate. A surrogate generated during the load (a sequence, a hash added by the pipeline) may not mean the same thing on both sides. Use it only when both sides carry the identical value.
- Expect composite keys. In multi-tenant and merged data, an id is often unique only within its source. In the sample,
ACCOUNT_IDrepeats once inDIM_ACCOUNTbecause two source customers use the same account id. The key isSOURCE_CUSTOMERplusACCOUNT_ID. - Do not trust a strategy because it says UNIQUE. On
S_DIM_ACCOUNT_10, "column name contains ID" picksACCOUNT_ID,CLIENT_IDandETL_INSERT_BATCH_ID, and that combination is unique, but only because a load batch id is part of it. A key that includes load columns matches nothing on the target.
Reconciliation Key Finder covers the strategies, the result columns and how to add a strategy of your own.
Step 4: Set the roles
Mark the key columns Key. Then mark Ignore on every column that is expected to differ between the two sides: load timestamps, batch ids, the name of the file a row came from. Leave everything else blank, which makes it a value.
The Ignore step matters more than it looks. In the sample, S_DIM_CLAIM_10 and DIM_CLAIM hold identical claims, yet a full row compare that includes the three ETL_INSERT_* columns fails on every one of its 53,313 rows, because each layer stamps its own load time. With those columns marked Ignore, the same compare finds no differences.
There are three ways to set the fields.
Bulk Edit, for a few tables. Open the source table in the catalog, go to the Columns tab, select the key columns, choose Bulk Edit, set Reconciliation Role to Key and save. Do the same for the Ignore columns. The data source and schema Columns tabs offer the same Bulk Edit across tables.
Excel import, for many tables at once. On the source data source, open Custom Metadata, generate the import file, fill in one row per column (schema.name, table.name, column.name, reconciliation_role), and upload it. Role values must be exactly Key, Value or Ignore.
A custom metadata script, for rules you want to keep. When keys follow a naming convention, let a query set them, and run it again whenever new tables arrive. On the source data source, go to Custom Metadata > Metadata Sources > New Source > New Query Source, keep Same as primary, and put a query in the column-level script that returns schema.name, table.name, column.name and reconciliation_role. The query the sample uses, in Snowflake syntax:
-- Key: SOURCE_CUSTOMER, and <ENTITY>_ID where the table is S_DIM_<ENTITY>_10 or S_FACT_<ENTITY>_10
-- Ignore: the ETL_INSERT_* load columns
-- Columns with no rule are not returned, so their role stays as it is.
SELECT "schema.name", "table.name", "column.name", "reconciliation_role"
FROM (
SELECT c.table_schema AS "schema.name",
c.table_name AS "table.name",
c.column_name AS "column.name",
CASE WHEN c.column_name = 'SOURCE_CUSTOMER'
OR c.column_name = REGEXP_REPLACE(c.table_name, '^S_(DIM|FACT)_(.+)_10$', '\\2') || '_ID' THEN 'Key'
WHEN STARTSWITH(c.column_name, 'ETL_INSERT_') THEN 'Ignore'
END AS "reconciliation_role"
FROM INFORMATION_SCHEMA.COLUMNS c
WHERE c.table_schema = 'STAGING'
) roles
WHERE "reconciliation_role" IS NOT NULL
The table-level script of the same source can set the targets the same way, returning schema.name, table.name and reconciliation_target_table. Setting the target also marks the table:
SELECT t.table_schema AS "schema.name",
t.table_name AS "table.name",
REGEXP_REPLACE(t.table_name, '^S_(.+)_10$', '\\1') AS "reconciliation_target_table"
FROM INFORMATION_SCHEMA.TABLES t
WHERE t.table_schema = 'STAGING'
Tick Ignore Missing Metadata Objects, use Preview Script to check the rows, then save and Refresh Now. Return only rows that have a value: a role that is not one of the three options makes Validatar reject the run.
Setting tags in the Tags version
The same three ways work, with one difference that matters: an import replaces a column's or table's whole tag list. A custom metadata script or an Excel import that sets Reconciliation Role: Key on a column removes every other tag that column had. In testing, a key column tagged Customer Identifier came out of the sample script with only Reconciliation Role: Key. Columns the import does not return keep their tags.
- Bulk Edit is the safe way to add a role tag to columns that already carry other tags. Choose Add to Existing rather than replacing.
- In an Excel import, a cell can hold several tags as a list, for example
["PII", "Reconciliation Role: Key"]. Include the tags a column already has. - Use a script only on objects whose tags you own, or have it return the full tag list. For the Tags version, the column script returns
column_tagswith values such as'Reconciliation Role: ' || 'Key'. The table script returns both table tags in one value, as a list:
SELECT t.table_schema AS "schema.name",
t.table_name AS "table.name",
'["Reconciliation Needed", "Reconciliation Target Table: '
|| REGEXP_REPLACE(t.table_name, '^S_(.+)_10$', '\\1') || '"]' AS "table_tags"
FROM INFORMATION_SCHEMA.TABLES t
WHERE t.table_schema = 'STAGING'
Whichever way you choose, materialize the templates again, then run the Key Finder. Every child should now pass.
Step 5: Run the reconciliation in order
Run the templates as a ladder. Each rung assumes the one below it passed, and each is cheaper than the one above it.
| Rung | Templates | Question | What a failure means |
|---|---|---|---|
| 1. Comparable | Columns Align, Key Is Unique on Both Sides, Key Has No Nulls on Both Sides | Can the two sides be matched row for row? | A structural problem. Fix it before reading anything higher up |
| 2. Totals | Key Coverage Summary, Row Count per Segment | Do the totals agree? | Rows are missing or extra. The next rung says which |
| 3. Keys | Row Count per Key, Totals per Key | Which keys are missing, extra or duplicated, and whose numbers moved? | The failing rows name the keys |
| 4. Rows | Full Row Compare and its variants | Which values changed? | The failing rows name the key and the column |
Two reasons to keep the order:
- Rung 1 failures masquerade as data failures higher up. A missing target column becomes a SQL error on every row-level template. A duplicate key becomes hundreds of value mismatches, because a keyed comparison pairs duplicate rows in sorted order and cannot tell which belongs with which.
- Cost grows with every rung. The totals touch two numbers per table. The full row compare moves every column of every row from both sides. On large tables, run rungs 1 to 3 on every load and the row compares less often, or incrementally.
What the sample shows
On a training instance with the six sample tables marked and the roles from Step 4, the pack finds every real defect in the sample warehouse. Verified 2026-10-20:
| Source | Target | What the pack reports |
|---|---|---|
S_DIM_ACCOUNT_10 |
DIM_ACCOUNT |
Passes everywhere, on a two-column key |
S_DIM_CLAIM_10 |
DIM_CLAIM |
Passes everywhere, once the audit columns are ignored |
S_FACT_PAYMENT_10 |
FACT_PAYMENT |
Passes everywhere |
S_FACT_EXPENSE_10 |
FACT_EXPENSE |
19,556 keys missing from the target (65,959 rows against 46,403). Every count, total and row template fails. Rows are modified as late as 2029, so even the 7-day incremental compare finds them |
S_DIM_PROVIDER_10 |
DIM_PROVIDER |
679 keys missing from the target (779 rows against 100) |
S_DIM_POLICY_10 |
DIM_POLICY |
Columns Align fails: the target has no SOURCE_FILE. The count and total templates pass. Five row templates error on the missing column, until SOURCE_FILE is marked Ignore, after which they pass. Full Row Compare, Explicit Values Only passes throughout, because it never selects SOURCE_FILE |
The policy table is the ladder in miniature. Columns Align names the problem in one line, and that one line explains the errors in five other templates.
Reading the failures
| What you see | Usual cause | What to do |
|---|---|---|
| A template materializes no child tests | No table is marked, or the mark is misspelled | Set Reconciliation Target Table, or add the Table Tag Reconciliation Needed exactly as written. Materialize again |
| Key Finder fails with NO CANDIDATE | No key defined, and no declared primary key | Set Reconciliation Role to Key on the key columns, then re-materialize |
| Key Finder fails with NOT UNIQUE or HAS NULLS | The key you set does not identify one row, or has gaps | Add the missing key column, or fix the data. Check the candidate rows for a better key |
A child errors with No key columns on SCHEMA.TABLE |
The table has no key | As above. This is the templates refusing to compare without a key |
| Columns Align fails, naming a column | The target lacks a compared column (dropped, renamed, not yet deployed) | Fix the target, or mark the column Ignore on the source if the difference is intended |
Row templates error with invalid identifier |
Same as the line above | Same fix. Columns Align tells you which column |
| Key Is Unique fails | Duplicate keys on one or both sides | Fix the duplicates or the key. Do not read the row-level results until it passes |
| Key Coverage or Row Count per Segment fails | Rows missing or extra overall, or in one segment | Go to Row Count per Key for the exact keys |
| Row Count per Key fails on rows found only in the source | Rows the target never received | A load dropped them. The failing rows are the keys to reload |
| Row Count per Key fails on rows found only in the target | Rows the source no longer has, or a target that never deletes | Check how deletes travel through the pipeline |
| Row Count per Key fails with both sides present and different counts | A key duplicated on one side | Key Is Unique fails too. Fix that first |
| Totals per Key fails while Row Count per Key passes | The same rows, with different numbers | A value changed in transit: a transformation, a rounding, a type change |
| Full Row Compare fails on every row, in a few columns | Columns expected to differ, usually load metadata | Mark them Ignore, or list them in Full Row Compare, Named Columns Excluded |
| Full Row Compare fails on text that looks identical | Case or trailing spaces. The comparison is exact and case-sensitive | Use the Normalized Full Row Compare, or fix the pipeline if the difference is real |
| Incremental Full Row Compare passes with no rows | Nothing was modified inside the window, so there was nothing to compare | Widen Window Start, or rely on the count templates for coverage |
| Incremental Full Row Compare finds rows from the future | Future-dated watermarks fall inside every window | Treat them as a data issue in their own right |
Every reconciliation template stops reading after 100 failing rows (Abort Processing After Failures). A failing child therefore shows at most 100 rows. Use the count templates for the full size of a problem, and raise the limit on the template when you need every row.
Running it every day
- Put the cheap rungs in the load job. Columns Align, Key Is Unique, Key Coverage Summary and Row Count per Segment add little to a load and catch most failures. Run them right after the target is loaded.
- Run the row compares on a schedule that fits their cost. Daily with the Incremental Full Row Compare over the last day or week, and a full compare weekly or before a release.
- Materialize when the catalog changes. Newly marked tables, new columns and new roles reach the children only through materialization. Add a materialize step ahead of the runs in the job, or materialize after each metadata change.
- Route failures to the people who fix them. The Jira integration opens one issue per failing child and comments on it while it keeps failing.
- Keep the history. A reconciliation that passed yesterday and fails today is the most useful thing the pack produces. Retain results long enough to compare.
Best practices
- Keys are decisions. Record them once. Put them in the Reconciliation Role field, not in template copies. A custom metadata script that sets them from a rule is easier to review than hundreds of hand edits.
- Rerun the Key Finder after schema changes. A new column, a merged source or a changed load can make yesterday's key non-unique.
- Never compare load metadata. Load times, batch ids and file names differ by design. Mark them Ignore once, at the start.
- Reconcile at the grain the target stores. If the target aggregates the source, compare totals per key at the target's grain, not rows.
- Fix structure before data. Rung 1 failures first, always.
- Make every allowance explicit. If a known difference is acceptable while it is fixed, record it (an Ignore role, a template copy with a tolerance) rather than learning to ignore a red test.
- Expect exact comparisons. Values are compared exactly and text is case-sensitive. Normalize deliberately, with the Normalized Full Row Compare, when the two sides are meant to differ in format.
- Prove a migration on a sample first. Select a handful of tables, get every rung green, then widen the selection.
Platform notes
The templates are proven on Snowflake and written to run on SQL Server, PostgreSQL and Oracle too. Every identifier is double-quoted, numbers are cast to DECIMAL instead of rounded with a platform function, and every query reads from a table. The few settings that differ by platform are variables:
| Template | Variable | Default | SQL Server | PostgreSQL | Oracle |
|---|---|---|---|---|---|
| Columns Align | Metadata Columns View | INFORMATION_SCHEMA.COLUMNS |
Default | Default | ALL_TAB_COLUMNS |
| Columns Align | Metadata Schema Column | TABLE_SCHEMA |
Default | Default | OWNER |
| Incremental Full Row Compare | Window Start | CURRENT_DATE - 7 |
DATEADD(day, -7, CAST(GETDATE() AS DATE)) |
Default | Default |
| Normalized Full Row Compare | Timestamp Type | TIMESTAMP(0) |
DATETIME2(0) |
Default | Default |
- A variable applies to both sides. When the source and target run on different platforms, pick a value both accept, or copy the template and edit one side's SQL. Columns Align across platforms usually needs the copy, because each side reads its own metadata view.
- Names match exactly. Quoted identifiers are case-sensitive. The catalog records names as the database stores them (PostgreSQL folds unquoted names to lower case, Oracle and Snowflake to upper case), so this only matters when the source and target spell a name differently.
- Row Fingerprint per Key is Snowflake-only. It uses Snowflake's
HASH, which other platforms lack, and which returns different values for the same data held in different types. Both sides must be Snowflake.