Migra da PostgreSQL a Microsoft SQL con mssql-python

Molti team che usano Python imparano a usare PostgreSQL come primo database. Quando il tuo carico di lavoro ha bisogno di funzionalità come tabelle temporali, semantica completa MERGE o indici di colonna, migra su Microsoft SQL. Questa guida copre le principali decisioni e modifiche al codice per spostare un'applicazione Python da PostgreSQL (usando psycopg2 o psycopg3) a Microsoft SQL utilizzando il mssql-python driver.

Note

Se stai migrando da Database di Azure per PostgreSQL, entrambi i servizi supportano l'autenticazione Microsoft Entra e l'identità gestita. Le modifiche al codice in questa guida si applicano indipendentemente dal fatto che il tuo codice sorgente PostgreSQL sia autogestito o ospitato su Azure.

Cosa guadagni passando a Microsoft SQL

Microsoft SQL include funzionalità che semplificano sicurezza, conformità e operazioni per i carichi di lavoro di produzione. Comprendi queste funzionalità prima di iniziare la migrazione così potrai sfruttarle durante la transizione:

  • Mascheramento dinamico dei dati e sicurezza a livello di riga. Maschera le colonne per gli utenti che non necessitano di accesso completo e limita la visibilità delle righe in base ai criteri di sicurezza. Queste caratteristiche funzionano con qualsiasi guidatore.
  • Tabelle temporali (versionate al sistema). Microsoft SQL traccia automaticamente la cronologia delle righe. Nessun trigger, nessuna tabella di audit, nessun codice applicativo.
  • Semantica MERGE completa. Una singola istruzione gestisce INSERT, UPDATE e DELETE con una clausola OUTPUT per gli audit trail. La clausola di ON CONFLICT PostgreSQL copre solo inserimento o aggiornamento su un singolo vincolo.
  • Indici Colonne. Aggiungi storage a colonne alle tabelle esistenti per carichi di lavoro ibridi OLTP/analytics. Non serve un database di analisi separato.
  • Autenticazione ID di Microsoft Entra. Connettiti con identità gestite, responsabili di servizio o accesso interattivo. Database di Azure per PostgreSQL supporta anche l'autenticazione Microsoft Entra, quindi se lo stai già usando, la transizione è semplice.

Installare il driver

Prima di iniziare, assicurati di avere Python 3.10 o successiva e un database SQL di destinazione.

Creare un database SQL

Crea o collegati a un database SQL su una delle seguenti piattaforme:

I driver PostgreSQL richiedono librerie native esterne.

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

Il mssql-python driver raggruppa il suo strato nativo. Su Windows, non serve un gestore di driver esterno né pacchetti di sistema.

pip install mssql-python

Su Linux e macOS, installa un piccolo insieme di librerie di sistema documentate in Installazione. Non esiste un equivalente a pg_config o libpq-dev.

Aggiorna il codice di connessione

Le sezioni seguenti trattano i cambiamenti chiave nelle stringhe di connessione, nell'autenticazione, nei gestori di contesto e nel pooling.

Stringhe di connessione

psycopg2 utilizza una stringa DSN o argomenti per parole chiave.

import psycopg2

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

MSSQL-Python supporta anche argomenti per parole chiave, evitando così i problemi di codifica URL che spesso hanno le stringhe di connessione SQLAlchemy quando le password contengono @, ;, o {} caratteri.

import mssql_python

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

Oppure usa una stringa di connessione.

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

Per l'insieme completo delle parole chiave delle stringa di connessione, vedi Connection strings.

Authentication

L'autenticazione PostgreSQL tipicamente utilizza pg_hba.conf regole con nome utente e password. Database di Azure per PostgreSQL supporta anche l'autenticazione Microsoft Entra. Microsoft SQL supporta più modalità di autenticazione tramite una singola parola chiave di connessione:

Approccio PostgreSQL Equivalente MSSQL-Python
Nome utente e password UID=...;PWD=...;
Crittografia SSL/TLS Encrypt=yes;(abilitato di default per Azure SQL)
Entra auth (Azure PostgreSQL) Authentication=ActiveDirectoryDefault; (senza password)
Managed identity (Azure PostgreSQL) Authentication=ActiveDirectoryMSI;
Service principal (Azure PostgreSQL) Authentication=ActiveDirectoryServicePrincipal;

Usare ActiveDirectoryDefault per lo sviluppo locale. Collega automaticamente attraverso interfaccia della riga di comando di Azure, variabili di ambiente e identità gestita. In produzione, usa una modalità specifica come ActiveDirectoryMSI (identità gestita) o ActiveDirectoryServicePrincipal per evitare la lenta scansione della catena di credenziali. Vedi l'autenticazione Microsoft Entra per tutte e sette le modalità di autenticazione.

Gestori di contesto

Entrambi i driver supportano i gestori di contesto, ma il comportamento differisce:

Il with conn: di psycopg2 esegue il commit in caso di successo e il rollback in caso di eccezione, ma non chiude la connessione:

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

MSSQL-Python with conn: chiude la connessione all'uscita. Il lavoro non confermato viene annullato:

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

Pool di connessioni

PsyCopG2 richiede una configurazione e gestione esplicita di un pool di connessioni.

from psycopg2 import pool

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

Il driver mssql-python ha il pooling integrato abilitato per impostazione predefinita. Non serve alcuna configurazione.

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

Configura la dimensione del pool se i valori predefiniti non si adattano al tuo carico di lavoro.

import mssql_python

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

Per indicazioni sul dimensionamento del pool e sulla risoluzione dei problemi di esaurimento del pool, vedere Pooling delle connessioni.

Differenze tra dialetti SQL

La seguente tabella mappa i comuni modelli PostgreSQL ai loro equivalenti Transact-SQL (T-SQL):

PostgreSQL SQL Server (T-SQL) Notes
SERIAL / BIGSERIAL int IDENTITY(1,1) Microsoft SQL utilizza IDENTITY per l'autoincremento.
TEXT nvarchar(max) Usa nvarchar per Unicode. Preferisci nvarchar(4000) o abbrevia quando i dati lo permettono.
BOOLEAN bit PostgreSQL accetta true/false; Microsoft SQL utilizza 1/0.
BYTEA varbinary(max) Stesso concetto, nome diverso.
JSONB nvarchar(max) con funzioni JSON Microsoft SQL memorizza JSON come testo e valida con ISJSON(). Vedi dati JSON.
TIMESTAMP WITH TIME ZONE datetimeoffset Entrambi memorizzano l'offset. Vedi Gestione di data e ora.
INTERVAL Nessun equivalente diretto Calcolare con DATEADD() e DATEDIFF().
ARRAY Nessun equivalente diretto Usa una tabella separata, un array JSON, oppure STRING_SPLIT().
UUID uniqueidentifier Il driver mssql-python mappa nativamente uuid.UUID. Vedi configurazione del modulo.
NOW() / CURRENT_TIMESTAMP GETDATE() oppure SYSDATETIME() SYSDATETIME() dà una precisione superiore.
LIMIT 10 OFFSET 20 OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY Richiede una ORDER BY clausola.
\|\| (concatenazione di stringhe) + oppure CONCAT() CONCAT() gestisce NULL i valori.
COALESCE(a, b) COALESCE(a, b) oppure ISNULL(a, b) COALESCE è identica in entrambi.
string_agg(col, ',') STRING_AGG(col, ',') Disponibile in SQL Server 2017+.
RETURNING id OUTPUT INSERTED.id Usa OUTPUT nell'istruzione INSERT, UPDATE o DELETE.
ON CONFLICT ... DO UPDATE MERGE istruzione MERGE supporta INSERT + UPDATE + DELETE in una sola istruzione. Vedi Modelli di riscrittura delle query.
EXPLAIN ANALYZE SET STATISTICS IO ON; SET STATISTICS TIME ON; Oppure usa i piani di esecuzione in SSMS / Azure Data Studio.
\d tablename sp_help 'tablename' Oppure eseguire una query INFORMATION_SCHEMA.COLUMNS.
pg_dump bcp, BACKUP DATABASE Usa bulkcopy() per il caricamento programmatico dei dati da Python.

CREATE TABLE Esempio

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
);

Modelli di riscrittura delle query

Le sezioni seguenti mostrano i comuni modelli di query PostgreSQL e i loro equivalenti T-SQL.

Pagination

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)
)

L'ordine dei parametri è invertito. Microsoft SQL pone OFFSET prima FETCH NEXTdi .

Upsert (inserire o aggiornare)

PostgreSQL ON CONFLICT gestisce l'inserimento o l'aggiornamento su un unico vincolo:

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

Microsoft SQL MERGE gestisce INSERT, UPDATE, e DELETE in un'unica istruzione. Usa una clausola USING con alias dei parametri:

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))

Per gli upsert in blocco, inserisci le righe in una tabella temporanea utilizzando bulkcopy(), quindi esegui MERGE da tale tabella. Per maggiori informazioni, vedi Bulk upsert con una tabella di staging.

Inserisci un ID

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 funziona con le istruzioni INSERT, UPDATE e DELETE. Può restituire più colonne.

Marcatori di parametro

Psycopg2 utilizza %s parametri posizionali e %(name)s parametri nominati. Il driver mssql-python utilizza ? per i parametri posizionali e %(name)s per i parametri denominati:

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}
)

Differenze tra transazioni e autocommit

PostgreSQL (psycopg2) apre automaticamente una transazione al primo comando e richiede un esplicito commit():

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

Il mssql-python driver funziona allo stesso modo di default. L'autocommit è disattivato, e chiami commit() esplicitamente:

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

Per abilitare l'autocommit:

Psycopg2:

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

MSSQL-Python:

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

Vedi Gestione delle transazioni per livelli di isolamento, punti di salvataggio e pattern di ritentazione di deadlock.

Considerazioni sul tipo

Le sezioni seguenti trattano le differenze più comuni nella mappatura dei tipi tra PostgreSQL e Microsoft SQL.

JSON

PostgreSQL ha operatori nativi JSONB con indicizzazione e query (->, ->>, @>). Microsoft SQL memorizza JSON come nvarchar(max) e fornisce funzioni per la consulta:

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, entrambi gli approcci usano json.dumps() per serializzare:

import json

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

Consulta i dati JSON per indicazioni complete sull'archiviazione JSON e sui pattern di interrogazione.

UUID (Identificatore Unico Universale)

Sia PostgreSQL che mssql-python mappano uuid.UUID nativamente:

import uuid

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

Vedi Configurazione del modulo per l'opzione native_uuid di connessione.

Data, ora e fuso orario

TIMESTAMPTZ di PostgreSQL viene convertito in UTC al momento della memorizzazione. Microsoft SQL datetimeoffset mantiene lo offset originale:

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})

Se hai bisogno di una memoria UTC coerente, converti in Python prima di inserire:

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

Vedi Gestione di datetime per la mappatura completa dei tipi.

Matrici

PostgreSQL supporta colonne native degli array (INTEGER[], TEXT[]). Microsoft SQL non ha un tipo di array. Alternative comuni:

  1. Tabella separata (normalizzata). Migliore per dati indicizzati e interrogabili.
  2. Array JSON memorizzato in nvarchar(max). Buono per i metadati opaci.
  3. Stringa separata da virgola con STRING_SPLIT(). Semplice ma limitato.
# 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 memorizza tutto il testo come UTF-8 di default. Microsoft SQL distingue tra varchar (codifica delle code page) e nvarchar (UTF-16). Il mssql-python driver invia valori Python str come nvarchar di default, quindi il testo Unicode funziona senza configurazioni aggiuntive. Se il tuo schema usa colonne varchar e devi evitare la conversione implicita, usa setinputsizes() per specificare il tipo di colonna. Vedi Stringhe e dati Unicode per i dettagli di codifica.

Caricamento massivo e spostamento dei dati

PostgreSQL utilizza COPY per operazioni in blocco. MSSQL-python fornisce 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)

Per file grandi, usa un generatore per evitare di caricare l'intero file in memoria:

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)

Vedi Operazioni di copia in blocco per mappaggi di colonne, gestione dell'identità e consigli sulle prestazioni.

Migrazione di schema e dati

Usa questo approccio per migrare un database PostgreSQL esistente:

  1. Esporta lo schema. Usa pg_dump --schema-only per ottenere DDL. Per dettagli sulle opzioni e casi limite (proprietà, privilegi, estensioni e filtraggio), vedi il riferimento PostgreSQLpg_dump. Riscrivi il DDL usando la tabella delle differenze dialettali SQL .
  2. Crea tabelle in Microsoft SQL. Esegui il DDL riscritto sul tuo database di destinazione.
  3. Esporta dati. Usa pg_dump --data-only --format=csv o interroga ogni tabella con psycopg2. Per grandi dataset e switch di compatibilità, consulta la documentazione di PostgreSQLpg_dump, in particolare la sezione delle opzioni.
  4. Carica i dati con bulkcopy. Leggi dal catalogo l'ordine delle colonne di destinazione, in modo da non dover definire rigidamente un elenco di colonne per ogni tabella, quindi trasmetti i dati di ogni tabella in Microsoft SQL. Ecco uno script di esempio:
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()

Di default, questo script migra ogni tabella esistente sia public in (PostgreSQL) che dbo in (SQL Server), ordinata per dipendenze di chiave esterna. Imposta TABLE_MAPPINGS su una lista esplicita se vuoi migrare solo un sottoinsieme.

Questo presuppone che la sorgente e la destinazione usino gli stessi nomi di colonne, come di consueto dopo aver riscritto il DDL. L'aiutante gestisce automaticamente la colonna identità: keep_identity preserva le chiavi primarie di origine quando la destinazione ha una IDENTITY colonna, così i riferimenti a chiave esterna rimangono intatti. Per permettere a SQL Server di assegnare nuove chiavi, escludi la colonna identità da columns e passa keep_identity=False.

Chiavi esterne e vincoli

bulkcopy() utilizza il protocollo TDS bulk insert, che non applica vincoli di chiave estranea né verifica durante il carico. Senza una richiesta esplicita di verificare, SQL Server ignora CHECK i FOREIGN KEY vincoli durante un'importazione di massa e li segna come non affidabili successivamente, come descritto in BULK INSERT. Questo comportamento ha due conseguenze pratiche per la migrazione:

  • L'ordine di caricamento non conta. Puoi caricare una tabella figlia prima della tabella padre senza incorrere in violazioni dei vincoli di chiave esterna. Mantieni le chiavi primarie con keep_identity=True, come fa la funzione helper, così i valori delle chiavi padre e figlio continuino a corrispondere dopo il caricamento.
  • I vincoli finiscono per non essere affidabili. Dopo un caricamento in massa, ogni chiave esterna viene contrassegnata come non affidabile (sys.foreign_keys.is_not_trusted = 1) perché SQL Server non l'ha verificata. L'ultimo passaggio dello script riconvalida ogni tabella caricata con ALTER TABLE ... WITH CHECK CHECK CONSTRAINT ALL. Questo passaggio segna i vincoli affidabili affinché l'ottimizzatore di query possa usarli, e fa emergere dati non validi. Se una riga figlia fa riferimento a un genitore mancante, l'istruzione fallisce con una violazione del vincolo di integrità che indica il nome del vincolo, in modo da poter correggere le righe orfane prima di andare in produzione.

Limitazioni

Rivedere queste differenze prima di migrare:

Argomento PostgreSQL mssql-python / SQL Server
callproc() Supportato Aumenta NotSupportedError. Utilizzare invece cursor.execute("EXECUTE ...").
Parametri a valori di tabella (TVP) Nessun equivalente diretto Non supportato dal driver attuale. Usa tabelle temporanee o JSON per i parametri multi-riga.
Colonne native ARRAY Supportato Nessun tipo di array. Usa tabelle normalizzate, array JSON o STRING_SPLIT().
LISTEN/NOTIFY Supportato Nessun equivalente diretto. Usa Service Broker o sondaggi a livello di applicazione.
COPY Streaming Supportato Usare bulkcopy() per il caricamento massivo dei dati.
Restituzione delle righe modificate clausola RETURNING OUTPUT INSERTED / OUTPUT DELETED clausola nelle istruzioni DML.
Driver asincrono psycopg3 supporta l'asincronia in modo nativo mssql-python Il supporto asincrono è orientato a soluzioni alternative (pool di thread).
Ricerca per testo completo tsvector / tsquery CONTAINS() / FREETEXT() con indici a testo integrale.
ORM (SQLAlchemy) supporto completo Supportato attraverso il dialetto mssql-python integrato in SQLAlchemy 2.1.0b2+ (versione preliminare).

Elenco di controllo di convalida

Usa questa checklist per verificare la tua migrazione:

  1. Sostituisci tutti i marcatori di parametro %s con i parametri ? o %(name)s.
  2. Assicurati che tutti %(name)s i parametri funzionino ancora (entrambi i driver supportano questo formato).
  3. Riscrivi LIMIT/OFFSET in OFFSET/FETCH NEXT.
  4. Riscrivi RETURNING in OUTPUT INSERTED.
  5. Riscrivi ON CONFLICT in MERGE.
  6. Sostituisci SERIAL / BIGSERIAL con IDENTITY.
  7. BOOLEAN colonne sostituite con bit.
  8. Sostituire le colonne dell'array con tabelle normalizzate o JSON.
  9. Sostituire gli operatori JSONB con JSON_VALUE() / JSON_QUERY().
  10. Aggiorna la stringa di connessione per l'autenticazione SQL Microsoft.
  11. Verifica l'applicazione rispetto ad AdventureWorks o allo schema di destinazione.

Autenticazione e distribuzione

Le applicazioni PostgreSQL autogestite tipicamente si distribuiscono con stringhe di connessione contenenti password, oppure utilizzano .pgpass file e PGPASSWORD variabili di ambiente. Database di Azure per PostgreSQL supporta l'autenticazione Microsoft Entra, quindi se stai già usando l'autenticazione senza password, lo stesso modello di identità viene trasferito su Azure SQL.

Per carichi di lavoro di produzione contro Azure SQL, usa l'identità gestita:

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

Per lo sviluppo locale e la CI, consulta Container e sviluppo locale per Docker, devcontainer e i modelli di configurazione delle pipeline CI.