diff old.csv new.csv beantwortet die falsche Frage. Es vergleicht Zeilen. Eine
anders sortierte Datei kommt deshalb als „alles geändert“ zurück, und eine Zeile,
in der ein einziges Feld korrigiert wurde, erscheint als Löschung plus Ergänzung
statt als Änderung.
Gesucht ist ein Vergleich über den Schlüssel: Welche Zeilen sind neu, welche sind weg und, bei Zeilen in beiden Dateien, welche Felder unterscheiden sich? Drei Fragen, und sie brauchen drei verschiedene Antworten.
Zuerst die Struktur prüfen
Bevor du Inhalte vergleichst, kläre, ob die beiden Dateien überhaupt vergleichbar sind:
head -n 1 old.csv new.csv
wc -l old.csv new.csv
Eine geänderte Kopfzeile ist der häufigste Grund für sinnlose Vergleichsergebnisse. Wurde zwischen den beiden Exporten eine Spalte hinzugefügt, umbenannt oder verschoben, ist jeder feldweise Vergleich wertlos, bis du das berücksichtigst. Die Zeilenzahl ist das zweite Signal: Ein Unterschied von drei ist eine normale Woche, ein Unterschied von 40.000 bedeutet meist, dass ein Export abgeschnitten oder gefiltert wurde.
Der Drei-Fragen-Vergleich in SQL
Wähle zuerst den Schlüssel: die Spalte, die denselben Datensatz in beiden
Dateien identifiziert. id, order_id, email. Gibt es keine, lies den
letzten Abschnitt.
Zeilen nur in der neuen Datei:
duckdb -c "SELECT * FROM read_csv('new.csv') n WHERE n.id NOT IN (SELECT id FROM read_csv('old.csv'))"
Zeilen nur in der alten Datei:
duckdb -c "SELECT * FROM read_csv('old.csv') o WHERE o.id NOT IN (SELECT id FROM read_csv('new.csv'))"
Zeilen in beiden Dateien mit geändertem Feld:
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 statt <> ist das Detail, auf das es ankommt: <> liefert
NULL, wenn eine der beiden Seiten NULL ist. Ein Feld, das von leer zu gefüllt oder
von gefüllt zu leer wurde, fällt dadurch stillschweigend aus deinen Ergebnissen
heraus. Genau diese Änderung hast du vermutlich gesucht.
So erfasst du Änderungen in allen Spalten auf einmal, ohne sie einzeln zu nennen:
duckdb -c "SELECT * FROM read_csv('old.csv') EXCEPT SELECT * FROM read_csv('new.csv')"
EXCEPT vergleicht ganze Zeilen und ignoriert die Reihenfolge. Das ist ein deutlich
besseres diff als diff. Es sagt dir, dass sich eine Zeile geändert hat,
aber nicht welches Feld. Kombiniere es daher mit dem Join oben, sobald du
weißt, welche Zeilen du dir ansehen musst.
Wenn die Dateien nur anders sortiert sind
Willst du nur wissen, ob der Inhalt derselbe ist, sortiere und hashe:
tail -n +2 old.csv | sort | md5sum
tail -n +2 new.csv | sort | md5sum
Gleicher Hash heißt gleicher Inhalt, unabhängig von der Zeilenreihenfolge. Ein unterschiedlicher Hash sagt nichts darüber, was sich geändert hat. Aber es dauert eine Sekunde und klärt die Frage „Ist das derselbe Export?“, die oft schon die ganze Untersuchung ist.
Für einen zeilenweisen Diff, der Umsortierungen übersteht:
diff <(sort old.csv) <(sort new.csv)
Das ist weiterhin zeilenbasiert, eine Änderung in einem Feld erscheint also weiterhin als Paar, aber wenigstens dominiert die Sortierung nicht mehr die Ausgabe.
Spezialwerkzeuge
csvdiff und daff vergleichen beide über den Schlüssel und geben Ausgaben
wie „Spalte status hat sich in Zeile 41 von A zu B geändert“ aus, statt dir
zwei Zeilen zu zeigen. Wenn du das regelmäßig tust, installiere eines davon.
daff diff old.csv new.csv --output diff.html erzeugt eine farbige Tabelle, die
du auf einen Blick lesen kannst, was keine der obigen Varianten schafft.
Git, wenn die Dateien versioniert sind. Mit einem CSV-Diff-Treiber in
.gitattributes wird git diff auf einer Datendatei von nutzlos zu nützlich,
und es sind nur zwei Zeilen Änderung.
Excel ist ehrlich gesagt schlecht darin. Der übliche Weg, ein SVERWEIS
(VLOOKUP) pro Spalte ins andere Blatt, funktioniert bei kleinen Dateien, wird
bei großen quadratisch langsam und erzeugt ein Raster aus #NV, das schwer zu
lesen ist. Das Zusammenführen in Power Query ist viel besser, und fast niemand
nutzt es.
Unterschiede auf dem Smartphone lesen
Die häufigste Variante in der Praxis: zwei Exporte im Abstand einer Woche, in deinem E-Mail-Postfach, und die Frage lautet „Was hat sich geändert?“ und nicht „Erstelle einen Abstimmungsbericht“.
Der praktische Weg ist gar kein Diff. Füge eine Spalte hinzu, die die Quelle markiert, stapele die beiden Dateien, sortiere nach dem Schlüssel und lies die Paare. Die geänderten Felder stehen nebeneinander, weil die beiden Versionen jedes Datensatzes jetzt direkt aufeinanderfolgen. Bei ein paar Dutzend Zeilen geht das schneller als mit jedem Werkzeug.
SQL direkt auf die Datei ist der ganze Vergleich: Ein Join über den Schlüssel mit IS DISTINCT FROM findet die geänderten Felder, und der KI-Assistent übernimmt die Variante der Frage, die du lieber in Worten stellst, als sie auszuschreiben.
Was es kannZahlen, Datumsangaben und die falschen Unterschiede
Ein Vergleich liefert meist eine große Zahl von „Änderungen“, die keine sind. Schließe sie aus, bevor du etwas untersuchst:
- Leerzeichen am Ende.
"active"und"active "sind verschieden. Vergleiche mittrim()bereinigte Werte, es sei denn, Leerzeichen sind relevant, was fast nie der Fall ist. - Zahlenformatierung.
1000,1,000und1000.00sind eine Zahl und drei Zeichenketten. Wandle beide Seiten in einen numerischen Typ um, bevor du vergleichst. - Datumsformate.
01/02/2026gegenüber2026-02-01und, schlimmer,01/02/2026in einer Datei für Februar und in einer anderen für Januar. Wandle beide in Datumswerte um, statt Text zu vergleichen. - Gleitkomma-Rauschen.
0.1 + 0.2ist in keiner Sprache, die du benutzt, gleich0.3. Runde auf die relevante Genauigkeit, bevor du Dezimalzahlen vergleichst. - Zeichenkodierung. Ein Name, der in der einen Datei als
Joséund in der anderen alsJoséerscheint, ist ein Kodierungsunterschied und keine Datenänderung.
Jeder dieser Punkte erzeugt bei einer großen Datei Tausende Unterschiede, und so endet ein Vergleich damit, dass „alles als geändert angezeigt wird“.
Wird aus dem Vergleich eine Korrektur, etwa eine Spalte zu normalisieren, eine Dezimalzahl zu runden oder die tatsächlich geänderten Zeilen zu korrigieren, wendet eine Formelspalte sie auf jede Zeile an, und das Ergebnis wird in der CSV gespeichert.
Was es kannWenn es keinen Schlüssel gibt
Manchmal haben die Dateien keine Kennung: zwei Exporte eines Logs, zwei Listen von Transaktionen. Dann hast du zwei Möglichkeiten, und beide sind Kompromisse.
Einen zusammengesetzten Schlüssel bilden aus den Spalten, die zusammen eine Zeile identifizieren: etwa Datum plus Betrag plus Beschreibung. Das funktioniert, wenn die Kombination wirklich eindeutig ist, und scheitert unbemerkt, wenn nicht: Zwei identische Transaktionen am selben Tag werden zu einer, und der Vergleich meldet eine Phantom-Löschung.
Als Multimengen vergleichen. Zähle, wie oft jede unterschiedliche Zeile in jeder Datei vorkommt, und vergleiche die Zählungen. Das ist die richtige Methode für Logs und Transaktionen, bei denen dieselbe Zeile legitim mehrfach vorkommt und die relevante Änderung darin besteht, wie oft:
duckdb -c "SELECT *, count(*) n FROM read_csv('old.csv') GROUP BY ALL"
Führe das für beide Dateien aus und vergleiche die n-Spalten. Es ist
der einzige Ansatz, der bei Daten mit vielen Duplikaten richtig liegt.