Comparing Two Versions of the Same Data File
You exported your customer list in January. Now it's February, and you have a new export. Someone asks: "What changed?" New customers, lost customers, updated emails, changed account tiers - you need a diff, but this isn't source code. It's a CSV with 15,000 rows.
Opening both files side by side and scrolling isn't going to cut it. What you actually need is a join - specifically a FULL OUTER JOIN on the unique identifier. That gives you every row from both files, lined up, with NULLs where something exists in one file but not the other.
Here's the approach, step by step.
Load both files
Open ExploreMyData and load
your first file - say, customers_january.csv.
Then load the second file, customers_february.csv.
Both files appear as separate tables in the app.
Prefix the columns before you join
This is the step everyone skips, and it's the one that makes the rest of the article possible. Both
files have a column called email. Join
them and you get two columns called email. Nothing prefixes them for you, and once you're staring at
two identically named columns you can't write a comparison that means anything.
So rename first. On the January table, click the green + in the Pipeline panel and select
Rename Columns from the Columns group. Map
customer_id to
jan_customer_id,
email to
jan_email,
tier to
jan_tier. One step handles all
three, and columns you don't list pass through unchanged. Switch to the February table and do the same
with feb_.
Renaming the key columns differently is fine, and in fact useful. The join panel asks for a left key and a right key separately, so they never needed to match in the first place. Having both jan_customer_id and feb_customer_id in the output is what lets you detect added and removed rows later.
Join with FULL OUTER
Stay on the January table and open the Join operation from the
Data group. Pick the February view as the table to join with. Joins resolve to the other view's current
pipeline output, so it arrives with your feb_ renames already applied. Set the join type to
Full, the left key to
jan_customer_id and the right key to
feb_customer_id.
Step 3 of that panel is Columns to include from right table. Leave it empty and you get everything from February; tick a few and you get only those. On a forty-column export, picking the six you actually plan to diff keeps the result readable. The panel shows a live preview of the join SQL underneath, which is worth a glance before you apply.
A FULL OUTER JOIN keeps everything. Rows that exist in both files get merged. Rows that exist only in January appear with NULLs for the February columns. Rows that exist only in February appear with NULLs for the January columns.
| jan_customer_id | jan_email | jan_tier | feb_customer_id | feb_email | feb_tier |
|---|---|---|---|---|---|
| 1001 | alice@co.com | starter | 1001 | alice@co.com | pro |
| 1002 | bob@co.com | pro | 1002 | bob.new@co.com | pro |
| 1003 | carol@co.com | starter | NULL | NULL | NULL |
| NULL | NULL | NULL | 1248 | dana@co.com | starter |
Full Outer Join on customer_id. Row 3 (carol) is NULL on the Feb side - she left. Row 4 (dana) is NULL on the Jan side - she's new. Row 2 has a changed email.
The result carries both sides, unambiguously, because you did the renaming yourself. That is the whole reason for the previous step.
Find new rows (added in February)
These are rows where the January side is NULL: the customer exists in February but not January.
Add a Filter with the condition jan_customer_id
is Empty. On a text column that operator generates
(jan_customer_id IS NULL OR jan_customer_id = ''),
which is normally a feature and here is a small hazard: a January row whose id was blank rather than
missing would be counted as a February addition. If your ids can be blank, switch the condition builder
to the SQL Expression tab and type
jan_customer_id IS NULL instead. On a
numeric id column the question doesn't arise, because "is Empty" is a plain IS NULL there.
Count the rows. If you went from 15,000 to 15,400 customers, you'd expect roughly 400 new rows here, plus any that were both added and removed.
Find removed rows (gone from February)
The mirror image: feb_customer_id is Empty, or
feb_customer_id IS NULL on the SQL tab.
These are customers who were in January's export but disappeared from February's. Churned customers,
deleted accounts, or data that fell out of scope.
Filter: feb_customer_id IS NULL - customers present in January but gone from February:
| jan_customer_id | jan_name | jan_email | jan_tier | feb_customer_id |
|---|---|---|---|---|
| 1003 | Carol Vega | carol@co.com | starter | NULL |
| 1019 | Dan Marsh | dan@marsh.io | pro | NULL |
| 1041 | Elena Park | epark@example.com | starter | NULL |
3 customers removed - churned accounts, deleted records, or data that fell out of scope since January.
Find changed values, and why != will lie to you
This is the interesting part. For rows that exist in both files, which fields actually changed? Use Add Column, switch the value box to Expression, and write a change flag. The obvious version:
CASE WHEN jan_email != feb_email THEN 'email_changed' ELSE 'no_change' END
And it will quietly miss things. In SQL, comparing anything to NULL yields NULL rather than true, and a
CASE branch that evaluates to NULL is not taken. So a customer whose email went from "bob@co.com" to
blank, which is exactly the kind of change you're hunting, comes back labelled
no_change. Every row where either
side is missing looks identical to a row where nothing happened.
The fix is IS DISTINCT FROM, which is
!= with the NULL handling you
actually wanted. It returns true when one side is NULL and the other isn't, and false when both are NULL:
CASE WHEN jan_email IS DISTINCT FROM feb_email THEN 'email_changed' WHEN jan_tier IS DISTINCT FROM feb_tier THEN 'tier_changed' ELSE 'no_change' END
Name it change_type, then
filter to exclude "no_change" to see only the rows where something is different. The same operator works
in the Filter condition builder, but not from the visual tab,
since the operator list doesn't offer it. Switch that panel to
SQL Expression and type the comparison yourself.
For a broader check across more columns, chain them with OR:
CASE WHEN jan_email IS DISTINCT FROM feb_email OR jan_tier IS DISTINCT FROM feb_tier OR jan_phone IS DISTINCT FROM feb_phone THEN 'changed' ELSE 'same' END
This catches any row where at least one field differs between the two exports.
Putting it all together
For a complete diff summary, create a single status column with Add Column:
CASE WHEN jan_customer_id IS NULL THEN 'added' WHEN feb_customer_id IS NULL THEN 'removed' WHEN jan_email IS DISTINCT FROM feb_email OR jan_tier IS DISTINCT FROM feb_tier THEN 'modified' ELSE 'unchanged' END
Branch order matters here. The added and removed tests come first, so a row that exists on one side only
is labelled by what happened to it, not flagged as "modified" because every field differs. Name the
column diff_status and set its type
to text.
Now every row is labelled. Click the diff_status header to open the Column Explorer and you get the whole diff as four counts: added, removed, modified, unchanged. To work through only what moved, add a Filter with diff_status is not "unchanged".
| jan_customer_id | jan_tier | feb_tier | diff_status |
|---|---|---|---|
| 1001 | starter | pro | modified |
| 1002 | pro | pro | modified |
| 1003 | starter | NULL | removed |
| NULL | NULL | starter | added |
| 1005 | enterprise | enterprise | unchanged |
The diff_status column labels every row. Customer 1002 reads "modified" even though the tiers match, because the email changed; the status looks at every field you listed, not just the two shown here.
When to use this
This works for any scenario where you have two snapshots of the same data:
- Monthly customer exports - track churn and growth
- Inventory snapshots - what was added to or removed from stock
- Price lists - which products changed price and by how much
- Employee rosters - new hires, departures, role changes
- Configuration exports - what settings changed between deployments
The key requirement is a stable unique identifier that exists in both files. Without that, you can't match rows. If your data doesn't have a natural key, you might need to create a composite one (like combining name + date + amount) before joining.