Downtime-arme Migration SQL Server → PostgreSQL — Strategien für den Cutover

Die Frage kommt verlässlich in der ersten Planungsrunde, und sie kommt selten aus der IT: Wie lange ist die Datenbank beim Umzug nicht erreichbar? Die Antwort entscheidet, ob die Daten in einem Wartungsfenster am Stück umgezogen werden, ob der Bestand vorab kopiert wird und bis zum Umschalten nur noch die Änderungen folgen, oder ob ein Replikations-Werkzeug beide Systeme über Tage parallel hält. Wer nach einer Datenbank-Migration ohne Downtime sucht, findet viele Versprechen und wenige ehrliche Zahlen.

Dieser Artikel ordnet die drei Strategien für den Umzug von SQL Server nach PostgreSQL vom längsten zum kürzesten Stillstand. Mit jeder Stufe kommt dafür mehr Technik ins Spiel, die in der Nacht des Cutovers funktionieren muss. Dazu kommt das Cutover-Drehbuch, also der Ablauf des Umschaltens selbst, denn der ist für alle drei gleich.

Das Wichtigste vorab:

  • Das Downtime-Budget zuerst: Wie lange die Datenbank weg sein darf, entscheidet der Fachbereich, nicht das Werkzeug. Ein Fenster, in dem nur nicht geschrieben werden darf, ist dabei etwas anderes als eines, in dem gar nichts geht, und deutlich leichter zu bekommen.
  • Drei Stufen: Big-Bang im Wartungsfenster, Snapshot mit Delta-Sync oder Replikation per CDC. Jede Stufe weniger Stillstand wird mit mehr Technik und mehr Fehlerquellen bezahlt. Sobald der Transfer nicht mehr ins Wartungsfenster passt, ist die mittlere Stufe der beste Kompromiss.
  • Die blinde Stelle des Delta-Syncs: Wer Änderungen über eine Zeitstempel- oder rowversion-Spalte nachzieht, findet neue und geänderte Zeilen, aber keine gelöschten. Change Tracking in SQL Server schließt diese Lücke ohne Änderung am Schema.
  • Das Cutover-Drehbuch: Schreib-Stopp, letzter Delta-Lauf, Verifikation, Sequenzen nachziehen, Freigabe. Bis zur Freigabe lässt sich der Cutover abbrechen, ohne dass etwas verloren geht, weil die Quelle unverändert bleibt. Deshalb liegen Verifikation und ein echter Schreibtest vor der Freigabe.

Voraussetzung: SQL Server 2017+ als Quelle, PostgreSQL 14+ als Ziel. Das Zielschema steht, und ein Bulk-Load funktioniert, denn beides behandeln die Schwester-Artikel zur Schema-Migration und zum Datentransfer. Hier geht es darum, wie die Änderungen während des Umzugs nachgezogen werden und wann und wie umgeschaltet wird. Die SQL-Server-Konzepte rowversion, Change Tracking und CDC werden beim ersten Auftreten erklärt.

Inhalt

Das Downtime-Budget bestimmen

Stillstand kostet. Die Datenbank selbst merkt davon nichts, aber ein Webshop verliert Bestellungen, ein Lager steht, und ein Reporting liefert am Montagmorgen keine Zahlen. Die Frage nach dem Fenster geht deshalb an den Fachbereich und den Betrieb, und sie braucht eine Zahl, keine Stimmung: Wie viele Stunden Stillstand sind an einem Sonntag um drei Uhr akzeptabel, wie viele Minuten an einem Dienstagvormittag?

Dabei lohnt es sich, zwei Fenster auseinanderzuhalten, die im Gespräch meist in eins fallen:

  • Komplett-Stopp: Niemand kann lesen, niemand kann schreiben. Die Anwendung zeigt eine Wartungsseite.
  • Schreib-Stopp: Lesen geht weiter, nur Änderungen sind gesperrt. Kunden sehen ihre Bestellungen, Berichte laufen, aber es entsteht nichts Neues.

Was den Umzug kompliziert macht, sind nicht die Lesezugriffe, sondern die Änderungen, die während des Transfers in der Quelle ankommen und im Ziel fehlen. Ein Schreib-Stopp beseitigt genau dieses Problem, ohne dass die Datenbank verschwindet. Fachbereiche, die bei „die Datenbank ist vier Stunden weg“ abwinken, akzeptieren oft „vier Stunden lang nur lesen“. Ein solches Read-only-Fenster spart damit nicht selten eine ganze Stufe an Technik.

Es hat allerdings eine Bedingung. SQL Server kann einen Schreib-Stopp mit ALTER DATABASE … SET READ_ONLY WITH ROLLBACK IMMEDIATE zwar erzwingen, wobei die Klausel laut Dokumentation offene Transaktionen zurückrollt und deren Verbindungen trennt, damit der Wechsel den exklusiven Zugriff bekommt, den er braucht. Die Anwendung bekommt dann aber bei jedem Schreibversuch einen Fehler, und ob daraus eine freundliche Meldung oder ein Absturz wird, entscheidet ihr Code. Ein brauchbares Read-only-Fenster ist deshalb eine Entscheidung der Anwendung, die über ein Feature-Flag, einen Wartungsmodus oder eine umgestellte Verbindungs-Konfiguration ihre Schreibpfade abschaltet. Die Datenbank liefert nur die Absicherung dahinter.

Mit dem Budget in der Hand lässt sich die Strategie wählen. Die Entscheidungsmatrix am Ende verfeinert diese Wahl nach Datenvolumen und Änderungsrate. Die drei Stufen im Überblick:

StufeStillstand beim UmschaltenTechnik, die funktionieren mussAbbruch vor der FreigabeTypischer Fall
1 Big-Bang im WartungsfensterStunden: Transfer plus VerifikationTransfer-Werkzeugeinfach, die Quelle bleibt unverändertkleine bis mittlere Datenbank, Nacht- oder Wochenend-Fenster vorhanden
2 Snapshot und Delta-SyncMinuten: letzter Delta-Lauf, Checks, UmschaltenTransfer-Werkzeug, Änderungsmarke, Delta-Läufeeinfach, die Quelle bleibt unverändertTransfer passt nicht ins Fenster, oder das Budget liegt bei Minuten
3 CDC und ReplikationSekunden bis wenige Minutenzusätzlich SQL Server Agent für CDC, Connector, Nachrichtenstrom, Sink-Connector zum Zieleinfach, Replikation stoppen, die Quelle bleibt unverändertgroße, dauernd beschriebene Systeme mit hartem Budget

Was in der Tabelle nicht steht, aber in jeder Zeile gilt: Jede Komponente, die dazukommt, kann in der Nacht des Cutovers ausfallen.

Stufe 1: Big-Bang im Wartungsfenster

Beim Big-Bang im Wartungsfenster wird die Quelle angehalten, der komplette Datenbestand übertragen, geprüft und das Ziel freigegeben. Der Stillstand dauert so lange wie Transfer und Verifikation zusammen, typischerweise Stunden. Dafür ist das Verfahren das einfachste von allen, und ein Abbruch kostet nichts: Bis zur Freigabe bleibt die Quelle unverändert.

Diese Stufe hat keinen schlechten Ruf verdient. Für eine Datenbank mit ein paar Dutzend Gigabyte, ein internes System ohne Nachtbetrieb oder eine Anwendung mit festem Wartungstermin ist sie die richtige Wahl, weil jede weitere Stufe Technik hinzufügt, die hier niemand braucht. Der Transfer selbst läuft mit den Werkzeugen aus dem Schwester-Artikel zum Datentransfer: bcp und COPY, pgloader oder eine ETL-Strecke.

Was diese Stufe verlangt, ist eine Messung, denn das Wartungsfenster muss zur Dauer des Transfers passen. Dafür reicht ein Restore des letzten Backups auf eine Testinstanz, gegen die die komplette Strecke läuft: Export, Load, Aufbau der Indizes, Prüfung der Constraints, ANALYZE und Verifikation. Was zählt, ist die Zeit von Anfang bis Ende, denn gerade der Index-Aufbau auf großen Tabellen dauert oft länger als der Transfer der Daten. Wer das Ergebnis mit einem Aufschlag von der Hälfte ins Fenster legt und das Fenster trotzdem bekommt, bleibt bei Stufe 1. Wer es nicht bekommt, hat damit den Übergang zu Stufe 2 begründet.

Der Ablauf im Fenster ist das Cutover-Drehbuch weiter unten, nur mit einem Unterschied: Der „letzte Delta-Lauf“ ist hier der ganze Transfer.

Stufe 2: Snapshot und Delta-Sync

Beim Snapshot mit Delta-Sync wird der Datenbestand vorab bei laufendem Betrieb übertragen. Anschließend zieht ein wiederholbarer Lauf nur die Zeilen nach, die sich seit dem letzten Lauf geändert haben. Der Stillstand schrumpft auf den letzten dieser Delta-Läufe, die Verifikation und das Umschalten, typischerweise Minuten. Dafür muss man selbst dafür sorgen, dass sich geänderte Zeilen überhaupt erkennen lassen, zum Beispiel über eine Spalte, die bei jeder Änderung einen neuen Wert bekommt. Gelöschte Zeilen findet man auf diesem Weg nicht, dafür braucht es einen eigenen Schritt.

Der Ablauf hat vier Schritte. Zuerst wird eine Änderungsmarke gesichert, in der englischen Literatur High-Water-Mark genannt. Dann läuft der Snapshot, also der Bulk-Load des kompletten Bestands, während die Anwendung weiterarbeitet. Danach folgen Delta-Läufe in beliebiger Zahl, jeder liest die Änderungen seit der letzten Marke und schreibt sie ins Ziel. Zum Schluss kommt der Cutover mit einem letzten, kurzen Delta-Lauf nach dem Schreib-Stopp.

Der Snapshot muss dabei kein in sich konsistentes Abbild der Datenbank zu einem Zeitpunkt sein, und genau deshalb wird die Marke vor ihm gesichert. Ein Bulk-Load liest jede Tabelle zu einem anderen Zeitpunkt, und innerhalb einer Tabelle ändern sich Zeilen, während der Export läuft. Jede Zeile, die der Snapshot veraltet oder noch gar nicht gesehen hat, trägt danach einen Wert ab der gesicherten Marke und kommt mit dem ersten Delta-Lauf noch einmal. Was zählt, ist allein der Stand nach dem letzten Lauf, und der entsteht nach dem Schreib-Stopp. Der Export muss dafür nur jede Zeile in einem festgeschriebenen Stand lesen und keine unveränderte Zeile überspringen, und genau das garantiert ein Export mit NOLOCK nicht. Gelöschte Zeilen sind von dieser Selbstheilung ausgenommen, dazu gleich mehr.

Die Änderungsmarke

Die Marke braucht eine Spalte, die bei jeder Änderung einen neuen, steigenden Wert bekommt. SQL Server bringt dafür den Datentyp rowversion mit: eine acht Byte lange Zahl, die bei jedem INSERT und UPDATE aus einem datenbankweiten Zähler neu vergeben wird, ohne Zutun der Anwendung. Eine updated_at-Spalte, die die Anwendung oder ein Trigger pflegt, tut es auch, ist aber angreifbarer, weil ein vergessener Codepfad oder eine verstellte Uhr Zeilen unsichtbar macht.

Eine Falle steckt im Lesen der Marke. Der naheliegende Wert @@DBTS liefert den zuletzt vergebenen rowversion-Wert. Eine Transaktion, die noch offen ist, kann aber bereits einen kleineren Wert vergeben haben, der erst nach ihrem Commit sichtbar wird. Wer @@DBTS als Marke sichert, liest diese Zeile in keinem Delta-Lauf mehr. Die Funktion MIN_ACTIVE_ROWVERSION() liefert stattdessen den kleinsten Wert, der noch in einer offenen Transaktion steckt, und wenn keine offen ist, den nächsten Wert, der vergeben wird. Für den Delta-Lauf ist das die Obergrenze, unterhalb derer nur noch abgeschlossene Änderungen liegen. Microsoft nennt in der Dokumentation die Datensynchronisation als den Anwendungsfall dieser Funktion.

Der Delta-Lauf zieht zuerst die neue Obergrenze und liest dann das Fenster zwischen der alten und der neuen Marke:

  1: DECLARE @last binary(8) = (SELECT next_rowver FROM dbo.sync_watermark WHERE table_name = N'dbo.customer');
  2: DECLARE @new  binary(8) = MIN_ACTIVE_ROWVERSION();
  3: 
  4: SELECT
  5:     customer_id
  6:    ,email
  7:    ,credit_limit
  8:    ,row_ver
  9: FROM
 10:    dbo.customer
 11: WHERE
 12:        row_ver >= @last
 13:    AND row_ver <  @new;
 14: -- a@example.com (UPDATE) und c@example.com (INSERT); b@example.com fehlt
 

Die Zeilen zeigen im Einzelnen: Zeile 1 holt die gesicherte Marke aus einer kleinen Zustands-Tabelle, Zeile 2 zieht die neue Obergrenze, und die Zeilen 12 und 13 grenzen das Fenster ein. Die Vergleichszeichen sind kein Versehen. Die Marke ist der erste noch nicht verarbeitete Wert, deshalb heißt die Spalte next_rowver: Ohne offene Transaktion liefert MIN_ACTIVE_ROWVERSION() den Wert @@DBTS + 1, also genau den, den die nächste Änderung bekommt. Das Fenster schließt die untere Marke deshalb ein und die obere aus, und jeder Wert fällt in genau einen Lauf. Mit einem > an der unteren Grenze ginge die Zeile verloren, die genau den Marken-Wert bekommen hat, im Beispiel das UPDATE auf a@example.com. Die neue Marke wird erst gespeichert, nachdem das Ziel die Zeilen übernommen hat. Bricht der Lauf vorher ab, liest der nächste dasselbe Fenster noch einmal.

Damit das Wiederholen unschädlich ist, schreibt das Ziel per Upsert, in PostgreSQL INSERT … ON CONFLICT (customer_id) DO UPDATE: Eine Zeile, die der Snapshot schon enthielt und die das Delta noch einmal liefert, wird einfach überschrieben. Aus demselben Grund braucht jede Tabelle im Delta-Sync einen Primärschlüssel oder zumindest einen eindeutigen Schlüssel, denn ohne ihn gibt es kein Ziel für den Upsert.

Die blinde Stelle des Delta-Syncs: gelöschte Zeilen

Eine gelöschte Zeile hinterlässt in der Quelle nichts, was eine Marke tragen könnte. Im Code-Block oben fehlt deshalb b@example.com. Im Ziel lebt die Zeile weiter, und nach dem Umschalten taucht ein Kunde wieder auf, den jemand vor zwei Wochen gelöscht hat. Diese Lücke gehört zum Verfahren und braucht eine eigene Entscheidung.

Vier Wege schließen sie:

  • Change Tracking von SQL Server protokolliert zu jeder geänderten Zeile die Operation, auch das Löschen. Das ist der sauberste Weg und wird im nächsten Abschnitt gezeigt.
  • Soft-Delete: Die Anwendung löscht nicht, sondern setzt ein Kennzeichen wie is_deleted. Für die Marke ist das ein gewöhnliches UPDATE. Das setzt voraus, dass die Anwendung so gebaut ist, und ist kein Umbau für eine Migration.
  • Tombstone-Tabelle: Ein DELETE-Trigger schreibt den Schlüssel jeder gelöschten Zeile in eine Protokoll-Tabelle, die der Delta-Lauf mitliest. Das funktioniert überall, kostet aber einen Trigger pro Tabelle.
  • Schlüssel-Abgleich vor dem Umschalten: Für kleine Tabellen reicht es, nach dem letzten Delta-Lauf alle Schlüssel des Ziels gegen die Quelle zu prüfen und die Zeilen zu löschen, die dort fehlen. Bei Millionen Zeilen dauert das zu lange für ein Minuten-Fenster.

Change Tracking: Löschungen sehen, ohne das Schema anzufassen

Change Tracking ist eine Funktion von SQL Server, die pro Tabelle festhält, welche Zeilen sich seit einer bestimmten Version geändert haben und ob es ein INSERT, ein UPDATE oder ein DELETE war. Sie speichert nur Schlüssel, Operation und Versionsnummer, keine Werte, und das genügt, weil der Delta-Lauf die aktuellen Werte ohnehin aus der Tabelle liest. Sie steht laut der Editions-Übersicht von Microsoft in allen Editionen zur Verfügung, auch in Express, braucht keine zusätzliche Spalte und wird einmal auf Datenbank-Ebene und einmal pro Tabelle eingeschaltet.

Der Delta-Lauf fragt dann statt einer Spalte die Funktion CHANGETABLE ab:

  1: DECLARE @last bigint = @mark;   -- aus dem persistierten Sync-Zustand
  2: 
  3: SET TRANSACTION ISOLATION LEVEL SNAPSHOT;   -- braucht ALLOW_SNAPSHOT_ISOLATION ON
  4: SET XACT_ABORT ON;                          -- der THROW unten rollt dann zurueck
  5: BEGIN TRAN;
  6: 
  7: IF @last < CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID(N'dbo.customer'))
  8:    THROW 50001, N'Change-Tracking-Retention ueberschritten, Snapshot wiederholen.', 1;
  9: 
 10: DECLARE @new bigint = CHANGE_TRACKING_CURRENT_VERSION();
 11: 
 12: SELECT
 13:     T01.customer_id
 14:    ,T01.sys_change_operation          -- I = Insert, U = Update, D = Delete
 15:    ,T01.sys_change_version
 16:    ,T02.email
 17:    ,T02.credit_limit
 18: FROM
 19:    CHANGETABLE(CHANGES dbo.customer, @last) T01
 20:    LEFT JOIN dbo.customer T02
 21:    ON
 22:      T02.customer_id = T01.customer_id
 23: WHERE
 24:    T01.sys_change_version <= @new
 25: ORDER BY
 26:    T01.customer_id;
 27: -- 1 | U | a@example.com | 150.00
 28: -- 2 | D | NULL          | NULL
 29: -- 3 | I | c@example.com | 300.00
 30: 
 31: COMMIT;

Die Zeilen zeigen im Einzelnen: Zeile 19 liefert jeden Schlüssel, der sich seit der Version @last geändert hat, zusammen mit der Operation. Der LEFT JOIN in Zeile 20 holt die aktuellen Werte dazu, und bei einer gelöschten Zeile bleibt die rechte Seite leer, wie Zeile 28 zeigt. Das Ziel löscht diesen Schlüssel dann einfach. Die Zeilen 7 und 8 sichern gegen die einzige echte Falle des Verfahrens: Change Tracking hebt die Versionen nur für eine eingestellte Dauer auf, die Retention. Liegt die gesicherte Marke unter der ältesten noch bekannten Version, weil die Läufe zu lange pausiert haben, hilft nur noch ein neuer Snapshot. Die Retention muss deshalb den längsten denkbaren Abstand zwischen zwei Läufen abdecken, mit Reserve.

Die Zeilen 3, 5 und 31 setzen um, was Microsoft für Change Tracking empfiehlt: Die Datenbank bekommt einmalig ALLOW_SNAPSHOT_ISOLATION ON, und der Delta-Lauf liest Retention-Prüfung, Version, Änderungen und aktuelle Werte in einer Snapshot-Transaktion, damit alles zu demselben Stand gehört. Für die Werte allein wäre eine Abweichung verkraftbar, denn eine Zeile, die sich zwischen dem Ziehen von @new und dem Lesen ihrer Werte noch einmal ändert, bekommt ihre Versionsnummer erst beim Commit, liegt damit über @new und kommt im nächsten Lauf erneut. Praktisch wichtiger sind zwei andere Gründe. Der Cleanup kann zwischen der Retention-Prüfung und dem Lesen der Änderungen aufräumen. Innerhalb der Transaktion bleibt das unsichtbar, und das Ergebnis bleibt gültig. Und unter der Standard-Isolation READ COMMITTED wartet die Abfrage auf jede offene Transaktion, die gerade eine verfolgte Zeile ändert. Auf SQL Server 2022 gemessen lief sie so neben einer einzigen offenen Transaktion in den Lock-Timeout, unter Snapshot-Isolation lieferte sie sofort den zuletzt festgeschriebenen Stand. Zeile 4 sorgt dafür, dass der THROW aus Zeile 8 die Transaktion nicht offen stehen lässt.

Was Stufe 2 voraussetzt und wo sie endet

Drei Regeln gelten für die ganze Zeit zwischen Snapshot und Cutover:

  • Schema-Freeze: Das Quellschema ändert sich zwischen Snapshot und Cutover nicht mehr. Eine Spalte, die in dieser Zeit dazukommt, kennt weder das Ziel noch der Delta-Lauf. Steht noch eine Schema-Änderung an, wird sie vor dem Snapshot eingespielt, und der Snapshot beginnt erst danach.
  • Constraints bleiben im Delta aktiv. Beim Bulk-Load ist es üblich, Fremdschlüssel und Trigger im Ziel abzuschalten, damit die Ladereihenfolge keine Rolle spielt. Der Delta-Lauf arbeitet dagegen mit aktiven Constraints, sonst sammeln sich im Ziel Verletzungen, die erst nach dem Umschalten auffallen. Jeder Lauf schreibt deshalb in einer Transaktion, Eltern-Tabellen vor Kind-Tabellen oder mit aufgeschobenen Fremdschlüsseln (DEFERRABLE INITIALLY DEFERRED).
  • Die Delta-Läufe müssen aufholen. Wenn ein Lauf länger braucht, als die Quelle für dieselbe Menge neuer Änderungen braucht, wird der Rückstand nie kleiner. Das ist der Punkt, an dem Stufe 2 endet und Stufe 3 beginnt.

Tabellen ohne Schlüssel und Tabellen mit sehr vielen Änderungen pro Minute passen nicht in dieses Verfahren. Sie lassen sich im Wartungsfenster nach Stufe 1 übertragen, während der Rest per Delta-Sync läuft. Eine Migration muss nicht für alle Tabellen dieselbe Stufe wählen.

Stufe 3: CDC und Replikation

Bei der Replikation per CDC (Change Data Capture) schreibt SQL Server jede festgeschriebene Änderung in Änderungstabellen, und ein Werkzeug trägt sie von dort fortlaufend nach PostgreSQL. Beide Systeme laufen parallel, bis der Rückstand null ist, und der Stillstand schrumpft auf das Umschalten selbst, also auf Sekunden bis wenige Minuten. Dafür betreibt man eine dritte Komponente mit eigenem Ausfallrisiko, und ein Restfenster bleibt auch hier.

Was Replikation zwischen SQL Server und PostgreSQL wirklich heißt

Wer bei diesem Paar nach Replikation sucht, muss eine Ernüchterung einplanen. Die logische Replikation von PostgreSQL mit PUBLICATION und SUBSCRIPTION verbindet PostgreSQL mit PostgreSQL. Die Transaktionsreplikation von SQL Server liefert an SQL-Server-Abonnenten und laut Dokumentation an Oracle und IBM Db2, wobei Microsoft diese Fremd-Abonnenten als veraltet führt und für den Datentransport stattdessen CDC und SSIS empfiehlt. PostgreSQL steht nicht auf dieser Liste. Eine Replikation von SQL Server direkt nach PostgreSQL gibt es als Bordmittel also nicht.

Was es gibt, sind Werkzeuge, die auf CDC aufsetzen. Change Data Capture ist die zweite Änderungs-Funktion von SQL Server neben Change Tracking, und der Unterschied zwischen beiden entscheidet, welche Stufe man bauen kann:

Change TrackingChange Data Capture (CDC)
Arbeitsweisesynchron, beim INSERT/UPDATE/DELETE selbstasynchron, ein Job liest das Transaktionsprotokoll
Festgehalten wirdSchlüssel, Operation, Versiondie geänderten Zeilen mit allen erfassten Spalten, bei einem UPDATE das Nachher-Bild und auf Abruf auch das Vorher-Bild
Editionenalle, auch ExpressStandard und Enterprise (Standard seit SQL Server 2016 SP1), nicht Express
Brauchtnichts weiterlaufenden SQL Server Agent für den Capture-Job
Passt zuDelta-Sync in Stufe 2Streaming-Werkzeuge in Stufe 3, oder im Takt abgeholt als Stufe 2

Im Takt statt im Strom: SSIS als Delta-Lauf

Wer CDC eingeschaltet hat, muss nicht gleich streamen. Ein SSIS-Paket, das alle paar Minuten läuft, liest die seit dem letzten Lauf angefallenen Änderungen aus den CDC-Tabellen, teilt sie nach Einfügen, Ändern und Löschen auf und schreibt sie nach PostgreSQL. Technisch ist das ein Delta-Sync nach Stufe 2, nur mit CDC statt rowversion oder Change Tracking als Änderungserkennung. Für SQL-Server-Teams ist es oft der kürzeste Weg, weil Werkzeug, Zeitsteuerung über den Agent und Betriebs-Erfahrung schon da sind, auch ohne die eigenen CDC-Komponenten von SSIS, dazu gleich mehr. Der Rückstand beträgt Intervall plus Laufzeit des Pakets. Für den Cutover genügt das, denn nach dem Schreib-Stopp ist ohnehin nur noch ein letzter Lauf fällig.

Drei Dinge gehören dazu. Erstens die Bausteine: SSIS bringt seit 2012 eigene CDC-Komponenten mit (CDC Control Task, CDC Source, CDC Splitter), Microsoft hat sie aber im Februar 2024 als veraltet gekennzeichnet und den Support zum Dezember 2025 eingestellt, die Dokumentation trägt Stand Oktober 2026 den Hinweis. Neue Pakete setzen deshalb besser direkt auf den CDC-Funktionen auf: sys.fn_cdc_get_max_lsn() liefert die obere Marke, cdc.fn_cdc_get_net_changes_<capture_instance> die Änderungen zwischen zwei Marken, und __$operation sagt, ob eine Zeile gelöscht, eingefügt oder geändert wurde. Die Net-Changes-Funktion verdichtet mehrere Änderungen derselben Zeile auf eine und setzt dafür einen Primärschlüssel oder eindeutigen Index voraus, den der Upsert im Ziel ohnehin braucht. Die Marke landet in einer Zustands-Tabelle wie sync_watermark oben, und mit Change Tracking statt CDC wird die CHANGETABLE-Abfrage zur Quelle des Pakets. Zweitens das Ziel: SSIS hat keine eigene PostgreSQL-Destination, der Weg führt über ODBC. Updates und Löschungen zeilenweise über ein OLE DB Command zu schicken ist langsam, schneller ist eine Staging-Tabelle in PostgreSQL, in der der Stapel eines Laufs mengenbasiert angewandt wird, mit INSERT … ON CONFLICT DO UPDATE für Einfügungen und Änderungen und DELETE … USING für Löschungen. Drittens die Grenze: Auch dieser Weg braucht CDC, also die Standard- oder Enterprise-Edition und den Agent, und er bleibt ein Takt. Wer den Rückstand auf Sekunden drücken will, landet beim Streaming.

Streaming-Werkzeuge

Das bekannteste freie Streaming-Werkzeug ist Debezium, Stand Oktober 2026 in Version 3.7. Sein SQL-Server-Connector setzt laut Dokumentation voraus, dass CDC auf der Datenbank und auf jeder Tabelle eingeschaltet ist, dass SQL Server mindestens als 2016 SP1 in der Standard- oder Enterprise-Edition läuft und dass der SQL Server Agent arbeitet, weil erst dessen Job die Änderungstabellen füllt. Der Connector liest beim Start einen Snapshot aller Tabellen und streamt danach fortlaufend die Änderungen ab der zuletzt verarbeiteten Protokoll-Position in einen Nachrichtenstrom, üblicherweise Kafka, aus dem ein zweiter Connector, der JDBC-Sink von Debezium, sie nach PostgreSQL schreibt. Wer kein Kafka betreiben will, nimmt Debezium Server, das ohne Broker auskommt und einen eigenen JDBC-Sink mitbringt. Trotzdem steht zwischen Quelle und Ziel eine eigene Infrastruktur mit eigener Konfiguration, eigenem Monitoring und eigenem Typ-Mapping, und das Typ-Mapping ist nicht trivialer als beim Bulk-Load.

Die großen Cloud-Anbieter bieten dasselbe als Dienst an, etwa AWS Database Migration Service: Das spart den Aufbau, nicht die Konfiguration, und kostet pro Stunde, solange die Replikation läuft. Auf einer Express-Edition ohne CDC und Agent bleibt SymmetricDS, ein freies Werkzeug, das Änderungen über Trigger einsammelt, und diese Trigger muss die Quelle bei jedem Schreibzugriff im laufenden Betrieb verkraften.

Wann sich der Aufwand lohnt

Stufe 3 lohnt sich, wenn drei Dinge zusammenkommen: ein Budget von Minuten oder Sekunden, das sich nicht verhandeln lässt, ein Datenbestand, für den ein Delta-Lauf nach Stufe 2 nicht mehr aufholt, und ein Team, das die zusätzliche Infrastruktur betreiben kann. Ein Nebeneffekt macht die Stufe für große Systeme zusätzlich interessant: Weil beide Datenbanken über Tage parallel laufen, lässt sich die Anwendung gegen PostgreSQL testen, während SQL Server produktiv bleibt.

Fehlt eine dieser drei Bedingungen, bringt Stufe 3 Betriebsaufwand, dem kein Gewinn an Downtime gegenübersteht. Der Schema-Freeze gilt auch hier, und seine Verletzung ist teurer als in Stufe 2: Eine DDL-Änderung in der Quelle hält die Erfassung zwar nicht an, aber die Änderungstabelle behält ihre alte Spaltenliste, und eine neue Spalte fehlt in jedem Ereignis, bis eine zweite Capture-Instanz angelegt ist und der Connector auf sie umgestellt hat. Debezium beschreibt dafür ein Verfahren mit und eines ohne Anhalten des Connectors. Für einen planbaren Cutover ist der Freeze trotzdem der einfachere Weg. Und das Umschalt-Restfenster bleibt auch mit perfekter Replikation bestehen: Die Verbindungen zur Quelle müssen auslaufen, der letzte Rückstand muss auf null, die Sequenzen müssen nachgezogen werden, dann erst zeigt die Anwendung auf das neue Ziel. Das gilt, solange nur eine der beiden Datenbanken beschrieben wird. Ein Doppel-Schreiben aus der Anwendung heraus umgeht das Fenster, ist aber ein Umbau der Anwendung mit eigenen Konsistenz-Fragen und kein Thema dieses Artikels. Wer hier „Zero Downtime“ verspricht, meint „so kurz, dass es niemandem auffällt“. Das ist ein legitimes Ziel, aber eine andere Aussage.

Das Cutover-Drehbuch

Der Cutover ist der Moment, in dem die Anwendung von der Quelle auf das Ziel umgestellt wird. Sein Ablauf ist für alle drei Stufen derselbe, nur die Dauer der einzelnen Stationen unterscheidet sich. Er gehört als Drehbuch aufgeschrieben, mit Uhrzeiten, Verantwortlichen und für jede Station einer Prüfung, die bestanden sein muss, bevor die nächste beginnt.

  1. Vorbedingungen prüfen. Der Probelauf ist grün, der Schema-Freeze gilt seit einem bekannten Datum, der Rückstand der Delta-Läufe oder der Replikation ist klein, und der Abbruch-Plan ist geschrieben. Fehlt eines davon, wird der Termin verschoben, nicht die Prüfung.
  2. Schreib-Stopp. Die Anwendung geht in den Wartungs- oder Read-only-Modus. Die Datenbank sichert das ab, in SQL Server mit SET READ_ONLY. Prüfung: Es gibt keine offenen Schreib-Transaktionen mehr.
  3. Letzter Delta-Lauf. In Stufe 1 ist das der ganze Transfer, in Stufe 2 der letzte Lauf nach dem Schreib-Stopp, in Stufe 3 das Warten, bis die Replikation keinen Rückstand mehr meldet.
  4. Verifikation. Zeilen-Abgleich für jede Tabelle gegen die Zählung der Quelle, Prüfsummen für die kritischen Tabellen, Suche nach verwaisten Fremdschlüsseln. Die Vollform mit Prüfsummen und Stichproben steht im Schwester-Artikel zur Verifikation der Migration.
  5. Sequenzen nachziehen. Jede Identitäts- und jede serial-Spalte in PostgreSQL zieht ihre Werte aus einer Sequenz, und die steht nach einem Load mit expliziten Schlüsseln noch auf ihrem Startwert. Sie muss auf den höchsten vergebenen Wert gehoben werden, und zwar nach dem letzten Delta-Lauf, nicht nach dem Snapshot. Sonst steht die Sequenz auf dem Höchstwert des Snapshots, während das Delta längst höhere Schlüssel geliefert hat, und der erste INSERT nach dem Umschalten läuft in eine Primärschlüssel-Verletzung.
  6. Umschalten und Schreibtest. Verbindungs-Konfiguration, DNS oder Feature-Flag zeigen auf PostgreSQL. Prüfung: ein Schreibzugriff über den Anwendungspfad, noch im Wartungsmodus und kein SELECT 1. Weil ein solcher Zugriff Mails, Nachrichten oder Folge-Buchungen auslösen kann, läuft er mit einem Testdatensatz, der für genau diesen Zweck angelegt ist und dessen Nebenwirkungen bekannt sind. Schlägt der Test fehl, zeigt die Konfiguration wieder auf SQL Server, und die Quelle ist unverändert.
  7. Freigabe. Der Wartungs- oder Read-only-Modus wird aufgehoben. Ab jetzt ist PostgreSQL die einzige produktive Datenbank, und die Uhrzeit kommt ins Protokoll.
  8. Beobachtungsfenster. Die alte Datenbank bleibt schreibgeschützt stehen, einige Tage bis Wochen, als Vergleichsstand für Rückfragen. In dieser Zeit werden Fehlerraten, Antwortzeiten, Batch-Jobs und die Sequenzen beobachtet. Erst danach wird die Quelle abgebaut.

Für Station 5 gibt es auf der Zielseite einen kurzen Check, der die Sequenzen aller Identitäts- und serial-Spalten im Schema auf den Höchstwert hebt:

  1: DO $$
  2: DECLARE
  3:    l_col   record;
  4:    l_seq   text;
  5:    l_max   bigint;
  6: BEGIN
  7:    FOR l_col IN
  8:       SELECT
  9:           table_schema
 10:          ,table_name
 11:          ,column_name
 12:          ,column_default
 13:       FROM
 14:          information_schema.columns
 15:       WHERE
 16:              table_schema = 'public'
 17:          AND (   is_identity    = 'YES'
 18:               OR column_default LIKE 'nextval(%')
 19:    LOOP
 20:       l_seq := COALESCE(
 21:                   pg_get_serial_sequence(format('%I.%I', l_col.table_schema, l_col.table_name), l_col.column_name)
 22:                  ,substring(l_col.column_default FROM '^nextval\(''(.+)''::regclass\)$')
 23:                );
 24: 
 25:       IF l_seq IS NULL THEN
 26:          RAISE WARNING '%.%: keine Sequenz gefunden, von Hand pruefen', l_col.table_name, l_col.column_name;
 27:          CONTINUE;
 28:       END IF;
 29: 
 30:       EXECUTE format('SELECT max(%I) FROM %I.%I', l_col.column_name, l_col.table_schema, l_col.table_name)
 31:          INTO l_max;
 32: 
 33:       IF l_max IS NULL THEN
 34:          EXECUTE format('ALTER SEQUENCE %s RESTART', l_seq);
 35:       ELSE
 36:          PERFORM setval(l_seq, l_max);
 37:       END IF;
 38:    END LOOP;
 39: END;
 40: $$;

Die Zeilen zeigen im Einzelnen: Die Zeilen 8 bis 18 suchen im Schema jede Spalte, die eine Identitäts-Spalte ist oder ihren Default aus einer Sequenz zieht. Die Zeilen 20 bis 23 ermitteln die zugehörige Sequenz, zuerst über pg_get_serial_sequence, das Identitäts- und serial-Spalten kennt, sonst aus dem Spalten-Default, denn eine von Hand angelegte Sequenz ohne OWNED BY findet die Funktion nicht. Ohne diesen Rückgriff täte setval mit einem NULL still nichts, der Block liefe fehlerfrei durch, und der erste INSERT nach dem Umschalten träfe trotzdem auf den Schlüssel-Konflikt. Die Zeilen 25 bis 28 melden, was beide Wege nicht kennen. Zeile 30 liest den höchsten vergebenen Wert, die Zeilen 33 bis 37 setzen die Sequenz darauf. Eine leere Tabelle bekommt in Zeile 34 ein ALTER SEQUENCE … RESTART ohne Wert, das die Sequenz auf ihren eigenen Startwert zurücksetzt. Ein setval(…, 1, false) täte das nicht: Es ignoriert ein START WITH 100 und scheitert an einem MINVALUE über 1. Warum der Schritt überhaupt nötig ist, erklärt der Schwester-Artikel zur Schema-Migration im Abschnitt zum Sequenz-Reset.

Der Abbruch-Plan

Der Rollback dieses Drehbuchs ist ein Abbruch vor der Freigabe, und der ist in jeder Stufe einfach, weil die Quelle bis dahin unverändert bleibt: Die Anwendung zeigt wieder auf SQL Server, und verloren ist nichts außer dem Wartungsfenster. Deshalb liegen die Verifikation und der Schreibtest vor der Freigabe, und deshalb legt das Drehbuch für jede Station vorher fest, welcher Befund zum Abbruch führt. Wer das aufgeschrieben hat, muss es in der Nacht nicht mehr entscheiden.

Nach der Freigabe ist ein Rückweg kein Abbruch mehr, denn ab dann wird nur noch in der neuen Datenbank gearbeitet. Wer später doch zurück nach SQL Server will, plant eine Migration in Gegenrichtung, ein eigenes Projekt, das dieser Artikel nicht behandelt. Die alte Datenbank bleibt im Beobachtungsfenster als Vergleichsstand stehen, nicht als Reserve.

Datenbank-Migration ohne Downtime: die Entscheidungsmatrix

Drei Größen bestimmen die Stufe: das Downtime-Budget, das Datenvolumen im Verhältnis zum Fenster und die Änderungsrate, also wie viele Zeilen sich pro Stunde ändern und wie oft dabei gelöscht wird.

Downtime-BudgetDatenvolumenÄnderungsrateEmpfehlung
Stunden, Fenster vorhandenTransfer passt mit Reserve ins FensterbeliebigStufe 1
Stunden, Fenster vorhandenTransfer passt nicht ins Fenstergering bis mittelStufe 2 mit rowversion-Marke, Löschungen per Schlüssel-Abgleich
Minutenbeliebiggering bis mittel, Löschungen kommen vorStufe 2 mit Change Tracking
Minutengroßhoch, Delta-Läufe holen nicht aufStufe 3
SekundenbeliebigbeliebigStufe 3, und das Restfenster trotzdem benennen
nur ein Read-only-Fenster nötigbeliebigbeliebigals Faustregel eine Stufe tiefer als die Zeile, die sonst gelten würde

Die letzte Zeile ist die wichtigste. Wer mit dem Fachbereich ein Read-only-Fenster statt eines Komplett-Stopps aushandelt, hat das Problem der laufenden Änderungen gelöst, bevor er eine einzige Zeile Delta-Logik geschrieben hat.

Downtime ist keine Eigenschaft eines Werkzeugs, sondern eine Entscheidung über die Architektur des Umzugs, und sie fällt vor dem ersten Transfer. Sobald der Transfer nicht mehr ins Wartungsfenster passt, ist Stufe 2 für die Migration von SQL Server nach PostgreSQL der richtige Kompromiss: Minuten statt Stunden, mit Mitteln, die SQL Server selbst mitbringt. Was dabei realistisch erreichbar ist, heißt downtime-arm, und das Restfenster beim Umschalten gehört ins Drehbuch und in die Zusage an den Fachbereich, nicht in die Fußnote.

FAQ

Geht eine Datenbank-Migration ganz ohne Downtime?

Nein, auch nicht mit Replikation. Selbst wenn beide Datenbanken bis zur letzten Zeile synchron sind, müssen beim Umschalten die Verbindungen zur Quelle auslaufen, der letzte Rückstand muss abgebaut und die Sequenzen müssen nachgezogen werden. Das dauert Sekunden bis wenige Minuten. Ehrlich erreichbar ist „downtime-arm“.

Meine Tabellen haben keine rowversion– oder Zeitstempel-Spalte. Geht Delta-Sync trotzdem?

Ja, auf zwei Wegen. Change Tracking von SQL Server braucht keine zusätzliche Spalte, erkennt zudem Löschungen und funktioniert in allen Editionen. Alternativ lässt sich eine rowversion-Spalte per ALTER TABLE nachrüsten, was bei großen Tabellen allerdings Zeit und Sperren kostet. Tabellen ohne Primärschlüssel bleiben auf beiden Wegen außen vor, denn Change Tracking setzt ihn voraus, und der Upsert im Ziel braucht ihn ohnehin. Sie werden im Wartungsfenster umgezogen.

Bis wann lässt sich ein Cutover abbrechen?

Bis zur Freigabe, und zwar in jeder Stufe ohne Verlust. Solange die Anwendung im Wartungs- oder Read-only-Modus ist, bleibt SQL Server unverändert, und ein Abbruch heißt nur, die Verbindungs-Konfiguration zurückzustellen. Deshalb gehören die Verifikation und ein echter Schreibtest über die Anwendung vor die Freigabe. Danach ist PostgreSQL die einzige produktive Datenbank, und ein Weg zurück wäre eine neue Migration.

Reicht Backup und Restore statt Replikation?

Für den Umzug nicht, für die Vorbereitung sehr wohl. Ein SQL-Server-Backup lässt sich nur in SQL Server wiederherstellen, nicht in PostgreSQL. Ein Restore auf eine Testinstanz ist aber das richtige Mittel, um die Dauer des Transfers zu messen und den ganzen Cutover zu proben, bevor er am Produktivsystem stattfindet.

Was ist der Unterschied zwischen Change Tracking und Change Data Capture?

Change Tracking hält synchron fest, welche Schlüssel sich geändert haben und ob es ein Insert, Update oder Delete war, ohne die Werte zu speichern. Es ist in allen Editionen enthalten und genügt für einen Delta-Sync. Change Data Capture liest asynchron das Transaktionsprotokoll und speichert die geänderten Zeilen mit ihren Spaltenwerten, bei Updates auf Abruf auch das Vorher-Bild. Es braucht die Standard- oder Enterprise-Edition und den SQL Server Agent und ist die Grundlage für Streaming-Werkzeuge wie Debezium. Für einen Delta-Lauf im Takt genügt Change Tracking, erst das Streaming braucht CDC.

Kann Debezium von SQL Server nach PostgreSQL replizieren?

Ja, über eine Zwischenschicht. Der SQL-Server-Connector liest die CDC-Änderungstabellen und streamt jede Änderung nach Kafka. Von dort schreibt der JDBC-Sink-Connector von Debezium die Zeilen nach PostgreSQL. Debezium Server kommt ohne Kafka aus und bringt dafür einen eigenen JDBC-Sink mit. In beiden Fällen braucht die Quelle CDC, also die Standard- oder Enterprise-Edition ab SQL Server 2016 SP1, und einen laufenden SQL Server Agent.

Verwandte Artikel

Dieser Artikel ist Teil einer Serie zur Migration von SQL Server nach PostgreSQL. Die übrigen Teile: