How To Compare Data In Two Tables In Sql

8 min read

Comparing data between two tables is a fundamental skill for database administrators, data analysts, and backend developers. Whether you are validating a migration, reconciling financial records, identifying duplicates, or auditing changes between a staging and production environment, knowing how to compare data in two tables in SQL efficiently saves hours of manual work. This guide explores the most effective techniques, ranging from basic set operations to advanced window functions, ensuring you can choose the right tool for your specific scenario.

Understanding the Core Scenarios

Before writing a query, you must define what "compare" means for your specific use case. Generally, there are three distinct outcomes you are looking for:

  1. Exact Matches (Intersection): Rows that exist identically in both tables.
  2. Differences (Symmetric Difference): Rows that exist in one table but not the other, or rows where the primary key matches but column values differ.
  3. Discrepancies in Specific Columns: The keys match, but you need a detailed report showing exactly which columns have changed values.

The structure of your tables—specifically the presence of a Primary Key or a unique composite key—dictates the best approach. If a reliable key exists, JOIN operations are usually superior. If no key exists, or you are comparing the entire row as a unit, Set Operators (EXCEPT, INTERSECT, UNION) are often cleaner It's one of those things that adds up. Surprisingly effective..

This changes depending on context. Keep that in mind Most people skip this — try not to..

Method 1: Using Set Operators (EXCEPT and INTERSECT)

The ANSI SQL standard provides set operators designed specifically for row-level comparison. These are the most concise syntax for "whole row" comparisons Most people skip this — try not to..

The EXCEPT Operator (Finding Differences)

EXCEPT returns distinct rows from the first query that are not output by the second query. To find all differences between Table A and Table B, you must run it in both directions.

-- Rows in Table_A but not in Table_B
SELECT * FROM Table_A
EXCEPT
SELECT * FROM Table_B;

-- Rows in Table_B but not in Table_A
SELECT * FROM Table_B
EXCEPT
SELECT * FROM Table_A;

Pros: Extremely readable; handles NULL values correctly (treats NULL = NULL as true for comparison purposes, unlike standard = operators). Cons: Requires identical column order, count, and compatible data types in both tables. It returns distinct rows, potentially hiding duplicate count discrepancies. Performance can degrade on massive datasets without proper indexing because it often implies a sort operation And that's really what it comes down to..

The INTERSECT Operator (Finding Matches)

If you simply need to verify which records are perfectly identical:

SELECT * FROM Table_A
INTERSECT
SELECT * FROM Table_B;

This is useful for a quick "sanity check" count: SELECT COUNT(*) FROM (SELECT * FROM A INTERSECT SELECT * FROM B) The details matter here..

Method 2: The FULL OUTER JOIN Approach (The Gold Standard)

For production-grade auditing, a FULL OUTER JOIN on the Primary Key is the most strong method. It preserves all rows from both sides, allowing you to see matches, orphaned records in A, orphaned records in B, and value mismatches in a single result set Surprisingly effective..

Not the most exciting part, but easily the most useful.

Assume id is the Primary Key.

SELECT 
    COALESCE(a.id, b.id) AS id,
    CASE 
        WHEN a.id IS NULL THEN 'Only in Table_B'
        WHEN b.id IS NULL THEN 'Only in Table_A'
        WHEN (a.col1, a.col2, a.col3) <> (b.col1, b.col2, b.col3) THEN 'Value Mismatch'
        ELSE 'Match'
    END AS comparison_status,
    a.col1 AS a_col1, b.col1 AS b_col1,
    a.col2 AS a_col2, b.col2 AS b_col2
FROM Table_A a
FULL OUTER JOIN Table_B b ON a.id = b.id
WHERE a.id IS NULL 
   OR b.id IS NULL 
   OR (a.col1, a.col2, a.col3) <> (b.col1, b.col2, b.col3);

Handling NULLs in Joins

Standard equality checks (a.col1 = b.col1) fail if either value is NULL because NULL = NULL evaluates to UNKNOWN (false) in SQL. The row-value comparison (a.col1, a.col2) <> (b.col1, b.col2) used above handles NULL semantics correctly in most modern databases (PostgreSQL, SQL Server, Oracle). In MySQL, you may need to use the null-safe operator <=> or explicit IS NOT DISTINCT FROM logic (e.g., NOT (a.col1 <=> b.col1)).

Method 3: UNION ALL with Aggregation (High Performance)

On very large tables (millions of rows), FULL OUTER JOIN can be memory-intensive because the engine must match every row. A UNION ALL + GROUP BY pattern is often significantly faster because it turns the problem into a single scan and aggregation.

SELECT 
    id,
    MAX(col1) AS col1,
    MAX(col2) AS col2,
    COUNT(*) AS row_count,
    SUM(CASE WHEN source = 'A' THEN 1 ELSE 0 END) AS cnt_a,
    SUM(CASE WHEN source = 'B' THEN 1 ELSE 0 END) AS cnt_b
FROM (
    SELECT id, col1, col2, 'A' AS source FROM Table_A
    UNION ALL
    SELECT id, col1, col2, 'B' AS source FROM Table_B
) combined
GROUP BY id
HAVING COUNT(*) <> 2 
    OR MAX(CASE WHEN source = 'A' THEN col1 END) <> MAX(CASE WHEN source = 'B' THEN col1 END)
    OR MAX(CASE WHEN source = 'A' THEN col2 END) <> MAX(CASE WHEN source = 'B' THEN col2 END);

Why this works:

  • UNION ALL avoids the expensive distinct sort of UNION.
  • Grouping by the Primary Key collapses the two potential rows (one from A, one from B) into one.
  • The HAVING clause filters for keys missing in one table (COUNT(*) <> 2) or value mismatches.
  • This method scales linearly and parallelizes well on modern MPP (Massively Parallel Processing) databases like Snowflake, Redshift, or BigQuery.

Method 4: CHECKSUM / HASHBYTES for Wide Tables

Comparing 50 columns individually in a JOIN or HAVING clause creates messy, unmaintainable SQL. For wide tables, compute a hash of the row content Turns out it matters..

SQL Server (T-SQL)

SELECT 
    id,
    CHECKSUM(*) AS row_hash -- Or HASHBYTES('SHA2_256', CONCAT_WS('|', col1, col2...))
FROM Table_A
-- Compare with Table_B hashes

PostgreSQL

SELECT id, md5(concat_ws('|', col1, col2, col3)::text) as row_hash FROM Table_A;

Generic Approach (CTE)

WITH hashes_a AS (
    SELECT id, MD5(CONCAT_WS('||', col1, col2, col3, col4)) AS row_hash FROM Table_A
),
hashes_b AS (
    SELECT id, MD5(CONCAT_WS('||', col1, col2, col3, col4)) AS row_hash FROM Table_B
)
SELECT 
   

### Completing the hash‑based comparison

The CTE skeleton can be closed with a simple anti‑join that highlights mismatches:

```sql
, comparison AS (
    SELECT 
        a.id,
        a.row_hash   AS hash_a,
        b.row_hash   AS hash_b,
        CASE WHEN a.row_hash IS NULL AND b.row_hash IS NULL THEN 'both missing'
             WHEN a.row_hash IS NULL THEN 'missing in A'
             WHEN b.row_hash IS NULL THEN 'missing in B'
             ELSE 'hash mismatch'
        END AS status
    FROM hashes_a a
    FULL OUTER JOIN hashes_b b
        ON a.id = b.id
        AND a.row_hash = b.row_hash          -- exact match only when both sides are non‑null
)
SELECT *
FROM comparison
WHERE status <> 'hash mismatch'          -- keep rows that differ or are absent
  AND status IS NOT NULL;                -- optional: filter out the perfect matches

The FULL OUTER JOIN guarantees that every key appears on both sides; when a key exists in only one source the corresponding hash will be NULL, allowing the CASE expression to flag the missing rows. If the hashes are identical, the row is considered a perfect match and can be omitted from the result set.


Method 5: Set‑based operators (EXCEPT / INTERSECT)

Modern relational engines support set operators that treat rows as atomic units, which means you can avoid writing any explicit join condition at all:

-- Rows present in A but not in B
SELECT * FROM Table_A
EXCEPT
SELECT * FROM Table_B

-- Rows present in B but not in A
SELECT * FROM Table_B
EXCEPT
SELECT * FROM Table_A

When the tables contain more columns than you care to compare, you can project a surrogate key (e.g., the primary key) before applying the operator:

SELECT id FROM Table_A
EXCEPT
SELECT id FROM Table_B;

If you need to verify that the content of matching keys is identical, combine the set operator with a secondary check:

SELECT a.id
FROM Table_A a
JOIN Table_B b ON a.id = b.id
WHERE NOT (a.col1 <=> b.col1 AND a.col2 <=> b.col2
           AND a.col3 <=> b.col3 AND …);

Pros

  • Very concise; the engine can parallelize the two scans natively.
  • Automatic handling of NULL values because the comparison operators (<=> or IS NOT DISTINCT FROM) are used inside the join condition.

Cons

  • Some platforms (e.g., MySQL) lack a native EXCEPT/INTERSECT implementation, requiring a workaround via LEFT JOIN … IS NULL.
  • When the tables are extremely wide, projecting only the primary key may hide column‑level mismatches unless you explicitly compare each column.

Choosing the right technique

Situation Recommended approach
Small‑to‑medium tables, few columns, need full row‑level diff FULL OUTER JOIN … IS NOT DISTINCT FROM (or EXCEPT)
Tables with millions of rows, limited memory, need linear scaling UNION ALL + aggregation (Method 3)
Very wide tables (dozens‑plus columns) where writing many comparisons is error‑prone Hash‑based comparison (Method 4)
Database lacks FULL OUTER JOIN or EXCEPT support Use UNION ALL + GROUP BY or a series of LEFT JOIN … IS NULL checks
Need to detect missing keys and value changes in a single pass Combine hash aggregation with a HAVING clause that flags both COUNT(*) <> 2 and hash inequality

In practice, the hash method shines when the primary key is not unique (e.g., composite keys) because the hash encapsulates the entire row. That said, it introduces a small CPU overhead for the hash calculation; on databases with fast built‑in hash functions (e.g., MD5, SHA2_256) the cost is negligible compared to the I/O required for a full scan.


Conclusion

Det

What's New

Just Went Live

Readers Also Checked

What Others Read After This

Thank you for reading about How To Compare Data In Two Tables In Sql. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home