diff old.csv new.csv では、知りたいことに答えられません。diff は行単位で比較するため、並び順が違うだけのファイルは「すべて変更」と判定されます。また、1つのフィールドだけを修正した行は、変更ではなく「削除と追加」の組として出力されます。
必要なのは、キーによる比較です。どの行が新しく増えたか、どの行がなくなったか、そして両方にある行ではどのフィールドが違うか。3つの問いには、それぞれ別の答え方が必要です。
まず形を確認する
内容を比べる前に、2つのファイルが比較できる状態かを確認します。
head -n 1 old.csv new.csv
wc -l old.csv new.csv
比較結果がおかしくなる原因で最も多いのは、ヘッダーの変更です。2回のエクスポートの間に列が追加、改名、並べ替えされていると、その点を考慮するまで、フィールド単位の比較はすべて意味を失います。次に見るのが行数です。差が3行なら普通の1週間分ですが、4万行も違うなら、どちらかのエクスポートが途中で切れているか、絞り込まれているのがたいていの原因です。
SQLで行う3つの問いの比較
最初にキーを決めます。同じレコードを両方のファイルで識別できる列です。id、order_id、email などが該当します。キーがない場合は、最後のセクションをご覧ください。
新しいファイルにだけある行:
duckdb -c "SELECT * FROM read_csv('new.csv') n WHERE n.id NOT IN (SELECT id FROM read_csv('old.csv'))"
古いファイルにだけある行:
duckdb -c "SELECT * FROM read_csv('old.csv') o WHERE o.id NOT IN (SELECT id FROM read_csv('new.csv'))"
両方にあり、フィールドが変わった行:
duckdb -c "SELECT o.id, o.status old_status, n.status new_status FROM read_csv('old.csv') o JOIN read_csv('new.csv') n USING (id) WHERE o.status IS DISTINCT FROM n.status"
重要なのは、<> ではなく IS DISTINCT FROM を使う点です。<> はどちらかが NULL のとき NULL を返すため、空欄から入力済みへ、または入力済みから空欄へ変わったフィールドが、気づかないうちに結果から漏れます。探していた変更は、まさにそれである可能性が高いはずです。
列名を1つずつ書かずに、すべての列の変更をまとめて拾うには次のようにします。
duckdb -c "SELECT * FROM read_csv('old.csv') EXCEPT SELECT * FROM read_csv('new.csv')"
EXCEPT は行全体を比較し、順序を無視します。そのため、diff よりずっと優れた diff として使えます。ただし、行が変わったことは分かっても、どのフィールドかは分かりません。見るべき行が絞れたら、上の JOIN と組み合わせてください。
並び順が違うだけのとき
内容が同じかどうかだけ知りたいなら、ソートしてハッシュを取ります。
tail -n +2 old.csv | sort | md5sum
tail -n +2 new.csv | sort | md5sum
ハッシュが同じなら、行の順序に関係なく内容は同じです。ハッシュが違っても、何が変わったかは分かりません。それでも1秒で済み、「これは同じエクスポートか」という疑問に決着がつきます。調査の中身が、実はそれだけだったという場合も少なくありません。
並べ替えに強い行単位の diff は、こうします。
diff <(sort old.csv) <(sort new.csv)
あくまで行単位なので、1フィールドの変更は相変わらず1組の行として表示されますが、少なくとも並び順が出力を埋め尽くすことはなくなります。
専用ツール
csvdiff と daff はどちらもキーで比較し、2行を並べて見せる代わりに「行41で列 status が A から B に変わった」という形で出力します。日常的に比較するなら、どちらかを入れておくとよいでしょう。daff diff old.csv new.csv --output diff.html を実行すると、色付きの表が生成されます。ひと目で読めるのは、ここまでの方法にはない長所です。
Git は、ファイルをバージョン管理している場合に使えます。.gitattributes に CSV 用の diff ドライバーを設定すると、データファイルに対する git diff が役に立たないものから使えるものに変わります。変更はたった2行です。
Excel は、正直なところこの作業が苦手です。よくあるやり方は、列ごとに別シートへ VLOOKUP をかける方法です。小さなファイルなら動きますが、大きなファイルでは処理量が二乗で増え、#N/A だらけの表が読みにくく残ります。Power Query のマージのほうがはるかに優れていますが、ほとんど誰も使っていません。
スマートフォンで差分を読む
実際によくあるのは、1週間空けた2つのエクスポートがメールに届いていて、知りたいのは「照合レポートを作ること」ではなく「何が変わったか」という場面です。
現実的な方法は、diff ではありません。どちらのファイル由来かを示す列を追加し、2つのファイルを縦に連結し、キーで並べ替えて、ペアを読みます。各レコードの2つのバージョンが隣り合うので、変更されたフィールドが横並びで見えます。数十行程度なら、どんなツールより速い方法です。
ファイルに直接SQLを書くだけで比較は完結します。キーでのJOINとIS DISTINCT FROMで変更されたフィールドが見つかり、SQLを書くより言葉で聞きたい質問はAIアシスタントが引き受けます。
できること数値、日付、そして偽の差分
比較すると、実際は変更ではないものが大量に「変更」として出てきます。調べる前に、これらを除外しましょう。
- 末尾の空白。
"active"と"active "は別の値です。空白に意味がある場合を除き、trim()した値で比較します。意味があることはほぼありません。 - 数値の書式。
1000、1,000、1000.00は、1つの数値でありながら3つの文字列です。比較の前に、両側を数値型にキャストします。 - 日付の書式。
01/02/2026と2026-02-01の違い、さらに厄介なのは、同じ01/02/2026が片方のファイルでは2月、もう片方では1月を意味するケースです。文字列として比べず、両方を日付として解析します。 - 浮動小数点の誤差。
0.1 + 0.2は、どの言語でも0.3にはなりません。小数を比較する前に、必要な精度に丸めます。 - 文字コード。 片方のファイルで
José、もう片方でJoséと表示される名前は、文字コードの違いであり、データの変更ではありません。
こうした問題は、大きなファイルでは1つだけで数千件の差分を生みます。比較結果が「すべて変更あり」になってしまうのは、そのためです。
比較が修正作業に変わる場面、たとえば列の正規化、小数の丸め、実際に変わった行の修正では、数式列で全行に一括適用できます。結果はCSVの中にそのまま保存されます。
できることキーがない場合
ログのエクスポートが2つ、取引の一覧が2つといったように、識別子のないファイルもあります。選択肢は2つで、どちらも妥協を含みます。
複合キーを作る。 行を一意に決められる複数の列を組み合わせます。たとえば、日付と金額と摘要です。組み合わせが本当に一意なら有効ですが、そうでなければ気づかないうちに失敗します。同じ日の同一取引が2件あると1件にまとまり、比較は存在しない「削除」を報告します。
多重集合として比較する。 各ファイルで、重複を除いた各行の出現回数を数え、その回数を比べます。ログや取引のように、同じ行が正当に何度も現れ、意味のある変更が何回現れるかである場合は、この方法が適しています。
duckdb -c "SELECT *, count(*) n FROM read_csv('old.csv') GROUP BY ALL"
これを両方のファイルに実行して、n 列を比べます。重複の多いデータを正しく扱えるのは、この方法だけです。