SQL • DATA QUALITY AND DEBUGGING

Reconciling Aggregates — Reconcile aggregates across different query approaches (conceptual)

Ensuring that SUM, COUNT, and AVG produce consistent results regardless of how your query is structured.

Historical Context & Motivation

As relational database systems matured through the 1980s and 1990s, organizations began writing increasingly complex SQL queries to drive business-critical reports. Analysts quickly discovered a frustrating phenomenon: two queries that should logically return the same aggregate result — say, a total revenue figure — would sometimes produce different numbers depending on how the query was structured. The root causes ranged from subtle join fan-out effects to inconsistent handling of NULL values, duplicate rows, and filtering logic placement. The discipline of reconciling aggregates emerged as a systematic practice to detect and resolve these discrepancies, ensuring that data-driven decisions rest on a foundation of trustworthy numbers.

1970
Codd's Relational Model
E.F. Codd publishes his seminal paper on the relational model. Aggregation is implicit in relational algebra's projection and grouping operators, but the practical implications of duplicate rows and NULLs are not yet fully explored.
1986
SQL-86 Standard
The first ANSI SQL standard formalizes aggregate functions — COUNT, SUM, AVG, MIN, MAX — along with GROUP BY and HAVING clauses. The standard's handling of NULLs in aggregates introduces subtle behavior that will later cause reconciliation headaches.
1992
SQL-92 and Outer Joins
SQL-92 standardizes LEFT, RIGHT, and FULL OUTER JOINs. These operations introduce NULLs for non-matching rows, creating new scenarios where aggregates can diverge across different query formulations.
2003
Window Functions Enter SQL
SQL:2003 introduces window functions (OVER clauses), giving analysts a new way to compute aggregates without collapsing row-level detail. Reconciling window-function results against GROUP BY results becomes a new challenge.
2010s
Big Data and Data Quality Tooling
As data warehouses scale to billions of rows, automated data quality frameworks like Great Expectations and dbt tests codify aggregate reconciliation checks — comparing row counts, sums, and averages across staging layers.

The central question this lesson addresses is deceptively simple: why do two seemingly equivalent SQL queries sometimes return different aggregate values, and how can we systematically detect and resolve these discrepancies? Understanding the answer requires examining how joins, subqueries, CTEs, window functions, and filtering interact with aggregate functions at a conceptual level.

Core Principles of Aggregate Reconciliation

Aggregate reconciliation rests on several foundational principles that govern how SQL processes rows before, during, and after aggregation. Understanding these principles lets you predict when two query approaches will agree and diagnose why they sometimes do not. Each principle isolates a specific mechanism in the SQL execution pipeline where aggregate results can silently diverge.

1

Row Multiplicity

Joins can multiply rows when a one-to-many or many-to-many relationship exists. A fan-out inflates SUM and COUNT unless explicitly handled via DISTINCT or pre-aggregation.
2

NULL Propagation

SQL aggregate functions ignore NULLs (except COUNT(*)). Outer joins introduce NULLs for unmatched rows, which can cause COUNT(column) and COUNT(*) to diverge.
3

Filter Placement

A predicate in a WHERE clause eliminates rows before aggregation; the same predicate in a HAVING clause filters after aggregation. Moving a filter from a JOIN ON clause to WHERE changes outer join semantics.
4

Granularity Alignment

Aggregating at different levels of granularity and then joining can produce incorrect results. The grain of each intermediate result set must be explicitly understood and documented.
5

Semantic Equivalence

Two queries are semantically equivalent only if they produce identical results for every possible database state. Reconciliation verifies this empirically on current data and reasons about edge cases.
KEY TAKEAWAY
Think of aggregate reconciliation like balancing a checkbook against your bank statement. Both sources should reflect the same underlying transactions, but if you accidentally count a check twice (fan-out), miss a deposit (NULL filtering), or look at the wrong date range (filter placement), your totals will differ. The goal is not to pick a 'winning' number — it is to understand the data lineage well enough that both approaches provably converge on the same correct answer.

Visual Explanation — How Joins Affect Aggregates

The diagram above shows how joining Orders (3 rows) to Line Items (4 rows) produces a fan-out for order 101, which has two line items. The order-level amount of $500 is duplicated, inflating SUM(amount) from the correct $1,000 to an erroneous $1,500.

This diagram captures the single most common source of aggregate discrepancies: row multiplication through joins. When you join a parent table (Orders) to a child table (Line Items), each parent row is replicated once per matching child row. If you then compute SUM(orders.amount) on the joined result, the parent-level amounts are counted multiple times. A query that computes the same SUM directly on the Orders table — without the join — returns the correct value. The discrepancy is not a bug in SQL; it is a logical consequence of how joins expand the row set before aggregation occurs.

🔍 Diagnostic Tip
When you suspect fan-out, compare COUNT(*) and COUNT(DISTINCT parent_id) on the joined result. If they differ, fan-out is present and your parent-level aggregates are at risk of inflation.

How Aggregates Diverge — Mechanisms in Detail

Although aggregate reconciliation is primarily a conceptual and diagnostic skill rather than a mathematical one, it helps to formalize the key mechanisms that cause divergence. We can express each mechanism as a relationship between a correct aggregate value and the inflated or deflated value that a poorly structured query produces.

Mechanism 1: Fan-Out Inflation

FAN-OUT INFLATION
SUM_joined(amount) = Σᵢ (amountᵢ × mᵢ)
Where amountᵢ is the parent-level value for row i, and mᵢ is the number of matching child rows. The correct aggregate is Σᵢ(amountᵢ), which corresponds to the case where every mᵢ = 1.

Mechanism 2: NULL Exclusion

NULL EXCLUSION IN COUNT
COUNT(*) ≥ COUNT(column) when NULLs exist
COUNT(*) counts all rows including those where column is NULL. COUNT(column) skips NULLs. After a LEFT JOIN, unmatched rows carry NULLs in the right-side columns, so these two counts diverge.

Mechanism 3: AVG Distortion

AVG DISTORTION VIA FAN-OUT
AVG_joined = Σᵢ(amountᵢ × mᵢ) / Σᵢ(mᵢ) ≠ Σᵢ(amountᵢ) / n
The joined AVG weights each parent row by its child-row count mᵢ rather than treating each parent equally. This produces a weighted average instead of the intended simple average, distorting the result whenever child-row counts are uneven.

Mechanism 4: Filter Placement Divergence

Consider a LEFT JOIN between customers and orders. Placing a filter like orders.status = 'completed' in the ON clause preserves all customers (unmatched ones get NULLs for order columns). Moving the same filter to the WHERE clause eliminates customers with no completed orders entirely, because WHERE filters after the join and treats NULLs as not satisfying the condition. The resulting COUNT of customers will differ between the two approaches.

Common Divergence Patterns and Fixes

In practice, aggregate discrepancies cluster into a small number of recognizable patterns. The diagram below maps each pattern to its root cause and the recommended fix. Internalizing these patterns transforms aggregate debugging from trial-and-error into systematic diagnosis.

Five common aggregate divergence patterns with their root causes and recommended fixes. Patterns 1–3 (top row) are the most frequently encountered; patterns 4–5 (bottom row) arise in more complex multi-source queries.
Diagnostic reference table for the five divergence patterns
PatternSymptomDiagnostic QueryFix Strategy
Fan-OutSUM is larger than expectedSELECT COUNT(*), COUNT(DISTINCT pk) FROM joinedPre-aggregate child table in a CTE before joining
NULL MismatchCOUNT differs between queriesSELECT COUNT(*) - COUNT(col) AS null_countUse COALESCE or explicitly choose COUNT(*)
Filter MisplacementOuter join acts like inner joinMove filter between ON and WHERE; compare resultsFilters on the outer table go in WHERE; filters on the inner table go in ON
Grain MismatchAggregates inflated or reduced unpredictablyCheck row counts of each CTE/subquery independentlyEnsure each subquery returns one row per intended grain
DISTINCT OveruseSUM is smaller than expectedCompare SUM(val) vs SUM(DISTINCT val)Eliminate duplicates at the row level, not the value level

Worked Example — Reconciling Revenue Across Two Queries

Suppose we have three tables: orders (order_id, customer_id, order_total), line_items (item_id, order_id, product_id, quantity, unit_price), and products (product_id, category). A product manager asks for total revenue by product category. An analyst writes two queries that return different results. Let us walk through the reconciliation process.

Reconciling Total Revenue by Category
1
Step 1 — Identify the Two QueriesQuery A joins all three tables and computes SUM(orders.order_total) grouped by products.category. Query B computes SUM(line_items.quantity × line_items.unit_price) grouped by products.category. Query A returns $15,200 total; Query B returns $10,000 total.
2
Step 2 — Establish Ground TruthRun a simple SELECT SUM(order_total) FROM orders without any joins. This returns $10,000. This is the true total revenue at the order level. Query A's $15,200 is inflated; Query B's $10,000 matches the ground truth. We now know the discrepancy lies in Query A.
Ground truth total revenue: $10,000
3
Step 3 — Diagnose the Fan-OutRun SELECT COUNT(*), COUNT(DISTINCT order_id) FROM orders JOIN line_items USING(order_id) JOIN products USING(product_id). The result shows 150 total rows but only 80 distinct order_ids. Since 150 > 80, fan-out is present. Each order with multiple line items contributes its order_total multiple times to the SUM.
Fan-out ratio: 150 / 80 = 1.875×
4
Step 4 — Choose the Correct ApproachSince we want revenue by product category, the correct measure is the line-item-level calculation SUM(quantity × unit_price) from Query B. The order_total field is an order-level attribute and cannot be meaningfully distributed across categories via a join. Alternatively, if we need to use order_total, we must aggregate it in a CTE at the order grain before joining.
5
Step 5 — Validate the FixConfirm that SELECT SUM(quantity * unit_price) FROM line_items also returns $10,000 (matching the order-level ground truth). Then verify that the category-level subtotals from Query B sum to $10,000. If the line-item total differs from the order total, investigate discounts, taxes, or data entry errors as a separate reconciliation task.
Reconciled total: $10,000 ✓

Comparing Query Approaches for Aggregate Safety

Different SQL constructs carry different risks for aggregate correctness. The table below compares the major approaches — flat joins, subqueries, CTEs, and window functions — along dimensions that matter for reconciliation. Understanding these trade-offs helps you choose the right tool for a given analytical task and reduces the likelihood of introducing discrepancies in the first place.

Comparison of query approaches by aggregate reconciliation risk
ApproachFan-Out RiskNULL HandlingReadabilityBest For
Flat JOIN + GROUP BYHighNULLs from outer joins mix with real dataSimple for 1:1 relationshipsSingle-grain queries with verified 1:1 joins
Pre-aggregated CTELowControlled — aggregation happens before joinModerate — requires understanding CTE structureMulti-grain analytics with different fact tables
Correlated SubqueryLowIsolated — subquery runs per outer rowLower for complex logicAd hoc checks and small data sets
Window FunctionMediumOperates on the full row set; NULLs visibleHigh — preserves row-level detailWhen you need both detail and aggregate in one result set
KEY TAKEAWAY
Think of a pre-aggregated CTE as a mise en place step in cooking: you measure and prepare each ingredient (aggregate each fact table to the correct grain) before combining them in the pan (the final join). If you dump raw, unprepped ingredients together, you lose control over proportions. Similarly, if you join raw detail tables and aggregate afterward, you lose control over row multiplicity.

Connection to Advanced Data Quality Practices

Aggregate reconciliation is not merely a one-off debugging technique; it connects to broader data engineering and data governance practices. In production analytics environments, reconciliation checks are automated as part of data validation pipelines. Tools like dbt tests, Great Expectations, and Soda Core allow engineers to define assertions such as 'the sum of revenue in the staging table must equal the sum of revenue in the source system within a tolerance of 0.01%.' These automated checks instantiate the same conceptual principles we have discussed, but they run continuously as data flows through transformation layers.

From conceptual reconciliation to production data quality
Concept in This LessonAdvanced Practice
Comparing SUM across two queriesCross-system reconciliation between OLTP and OLAP databases
Checking COUNT(*) vs COUNT(DISTINCT pk)Automated uniqueness and row-count assertions in CI/CD pipelines
Pre-aggregating in CTEsDimensional modeling with conformed fact tables at defined grains
Diagnosing fan-out manuallyAutomated lineage tracking and impact analysis in metadata catalogs
Verifying filter placementQuery plan analysis and semantic layer enforcement in BI tools

As you advance into data engineering or analytics engineering roles, you will encounter the concept of a semantic layer — an abstraction that defines metrics (like 'total revenue') once and ensures every downstream query computes them consistently. Semantic layers are, in essence, an architectural solution to the aggregate reconciliation problem at scale. The conceptual skills you build here — reasoning about grain, join type, NULL behavior, and filter placement — are precisely the skills needed to design and maintain such layers.

Practice Problems

PROBLEM 1CONCEPTUAL
Explain why SUM(orders.amount) computed after joining orders to line_items in a one-to-many relationship will typically exceed the correct total. What is the name for this phenomenon, and why does it occur at the row level?
PROBLEM 2BASIC CALCULATION
A customers table has 500 rows. After a LEFT JOIN to orders, the result has 1,200 rows with 480 distinct customer_ids appearing in the orders.order_id column (the remaining 20 customers have no orders, so order_id is NULL for them). What values do COUNT(*), COUNT(order_id), and COUNT(DISTINCT customer_id) return on the joined result?
PROBLEM 3INTERMEDIATE
You need total revenue and total number of support tickets per customer. Revenue lives in the orders table and tickets live in the support_tickets table. A colleague writes a single query that joins customers to both orders and support_tickets, then computes SUM(order_total) and COUNT(ticket_id) in a single GROUP BY. Explain why both aggregates are likely wrong and describe a CTE-based fix.
PROBLEM 4APPLIED
A data engineer at an e-commerce company notices that a dashboard shows $2.3M in monthly revenue, but the finance team's ERP system reports $2.1M. Both numbers are derived from the same underlying transactions table. Outline a systematic reconciliation process with at least four specific diagnostic steps.
PROBLEM 5CRITICAL THINKING
Consider the expression SUM(DISTINCT amount) as a strategy for avoiding fan-out inflation. Under what circumstances does this approach produce the correct result, and under what circumstances does it silently produce an incorrect result? Construct a concrete example with specific data where it fails.

Lesson Summary

Reconciling aggregates is the practice of verifying that aggregate values — SUM, COUNT, AVG — remain consistent across different query formulations. The five primary causes of divergence are join fan-out (row multiplication inflating sums), NULL propagation (aggregate functions skipping NULLs introduced by outer joins), filter placement (ON vs. WHERE changing outer join semantics), grain mismatch (joining data aggregated at incompatible levels), and DISTINCT overuse (collapsing legitimately repeated values).

The systematic reconciliation workflow involves establishing a single-table ground truth, diagnosing the root cause with targeted checks like COUNT(*) vs COUNT(DISTINCT pk), and fixing the query — typically by pre-aggregating in CTEs before joining. These conceptual skills scale directly into production data quality practices: automated reconciliation tests, dimensional modeling at defined grains, and semantic layers that enforce metric consistency across an organization.

Varsity Tutors • SQL • Reconciling Aggregates