Access-Datenbank nach PostgreSQL migrieren — Schritt für Schritt
Schemaextraktion, Datentyp-Mapping, pgloader oder `\copy`, Upsert mit `ON CONFLICT` und die Validierung am Ende — eine Migration ohne Drama.
Eine Access-Anwendung an die Grenze ihrer Möglichkeiten zu bringen ist einfach: zwanzig gleichzeitige Nutzer, ein Backend über VPN, ein paar verschachtelte Abfragen. Spätestens dann steht die Migration nach PostgreSQL im Raum. Sie ist keine Raketenwissenschaft, aber sie hat eine Reihenfolge, in der man sie machen sollte.
Schritt 1 — Schema extrahieren, nicht raten
Die .accdb enthält das vollständige Schema in den Systemtabellen MSysObjects, MSysQueries und MSysRelationships. Wer das per VBA-Skript abzieht, bekommt sauberes JSON; wer es lieber visuell macht, exportiert die Tabellenstruktur über „Datenbank dokumentieren”. Wichtig: nicht aus dem Bauch nachbauen. Spaltennamen, Längen, Default-Werte und vor allem die Referenzbeziehungen wandern als Inventarliste in eine Tabelle, gegen die später validiert wird.
Schritt 2 — Datentypen mappen
Hier liegen die meisten Stolpersteine. Eine pragmatische Mapping-Tabelle:
Text(n)→varchar(n)bei festem Limit, sonsttext. PostgreSQL macht intern keinen Performance-Unterschied;textohne Limit ist legitim.Memo(Langtext) →text.Zahl/Long Integer→integer,Zahl/Double→double precision.Währung→numeric(15,4). Niemalsmoney— der Typ hat in Postgres Locale-Probleme und ist deprecated für ernsthafte Buchhaltung.Datum/Uhrzeit→timestamp(ohne Zeitzone, wenn Access keine pflegt) odertimestamptz(wenn der Anwendungscode aktiv mit Zeitzonen arbeitet).Ja/Nein→boolean. Achtung: Access speichertTrueals-1, nicht als1.AutoWert→bigint GENERATED ALWAYS AS IDENTITY(Postgres 10+).serialfunktioniert noch, ist aber Legacy.OLE-Objekt→bytea, aber als Warnsignal markieren. Binärdaten in der Datenbank sind in 90 % der Fälle ein Hinweis darauf, dass die Anwendung historisch keinen Dateispeicher hatte. Bei der Migration ist das der richtige Moment, das auf ein Filesystem oder S3 auszulagern und die Tabelle nur noch den Pfad halten zu lassen.
Boolesche Werte werden beim Import explizit umgesetzt: CASE WHEN feld = -1 THEN true ELSE false END. Sonst landen -1 in einem integer-Feld und niemand wundert sich.
Schritt 3 — Daten transportieren
Zwei sinnvolle Wege, je nach Datenmenge.
pgloader ist der bequeme Weg. Eine Konfigurationsdatei beschreibt Quelle, Ziel und Mapping; der Lauf erledigt Schema, Daten und Indizes in einem Rutsch:
pgloader \
--with "include drop" \
--with "create tables" \
--with "create indexes" \
mdb://./altbestand.accdb \
postgresql://migrate@localhost/altbestand
pgloader liest .mdb direkt (über mdbtools); für .accdb ist meist ein Zwischenschritt nötig: in Access als ODBC-Quelle freigeben und pgloader per ODBC-Connector ansprechen, oder vorab nach .mdb exportieren. Bei Datenmengen unter etwa 5 Mio. Zeilen pro Tabelle ist das der Weg mit dem besten Aufwand-Ergebnis-Verhältnis.
CSV plus \copy ist der Weg für alles, was darüber liegt oder wo pgloader an exotischen Spalten scheitert. Pro Tabelle wird aus Access nach CSV exportiert (UTF-8, mit Header, Trennzeichen Semikolon), das Zielschema in Postgres vorab angelegt, dann:
\copy bestellung FROM '/tmp/bestellung.csv'
WITH (FORMAT csv, HEADER true, DELIMITER ';', ENCODING 'UTF8', NULL '');
\copy läuft client-seitig, braucht also keine Server-Filesystem-Rechte. Bei 1,2 Mio. Zeilen liegt das in der Größenordnung von 20 bis 40 Sekunden — schneller als jeder Schleifen-Insert.
Schritt 4 — Wiederanlauf-fähig importieren
Migrationsläufe brechen ab. Netz weg, Schlüsselkonflikt, Encoding-Problem in Zeile 480.000. Damit der nächste Anlauf nicht alles wiederholen muss, läuft der eigentliche Import in eine Staging-Tabelle und wird per INSERT ... ON CONFLICT in die Ziel-Tabelle übernommen:
INSERT INTO kunde (kunde_id, name, plz, ort)
SELECT kunde_id, name, plz, ort
FROM staging.kunde
ON CONFLICT (kunde_id) DO UPDATE
SET name = EXCLUDED.name,
plz = EXCLUDED.plz,
ort = EXCLUDED.ort;
Damit ist der Lauf idempotent. Ein abgebrochener Import wird einfach neu gestartet; bereits geschriebene Zeilen werden aktualisiert oder übersprungen.
Schritt 5 — Validieren, bevor man feiert
Drei Prüfungen, in dieser Reihenfolge: Zeilenzahlen pro Tabelle zwischen Quelle und Ziel vergleichen (eine einzelne SQL-Query mit UNION ALL macht die Übersicht), Stichprobenvergleich von je 100 zufälligen Zeilen pro großer Tabelle (ORDER BY random() LIMIT 100), Aggregat-Vergleich der wichtigsten Kennzahlen (Summen, Zählungen pro Kategorie). Erst wenn alle drei sauber sind, geht das Frontend auf Postgres.
Häufige Restprobleme: Datumswerte vor 1900 (Access kennt sie, Postgres auch, aber Frontends manchmal nicht), Texte mit eingebettetem Chr(13) ohne Chr(10) (Postgres ist toleranter, manche Reports nicht), und implizite Sortierungen, die in Access nach Eingabereihenfolge laufen, in Postgres aber ohne ORDER BY undefiniert sind.
Eine Access-zu-Postgres-Migration ist zu 70 % Inventur, zu 20 % Datentransport und zu 10 % SQL — die häufigste Fehlerquelle liegt in den 70 %.