Migration von PostgreSQL zu Microsoft SQL mit mssql-python

Viele Python-Teams lernen zuerst PostgreSQL. Wenn Ihre Arbeitslast Funktionen wie temporale Tabellen, vollständige MERGE Semantik oder Columnstore-Indizes benötigt, migrieren Sie auf Microsoft SQL. Dieser Leitfaden behandelt die wichtigsten Entscheidungen und Codeänderungen für den Umzug einer Python-Anwendung von PostgreSQL (unter Verwendung von psycopg2 oder psycopg3) auf Microsoft SQL mithilfe des Treibersmssql-python.

Note

Wenn Sie von Azure Database for PostgreSQL migrieren, unterstützen beide Dienste Microsoft Entra-Authentifizierung und Managed Identity. Die Codeänderungen in diesem Leitfaden gelten unabhängig davon, ob Ihr PostgreSQL-Quellcode selbstverwaltet oder von Azure gehostet ist.

Was gewinnst du, wenn du zu Microsoft SQL wechselst

Microsoft SQL bietet Funktionen, die Sicherheit, Compliance und Abläufe für Produktionsarbeitslasten vereinfachen. Verstehen Sie diese Funktionen, bevor Sie mit der Migration beginnen, damit Sie sie während des Übergangs nutzen können:

  • Dynamische Datenmaskierung und Sicherheit auf Zeilenebene. Maskiere Spalten für Nutzer, die keinen vollen Zugriff benötigen, und schränke die Zeilensichtbarkeit durch Sicherheitsrichtlinien ein. Diese Funktionen funktionieren mit jedem Fahrer.
  • Zeitliche Tabellen (systemversioniert). Microsoft SQL verfolgt die Zeilenhistorie automatisch. Keine Trigger, keine Audit-Tabellen, kein Anwendungscode.
  • Vollständige MERGE Semantik. Eine einzelne Aussage behandelt INSERT, UPDATE, und DELETE mit einer OUTPUT-Klausel für Audit-Trails. Die ON CONFLICT Klausel von PostgreSQL deckt nur das Einfügen oder Aktualisieren einer einzelnen Einschränkung ab.
  • Columnstore-Indizes. Fügen Sie bestehenden Tabellen für hybride OLTP/Analytics-Workloads einen Spaltenspeicher hinzu. Keine separate Analysedatenbank erforderlich.
  • Microsoft Entra ID-Authentifizierung. Verbinden Sie sich mit verwalteten Identitäten, Service-Principals oder interaktiver Anmeldung. Azure Database for PostgreSQL unterstützt ebenfalls Microsoft Entra-Authentifizierung, sodass der Übergang unkompliziert ist, wenn Sie sie bereits nutzen.

Installieren des Treibers

Bevor du anfängst, stelle sicher, dass du Python 3.10 oder später hast und eine Ziel-SQL-Datenbank.

Erstellen einer SQL-Datenbank

Erstellen oder verbinden Sie sich mit einer SQL-Datenbank auf einer der folgenden Plattformen:

PostgreSQL-Treiber benötigen externe native Bibliotheken.

# psycopg2 requires pg_config, libpq-dev, and platform-specific build tools
sudo apt-get install libpq-dev  # Debian/Ubuntu
pip install psycopg2

Der Treiber mssql-python bündelt seine native Schicht. Unter Windows brauchst du keinen externen Treibermanager oder Systempakete.

pip install mssql-python

Installieren Sie unter Linux und macOS eine kleine Anzahl von Systembibliotheken, die in Installation dokumentiert sind. Es gibt kein Äquivalent zu pg_config oder libpq-dev.

Verbindungscode aktualisieren

Die folgenden Abschnitte behandeln die Schlüsseländerungen an Verbindungszeichenfolgen, Authentifizierung, Kontextmanager und Pooling.

Verbindungszeichenfolgen

psycopg2 verwendet eine DSN-Zeichenkette oder Schlüsselwortargumente.

import psycopg2

conn = psycopg2.connect(
    host="<server>",
    dbname="<database>",
    user="<username>",
    password="<password>"
)

mssql-python unterstützt außerdem Schlüsselwortargumente, was die URL-Codierungsprobleme vermeidet, die SQLAlchemy-Verbindungszeichenketten oft haben, wenn Passwörter , @, oder ; Zeichen enthalten{}.

import mssql_python

conn = mssql_python.connect(
    server="<server>.database.windows.net",
    database="<database>",
    authentication="ActiveDirectoryDefault",
    encrypt="yes"
)

Oder verwenden Sie eine Verbindungszeichenfolge.

conn = mssql_python.connect(
    "Server=<server>.database.windows.net;"
    "Database=<database>;"
    "Authentication=ActiveDirectoryDefault;"
    "Encrypt=yes;"
)

Für den vollständigen Satz der Verbindungszeichenfolge-Schlüsselwörter siehe Connection strings.

Authentifizierung

Die PostgreSQL-Authentifizierung verwendet pg_hba.conf typischerweise Regeln mit Benutzername und Passwort. Azure Database for PostgreSQL unterstützt außerdem die Microsoft Entra-Authentifizierung. Microsoft SQL unterstützt mehrere Authentifizierungsmodi über ein einziges Verbindungsschlüsselwort:

PostgreSQL-Ansatz MSSQL-Python-Äquivalent
Benutzername und Kennwort UID=...;PWD=...;
SSL/TLS-Verschlüsselung Encrypt=yes;(standardmäßig für Azure SQL aktiviert)
Entra auth (Azure PostgreSQL) Authentication=ActiveDirectoryDefault; (passwortlos)
Managed identity (Azure PostgreSQL) Authentication=ActiveDirectoryMSI;
Service principal (Azure PostgreSQL) Authentication=ActiveDirectoryServicePrincipal;

Verwenden Sie ActiveDirectoryDefault für die lokale Entwicklung. Es durchläuft automatisch Azure CLI, Umgebungsvariablen und die verwaltete Identität. Für die Produktion verwenden Sie einen bestimmten Modus wie ActiveDirectoryMSI (verwaltete Identität) oder ActiveDirectoryServicePrincipal um den langsamen Credential-Chain Walk zu vermeiden. Siehe Microsoft Entra-Authentifizierung für alle sieben Authentifizierungsmodi.

Kontextmanager

Beide Treiber unterstützen Kontextmanager, aber das Verhalten unterscheidet sich:

psycopg2s with conn: führt bei Erfolg einen Commit durch und bei einer Ausnahme ein Rollback, schließt aber die Verbindung nicht:

with psycopg2.connect(...) as conn:
    with conn.cursor() as cur:
        cur.execute("INSERT INTO ...")
    # conn.commit() happens automatically on success
# Connection is still open here
conn.close()  # Must close explicitly

Das with conn: von mssql-python schließt die Verbindung beim Beenden. Ungebundene Arbeit wird zurückgedreht:

with mssql_python.connect(...) as conn:
    with conn.cursor() as cursor:
        cursor.execute("INSERT INTO ...")
    conn.commit()
# Connection is closed here

Verbindungspooling

psycopg2 erfordert eine explizite Einrichtung und Verwaltung eines Verbindungspools.

from psycopg2 import pool

connection_pool = pool.ThreadedConnectionPool(1, 10, dsn="...")
conn = connection_pool.getconn()
# ... use conn ...
connection_pool.putconn(conn)

Der Treiber mssql-python hat standardmäßig eingebautes Pooling aktiviert. Es ist keine Einrichtung nötig.

# Pooling is automatic. Each connect() call reuses pooled connections.
conn = mssql_python.connect(...)

Konfiguriere die Poolgröße, falls die Standardeinstellungen nicht zu deiner Arbeitslast passen.

import mssql_python

mssql_python.pooling(max_size=20, idle_timeout=300)

Für Hinweise zur Poolgröße und zur Problembehebung der Pool-Erschöpfung siehe Connection Pooling.

Unterschiede im SQL-Dialekt

Die folgende Tabelle ordnet gängige PostgreSQL-Muster ihren Transact-SQL (T-SQL)-Äquivalenten zu:

PostgreSQL SQL Server (T-SQL) Hinweise
SERIAL / BIGSERIAL int IDENTITY(1,1) Microsoft SQL verwendet IDENTITY für Autoinkrement.
TEXT nvarchar(max) Verwenden Sie nvarchar für Unicode. Bevorzugt nvarchar(4000) oder kürzer, wenn die Daten es erlauben.
BOOLEAN bit PostgreSQL akzeptiert true/false; Microsoft SQL verwendet 1/0.
BYTEA varbinary(max) Dasselbe Konzept, anderer Name.
JSONB nvarchar(max) mit JSON-Funktionen Microsoft SQL speichert JSON als Text und validiert mit ISJSON(). Siehe JSON-Daten.
TIMESTAMP WITH TIME ZONE datetimeoffset Beide speichern den Offset. Siehe Datetime-Handling.
INTERVAL Keine direkte Entsprechung Berechne mit DATEADD() und DATEDIFF().
ARRAY Keine direkte Entsprechung Verwenden Sie eine separate Tabelle, ein JSON-Array oder STRING_SPLIT().
UUID uniqueidentifier Der mssql-python Treiber ordnet uuid.UUID nativ zu. Siehe Modulkonfiguration.
NOW() / CURRENT_TIMESTAMP GETDATE() oder SYSDATETIME() SYSDATETIME() führt zu höherer Präzision.
LIMIT 10 OFFSET 20 OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY Erfordert eine ORDER BY Klausel.
\|\| (Saitenkonkat) + oder CONCAT() CONCAT() verarbeitet NULL Werte.
COALESCE(a, b) COALESCE(a, b) oder ISNULL(a, b) COALESCE ist in beiden identisch.
string_agg(col, ',') STRING_AGG(col, ',') Verfügbar in SQL Server 2017+.
RETURNING id OUTPUT INSERTED.id Verwenden Sie OUTPUT in der INSERT-, UPDATE- oder DELETE-Anweisung.
ON CONFLICT ... DO UPDATE MERGE-Anweisung MERGE unterstützt INSERT + UPDATE + DELETE in einer Anweisung. Siehe Abfrage-Umschreibmuster.
EXPLAIN ANALYZE SET STATISTICS IO ON; SET STATISTICS TIME ON; Oder verwenden Sie Ausführungspläne in SSMS / Azure Data Studio.
\d tablename sp_help 'tablename' Oder eine Abfrage starten INFORMATION_SCHEMA.COLUMNS.
pg_dump bcp, BACKUP DATABASE Verwenden Sie bulkcopy() zum programmgesteuerten Laden von Daten aus Python.

CREATE TABLE Beispiel

PostgreSQL:

CREATE TABLE IF NOT EXISTS products (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    price NUMERIC(10, 2) DEFAULT 0.0,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    metadata JSONB,
    is_active BOOLEAN DEFAULT TRUE
);

SQL Server:

IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'products')
CREATE TABLE products (
    id int IDENTITY(1,1) PRIMARY KEY,
    name nvarchar(100) NOT NULL,
    price decimal(10,2) DEFAULT 0.0,
    created_at datetimeoffset DEFAULT SYSDATETIMEOFFSET(),
    metadata nvarchar(max),
    is_active bit DEFAULT 1
);

Abfrage-Umschreibmuster

Die folgenden Abschnitte zeigen gängige PostgreSQL-Abfragemuster und deren T-SQL-Entsprechungen.

Paginierung

PostgreSQL:

cursor.execute("SELECT * FROM products ORDER BY name LIMIT %s OFFSET %s", (10, 20))

mssql-python:

cursor.execute(
    "SELECT * FROM Production.Product ORDER BY Name OFFSET ? ROWS FETCH NEXT ? ROWS ONLY",
    (20, 10)
)

Die Reihenfolge der Parameter ist umgekehrt. Microsoft SQL stellt OFFSET vor FETCH NEXT.

Upsert (Einfügen oder Aktualisieren)

PostgreSQLs ON CONFLICT verarbeitet Einfüge- oder Aktualisierungsvorgänge bei nur einer Constraint:

cursor.execute("""
    INSERT INTO settings (key, value)
    VALUES (%s, %s)
    ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
""", (key, value))

Microsoft SQL MERGE verwaltet INSERT, UPDATE, und DELETE in einer Anweisung. Verwenden Sie eine USING Klausel mit Parameteraliasen:

cursor.execute("""
    MERGE #Settings AS target
    USING (SELECT ? AS [key], ? AS value) AS source
    ON target.[key] = source.[key]
    WHEN MATCHED THEN UPDATE SET value = source.value
    WHEN NOT MATCHED THEN INSERT ([key], value) VALUES (source.[key], source.value);
""", (key, value))

Für Bulk-Upserts werden die Zeilen mithilfe von bulkcopy() in einer temporären Tabelle zwischengespeichert und dann daraus mit MERGE verarbeitet. Weitere Informationen finden Sie unter Massen-Upsert mit einer Staging-Tabelle.

Setze einen Ausweis ein

PostgreSQL:

cursor.execute(
    "INSERT INTO products (name) VALUES (%s) RETURNING id",
    ("Widget",)
)
product_id = cursor.fetchone()[0]

mssql-python:

cursor.execute(
    "INSERT INTO #Products (Name) OUTPUT INSERTED.ProductID VALUES (%(name)s)",
    {"name": "Widget"}
)
product_id = cursor.fetchval()

OUTPUT INSERTED funktioniert mit INSERT, UPDATE, und DELETE Anweisungen. Es kann mehrere Spalten zurückgeben.

Parametermarkierungen

psycopg2 verwendet %s für positionsbezogene Parameter und %(name)s für benannte Parameter. Der mssql-python Treiber verwendet ? für positionale und %(name)s für benannte Parameter:

psycopg2:

cursor.execute("SELECT * FROM products WHERE id = %s", (42,))
cursor.execute("SELECT * FROM products WHERE id = %(id)s", {"id": 42})

mssql-python:

cursor.execute("SELECT * FROM Production.Product WHERE ProductID = ?", (42,))
cursor.execute(
    "SELECT * FROM Production.Product WHERE ProductID = %(id)s", {"id": 42}
)

Unterschiede zwischen Transaktionen und Autocommit

PostgreSQL (psycopg2) öffnet eine Transaktion automatisch beim ersten Befehl und erfordert ein explizites commit():

conn = psycopg2.connect(...)
cursor = conn.cursor()
cursor.execute("INSERT INTO ...")
conn.commit() 

Der Treiber mssql-python funktioniert standardmäßig genauso. Autocommit ist ausgeschaltet, und du rufst commit() explizit:

conn = mssql_python.connect(...)
cursor = conn.cursor()
cursor.execute("INSERT INTO ...")
conn.commit()

Um Autocommit zu aktivieren:

psycopg2:

conn = psycopg2.connect(...)
conn.autocommit = True

mssql-python:

conn = mssql_python.connect(..., autocommit=True)
# or: conn.autocommit = True

Siehe Transaktionsmanagement für Isolationsstufen, Speicherpunkte und Deadlock-Wiederholungsmuster.

Typenüberlegungen

Die folgenden Abschnitte behandeln die häufigsten Unterschiede in der Typabbildung zwischen PostgreSQL und Microsoft SQL.

JSON

PostgreSQL bietet native Unterstützung für JSONB mit Indexierungs- und Abfrageoperatoren (->, ->>, @>). Microsoft SQL speichert JSON als nvarchar(max) und bietet Funktionen zum Abfragen an:

PostgreSQL SQL Server
data->>'name' JSON_VALUE(data, '$.name')
data->'items' JSON_QUERY(data, '$.items')
data @> '{"active": true}' JSON_VALUE(data, '$.active') = 'true'
jsonb_array_length(data) (SELECT COUNT(*) FROM OPENJSON(data))

In Python verwenden beide Ansätze json.dumps() zum Serialisieren:

import json

cursor.execute(
    "INSERT INTO #Settings ([key], data) VALUES (%(key)s, %(data)s)",
    {"key": "config", "data": json.dumps({"theme": "dark", "lang": "en"})}
)

Siehe JSON-Daten für vollständige Hinweise zu JSON-Speicher- und Abfragemustern.

UUID

Sowohl PostgreSQL als auch mssql-python unterstützen uuid.UUID nativ:

import uuid

cursor.execute(
    "INSERT INTO #Events (EventID, Name) VALUES (%(event_id)s, %(name)s)",
    {"event_id": uuid.uuid4(), "name": "signup"}
)

Siehe Modulkonfiguration für die native_uuid Verbindungsoption.

Datum, Zeit und Zeitzone

PostgreSQL konvertiert TIMESTAMPTZ auf UTC im Speicher. Microsoft SQL datetimeoffset bewahrt den ursprünglichen Offset:

from datetime import datetime, timezone, timedelta

eastern = timezone(timedelta(hours=-5))
dt = datetime(2025, 6, 15, 14, 30, tzinfo=eastern)

# PostgreSQL stores as UTC: 2025-06-15 19:30:00+00
# SQL Server stores as-is: 2025-06-15 14:30:00-05:00
cursor.execute("INSERT INTO #Events (EventTime) VALUES (%(event_time)s)", {"event_time": dt})

Wenn du konsistenten UTC-Speicher brauchst, konvertiere in Python, bevor du einfügst:

dt_utc = dt.astimezone(timezone.utc)
cursor.execute("INSERT INTO #Events (EventTime) VALUES (%(event_time)s)", {"event_time": dt_utc})

Siehe Datetime-Handling für die vollständige Typzuordnung.

Felder

PostgreSQL unterstützt native Array-Spalten (INTEGER[], TEXT[]). Microsoft SQL hat keinen Array-Typ. Häufige Alternativen:

  1. Separate Tabelle (normalisiert). Am besten für abfragbare, indexierte Daten.
  2. JSON-Array in nvarchar(max) gespeichert. Gut für undurchsichtige Metadaten.
  3. Durch Kommas getrennte Zeichenkette mit STRING_SPLIT(). Einfach, aber begrenzt.
# Option 1: Normalized table
cursor.execute("INSERT INTO #ProductTags (ProductID, Tag) VALUES (%(product_id)s, %(tag)s)", {"product_id": 1, "tag": "electronics"})
cursor.execute("INSERT INTO #ProductTags (ProductID, Tag) VALUES (%(product_id)s, %(tag)s)", {"product_id": 1, "tag": "sale"})

# Option 2: JSON array
import json
tags = json.dumps(["electronics", "sale"])
cursor.execute("INSERT INTO #Products (Name, Tags) VALUES (%(name)s, %(tags)s)", {"name": "Widget", "tags": tags})

Unicode

PostgreSQL speichert standardmäßig alle Texte als UTF-8. Microsoft SQL unterscheidet zwischen varchar (Codepage-Codierung) und nvarchar (UTF-16). Der Treiber mssql-python sendet standardmäßig Python-Werte str als nvarchar, sodass Unicode-Text ohne zusätzliche Konfiguration funktioniert. Wenn dein Schema Varchar-Spalten verwendet und du die implizite Umwandlung vermeiden musst, gib setinputsizes() den Spaltentyp an. Siehe String- und Unicode-Daten für Kodierungsdetails.

Massenladen und Datenbewegung

PostgreSQL verwendet COPY für Massenoperationen. mssql-python bietet bulkcopy():

psycopg2:

with open("data.csv") as f:
    cursor.copy_expert("COPY products FROM STDIN CSV HEADER", f)

mssql-python:

import csv

with open("data.csv", newline="") as f:
    reader = csv.reader(f)
    next(reader)  # Skip header
    rows = [tuple(row) for row in reader]

cursor.bulkcopy("##Products", rows)

Für große Dateien verwenden Sie einen Generator, um zu vermeiden, dass die gesamte Datei in den Speicher geladen wird:

import csv

def csv_rows(path):
    with open(path, newline="") as f:
        reader = csv.reader(f)
        next(reader)  # Skip header
        for row in reader:
            yield tuple(row)

cursor.bulkcopy("##Products", csv_rows("data.csv"), batch_size=5000)

Siehe Massenkopieroperationen für Spaltenabbildungen, Identitätshandhabung und Performance-Tipps.

Schema- und Datenmigration

Verwenden Sie diesen Ansatz, um eine bestehende PostgreSQL-Datenbank zu migrieren:

  1. Exportiere das Schema. Verwenden Sie pg_dump --schema-only, um DDL zu erhalten. Für Details zu Optionen und Randfällen (Eigentum, Privilegien, Erweiterungen und Filterung) siehe die PostgreSQL-Referenzpg_dump. Schreibe die DDL mit der SQL-Dialektunterschiedstabelle um.
  2. Erstellen Sie Tabellen in Microsoft SQL. Führen Sie die umgeschriebene DDL gegen Ihre Zieldatenbank aus.
  3. Exportieren von Daten. Verwende pg_dump --data-only --format=csv oder frage jede Tabelle mit psycopg2 ab. Für große Datensätze und Kompatibilitätsswitches lesen Sie die PostgreSQL-Dokumentationpg_dump, insbesondere den Optionsbereich.
  4. Daten mit Bulkcopy laden. Lies die Reihenfolge der Zielspalten aus dem Katalog, damit du keine Spaltenliste pro Tabelle festkodieren musst, und streame dann jede Tabelle in Microsoft SQL. Hier ist ein Beispielskript:
import json
import psycopg2
from psycopg2 import sql
import mssql_python

pg_conn = psycopg2.connect(host="<pgserver>", dbname="<database>", user="<username>", password="<password>")
sql_conn = mssql_python.connect(
    server="<server>.database.windows.net",
    database="<database>",
    authentication="ActiveDirectoryDefault",
    encrypt="yes"
)

def table_columns(cursor, table):
    """Return the ordered column names and identity column from the catalog."""
    cursor.execute(
        "SELECT c.name, c.is_identity FROM sys.columns AS c "
        "WHERE c.object_id = OBJECT_ID(?) ORDER BY c.column_id",
        (table,)
    )
    columns, identity = [], None
    for name, is_identity in cursor.fetchall():
        columns.append(name)
        if is_identity:
            identity = name
    return columns, identity

def parse_pg_table_name(qualified_name):
    """Split a PostgreSQL table name into schema and table parts."""
    if "." in qualified_name:
        schema_name, table_name = qualified_name.split(".", 1)
    else:
        schema_name, table_name = "public", qualified_name
    return schema_name, table_name

def parse_sql_table_name(qualified_name):
    """Split a SQL Server table name into schema and table parts."""
    if "." in qualified_name:
        schema_name, table_name = qualified_name.split(".", 1)
    else:
        schema_name, table_name = "dbo", qualified_name
    return schema_name, table_name

def dependency_order(pg_cursor, table_names, schema_name="public"):
    """Topologically sort tables by foreign key dependencies."""
    table_set = set(table_names)
    incoming = {name: 0 for name in table_set}
    edges = {name: set() for name in table_set}

    pg_cursor.execute(
        """
        SELECT
            child.relname AS child_table,
            parent.relname AS parent_table
        FROM pg_constraint c
        JOIN pg_class child ON c.conrelid = child.oid
        JOIN pg_namespace child_ns ON child.relnamespace = child_ns.oid
        JOIN pg_class parent ON c.confrelid = parent.oid
        JOIN pg_namespace parent_ns ON parent.relnamespace = parent_ns.oid
        WHERE c.contype = 'f'
          AND child_ns.nspname = %s
          AND parent_ns.nspname = %s
        """,
        (schema_name, schema_name),
    )

    for child, parent in pg_cursor.fetchall():
        if child in table_set and parent in table_set and child != parent:
            if child not in edges[parent]:
                edges[parent].add(child)
                incoming[child] += 1

    ready = sorted([name for name, degree in incoming.items() if degree == 0])
    ordered = []

    while ready:
        current = ready.pop(0)
        ordered.append(current)
        for neighbor in sorted(edges[current]):
            incoming[neighbor] -= 1
            if incoming[neighbor] == 0:
                ready.append(neighbor)
        ready.sort()

    # If cycles remain, process remaining tables alphabetically.
    if len(ordered) < len(table_set):
        remaining = sorted(table_set - set(ordered))
        ordered.extend(remaining)

    return ordered

def discover_table_pairs(pg_cursor, sql_cursor, pg_schema="public", sql_schema="dbo"):
    """Find tables that exist in both PostgreSQL and SQL Server, in dependency order."""
    pg_cursor.execute(
        """
        SELECT table_name
        FROM information_schema.tables
        WHERE table_schema = %s AND table_type = 'BASE TABLE'
        """,
        (pg_schema,),
    )
    pg_tables = {row[0] for row in pg_cursor.fetchall()}

    sql_cursor.execute(
        """
        SELECT t.name
        FROM sys.tables AS t
        JOIN sys.schemas AS s ON t.schema_id = s.schema_id
        WHERE s.name = ?
        """,
        (sql_schema,),
    )
    sql_tables = {row[0] for row in sql_cursor.fetchall()}

    common_tables = sorted(pg_tables & sql_tables)
    ordered_tables = dependency_order(pg_cursor, common_tables, schema_name=pg_schema)

    return [(f"{pg_schema}.{name}", f"{sql_schema}.{name}") for name in ordered_tables]

def source_columns(pg_cursor, source_table):
    """Return ordered source columns from PostgreSQL information_schema."""
    schema_name, table_name = parse_pg_table_name(source_table)
    pg_cursor.execute(
        """
        SELECT column_name
        FROM information_schema.columns
        WHERE table_schema = %s AND table_name = %s
        ORDER BY ordinal_position
        """,
        (schema_name, table_name),
    )
    return [row[0] for row in pg_cursor.fetchall()]

def migrate_table(pg_cursor, sql_cursor, source_table, dest_table):
    # The destination defines the authoritative column order for positional bulkcopy().
    dest_columns, identity = table_columns(sql_cursor, dest_table)
    if not dest_columns:
        raise RuntimeError(
            f"No destination columns found for {dest_table}. "
            "Make sure the destination table exists before migration."
        )

    src_columns = source_columns(pg_cursor, source_table)
    if not src_columns:
        raise RuntimeError(
            f"No source columns found for {source_table}. "
            "Check the source table name and schema."
        )

    # Load only columns present on both sides and keep destination column order.
    src_column_set = set(src_columns)
    load_columns = [c for c in dest_columns if c in src_column_set]
    if not load_columns:
        raise RuntimeError(
            f"No shared columns between {source_table} and {dest_table}."
        )

    source_schema, source_name = parse_pg_table_name(source_table)
    select_query = sql.SQL("SELECT {cols} FROM {schema}.{table}").format(
        cols=sql.SQL(", ").join(sql.Identifier(c) for c in load_columns),
        schema=sql.Identifier(source_schema),
        table=sql.Identifier(source_name),
    )
    pg_cursor.execute(select_query)

    copied = 0
    while True:
        batch = pg_cursor.fetchmany(10000)
        if not batch:
            break
        # Serialize JSONB or array values (dict/list) for nvarchar(max) columns.
        rows = [
            tuple(json.dumps(v) if isinstance(v, (dict, list)) else v for v in row)
            for row in batch
        ]
        # keep_identity preserves source primary keys so foreign keys still line up.
        result = sql_cursor.bulkcopy(
            dest_table,
            rows,
            batch_size=10000,
            keep_identity=identity in load_columns,
        )
        copied += result["rows_copied"]
    return copied

pg_cursor = pg_conn.cursor()
sql_cursor = sql_conn.cursor()

# Leave TABLE_MAPPINGS as None to migrate every table that exists in both schemas.
# To migrate only selected tables, replace None with explicit mappings.
TABLE_MAPPINGS = None

if TABLE_MAPPINGS is None:
    tables = discover_table_pairs(pg_cursor, sql_cursor, pg_schema="public", sql_schema="dbo")
else:
    tables = TABLE_MAPPINGS

if not tables:
    raise RuntimeError(
        "No shared tables found between source and destination schemas. "
        "Check schema names and table creation on SQL Server."
    )

print(f"Migrating {len(tables)} table(s)...")
for source_table, dest_table in tables:
    count = migrate_table(pg_cursor, sql_cursor, source_table, dest_table)
    print(f"{dest_table}: copied {count} rows")

# bulkcopy() bypasses constraint checks, so foreign keys are left untrusted.
# Re-validate each table to mark them trusted and surface any orphaned rows.
for _, dest_table in tables:
    dest_schema, dest_name = parse_sql_table_name(dest_table)
    sql_cursor.execute(
        f"ALTER TABLE [{dest_schema}].[{dest_name}] WITH CHECK CHECK CONSTRAINT ALL"
    )
sql_conn.commit()

pg_conn.close()
sql_conn.close()

Standardmäßig migriert dieses Skript jede Tabelle, die sowohl in public (PostgreSQL) als auch in dbo (SQL Server) vorhanden ist, in der Reihenfolge ihrer Fremdschlüsselabhängigkeiten. Setze TABLE_MAPPINGS auf eine explizite Liste, wenn du nur eine Teilmenge migrieren möchtest.

Dies setzt voraus, dass Quelle und Ziel dieselben Spaltennamen verwenden, was nach dem Umschreiben der DDL üblich ist. Der Helfer verarbeitet die Identitätsspalte automatisch: keep_identity behält die Primärschlüssel der Quelle bei, wenn das Ziel über eine IDENTITY-Spalte verfügt, damit Fremdschlüsselverweise erhalten bleiben. Um SQL Server stattdessen neue Schlüssel zuweisen zu lassen, schließen Sie die Identity-Spalte aus columns aus und geben Sie keep_identity=False an.

Fremdschlüssel und Einschränkungen

bulkcopy() verwendet das TDS-Bulk-Insert-Protokoll, das beim Laden weder Fremdschlüssel- noch Prüfbeschränkungen erzwingt. Ohne eine ausdrückliche Anforderung, sie zu überprüfen, ignoriert SQL Server beim Massenimport CHECK- und FOREIGN KEY-Constraints und markiert sie anschließend als nicht vertrauenswürdig, wie in BULK INSERT beschrieben. Dieses Verhalten hat zwei praktische Folgen für die Migration:

  • Die Ladereihenfolge spielt keine Rolle. Du kannst eine Kindertabelle vor ihrem Elternteil laden, ohne auf Fremdschlüsselverstöße zu stoßen. Bewahren Sie die Primärschlüssel mit keep_identity=True, wie es der Helfer tut, damit Eltern- und Kindschlüsselwerte auch nach dem Laden übereinstimmen.
  • Einschränkungen werden letztlich als nicht vertrauenswürdig eingestuft. Nach einer Massenladung wird jeder Fremdschlüssel als nicht vertrauenswürdig (sys.foreign_keys.is_not_trusted = 1) markiert, weil SQL Server ihn nicht überprüft hat. Der letzte Schritt im Skript validiert jede geladene Tabelle erneut mit ALTER TABLE ... WITH CHECK CHECK CONSTRAINT ALL. Dieser Schritt markiert die vertrauenswürdigen Constraints, damit der Abfrageoptimierer sie verwenden kann, und zeigt fehlerhafte Daten auf. Wenn eine Kindzeile auf einen fehlenden Elternteil verweist, scheitert die Anweisung mit einer Integritätsbedingungsverletzung, die die Einschränkung benannt, sodass du die verwaisten Zeilen vor dem Live-Gehen beheben kannst.

Einschränkungen

Überprüfen Sie diese Unterschiede, bevor Sie migrieren:

Thema PostgreSQL mssql-python / SQL Server
callproc() Unterstützt Erhöhungen NotSupportedError. Verwenden Sie stattdessen cursor.execute("EXECUTE ...").
Tabellenwertparameter (TVPs) Keine direkte Entsprechung Wird im aktuellen Treiber nicht unterstützt. Verwenden Sie temporäre Tabellen oder JSON für mehrzeilige Parameter.
Einheimische Säulen ARRAY Unterstützt Kein Array-Typ. Verwenden Sie normalisierte Tabellen, JSON-Arrays oder STRING_SPLIT().
LISTEN/NOTIFY Unterstützt Keine direkte Entsprechung. Verwenden Sie Service Broker oder Abfragen auf Anwendungsebene.
COPY Streaming Unterstützt Verwenden Sie bulkcopy() zum Laden von Massendaten.
Rückgabe modifizierter Reihen RETURNING-Klausel OUTPUT INSERTED / OUTPUT DELETED Klausel in DML-Erklärungen.
Asynchroner Treiber psycopg3 hat nativ asynchron mssql-python Die asynchrone Unterstützung ist Workaround-orientiert (Thread Pool).
Volltextsuche tsvector / tsquery CONTAINS() / FREETEXT() mit Volltext-Indexen.
ORM (SQLAlchemy) Vollständig unterstützt Unterstützt über den integrierten mssql-python-Dialekt in SQLAlchemy 2.1.0b2+ (Vorabversion).

Überprüfungsprüfliste

Verwenden Sie diese Checkliste, um Ihre Migration zu überprüfen:

  1. Ersetzen Sie alle %s Parametermarker durch ? oder %(name)s Parameter.
  2. Stellen Sie sicher, dass alle %(name)s Parameter weiterhin funktionieren (beide Treiber unterstützen dieses Format).
  3. Umschreiben LIMIT/OFFSET auf .OFFSET/FETCH NEXT
  4. Umschreiben RETURNING auf OUTPUT INSERTED.
  5. Umschreiben ON CONFLICT auf MERGE.
  6. Ersetzen Sie SERIAL / BIGSERIAL durch .IDENTITY
  7. BOOLEAN Spalten wurden durch bit ersetzt.
  8. Ersetzen Sie Array-Spalten durch normalisierte Tabellen oder JSON.
  9. Ersetze JSONB Operatoren durchJSON_VALUE() / JSON_QUERY() .
  10. Aktualisieren Sie die Verbindungszeichenfolge für die Microsoft SQL-Authentifizierung.
  11. Teste die Anwendung mit AdventureWorks oder deinem Zielschema.

Authentifizierung und Bereitstellung

Selbstverwaltete PostgreSQL-Anwendungen werden typischerweise mit Verbindungszeichen, die Passwörter enthalten, bereitgestellt oder verwenden .pgpass Dateien und PGPASSWORD Umweltvariablen. Azure Database for PostgreSQL unterstützt Microsoft Entra-Authentifizierung, sodass dasselbe Identitätsmodell auf Azure SQL übertragen wird, wenn Sie bereits passwortlose Auth verwenden.

Für Produktionsworkloads gegen Azure SQL verwenden Sie Managed Identity:

conn = mssql_python.connect(
    server="<server>.database.windows.net",
    database="AdventureWorks",
    authentication="ActiveDirectoryMSI",
    encrypt="yes"
)

Für lokale Entwicklung und CI siehe Container und lokale Entwicklung für Docker-, devcontainer- und CI-Pipeline-Setup-Patterns.