Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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 CONFLICTPostgreSQL 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:
- Tabella separata (normalizzata). Migliore per dati indicizzati e interrogabili.
- Array JSON memorizzato in nvarchar(max). Buono per i metadati opaci.
-
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:
- Esporta lo schema. Usa
pg_dump --schema-onlyper 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 . - Crea tabelle in Microsoft SQL. Esegui il DDL riscritto sul tuo database di destinazione.
- Esporta dati. Usa
pg_dump --data-only --format=csvo interroga ogni tabella con psycopg2. Per grandi dataset e switch di compatibilità, consulta la documentazione di PostgreSQLpg_dump, in particolare la sezione delle opzioni. - 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 conALTER 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:
- Sostituisci tutti i marcatori di parametro
%scon i parametri?o%(name)s. - Assicurati che tutti
%(name)si parametri funzionino ancora (entrambi i driver supportano questo formato). - Riscrivi
LIMIT/OFFSETinOFFSET/FETCH NEXT. - Riscrivi
RETURNINGinOUTPUT INSERTED. - Riscrivi
ON CONFLICTinMERGE. - Sostituisci
SERIAL/BIGSERIALconIDENTITY. -
BOOLEANcolonne sostituite con bit. - Sostituire le colonne dell'array con tabelle normalizzate o JSON.
- Sostituire gli operatori
JSONBconJSON_VALUE()/JSON_QUERY(). - Aggiorna la stringa di connessione per l'autenticazione SQL Microsoft.
- 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.