Migre do SQLite para Microsoft SQL com mssql-python

SQLite é o banco de dados padrão para muitos projetos Python, FastAPI e aplicativos Flask. Ele permite que você construa sua aplicação rapidamente adiando a decisão da plataforma de dados para depois. Quando chegar a hora de levar sua aplicação para produção, você precisa de suporte para usuários concorrentes, segurança baseada em funções, alta disponibilidade, recuperação de desastres e outros recursos corporativos. Você precisa migrar para Microsoft SQL usando o driver mssql-python.

Diferenças no diaaleto SQL

Quando você migra do SQLite para o Microsoft SQL, precisa resolver duas coisas: reescrever suas instruções SQL para Transact-SQL (T-SQL) e migrar seus dados.

A tabela a seguir mapeia padrões comuns do SQLite para seus equivalentes em Microsoft SQL:

SQLite SQL Server (T-SQL) Notes
INTEGER PRIMARY KEY AUTOINCREMENT int IDENTITY(1,1) PRIMARY KEY O Microsoft SQL usa IDENTITY para autoincremento.
TEXT nvarchar(255) ou nvarchar(max) Sempre especifique um comprimento. Use nvarchar para Unicode.
REAL float ou decimal(18,2) Use decimal para valores exatos, como dinheiro.
BLOB varbinary(max) Mesmo comportamento, nome diferente.
BOOLEAN (armazenado como INTEGER) bit Nenhum dos dois bancos de dados possui um booleano nativo. Ambos armazenam 0/1.
DATETIME('now') GETDATE() ou SYSDATETIME() SYSDATETIME() Dá maior precisão.
LIMIT 10 OFFSET 20 OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY Exige uma ORDER BY cláusula.
\|\| (concat de string) + ou CONCAT() CONCAT() lida com valores NULL.
IFNULL(a, b) ISNULL(a, b) ou COALESCE(a, b) COALESCE é padrão ANSI.
GROUP_CONCAT(col) STRING_AGG(col, ',') Disponível no SQL Server 2017+.
INSERT OR REPLACE INTO Instrução MERGE SQLite exclui e reinsere; MERGE atualiza no local. Veja o exemplo a seguir.
last_insert_rowid() OUTPUT INSERTED.id Use OUTPUT na INSERT declaração. SCOPE_IDENTITY() também funciona, mas requer um arquivo separado SELECT.
typeof(x) SQL_VARIANT_PROPERTY(x, 'BaseType') Raramente necessário com digitação rigorosa.

CREATE TABLE exemplo

-- SQLite
CREATE TABLE IF NOT EXISTS products (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    price REAL DEFAULT 0.0,
    created_at TEXT DEFAULT (datetime('now')),
    is_active BOOLEAN DEFAULT 1
);

-- 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 datetime2 DEFAULT SYSDATETIME(),
    is_active bit DEFAULT 1
);

Exemplos de consulta

Paginação:

SQLite:

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

MSSQL-Python:

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

A ordem dos parâmetros é invertida. Transact-SQL põe OFFSET antes FETCH NEXTde .

Upsert (inserir ou atualizar):

SQLite:

cursor.execute("""
    INSERT OR REPLACE INTO settings (key, value)
    VALUES (?, ?)
""", (key, value))

MSSQL-Python:

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

A USING cláusula define source.[key] e source.value como aliases de coluna. As WHEN cláusulas fazem referência a esses apelidos. São necessários apenas dois ? marcadores.

Último ID inserido:

SQLite:

cursor.execute("INSERT INTO products (name) VALUES (?)", ("Widget",))
product_id = cursor.lastrowid

MSSQL-Python:

cursor.execute(
    "INSERT INTO #Products (Name) OUTPUT INSERTED.ProductID VALUES (?)",
    ("Widget",)
)
product_id = cursor.fetchval()

Atualizar código de conexão

Substitua sqlite3.connect() por mssql_python.connect().

SQLite:

import sqlite3

def get_connection():
    conn = sqlite3.connect("myapp.db")
    conn.row_factory = sqlite3.Row
    return conn

MSSQL-Python:

import mssql_python

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

O acesso à linha funciona de forma semelhante. O factory do Row SQLite retorna linhas semelhantes a dicionários com a sintaxe row["column"]. O driver mssql-python retorna objetos Row que suportam o mesmo tipo de acesso por chave de string, além de acesso por atributo e por índice:

SQLite (com row_factory):

row["name"]

MSSQL-Python:

row["name"]   # String-key access, like SQLite
row.name      # Attribute access
row[0]        # Index access

Estilo de parâmetro de atualização

Tanto o SQLite quanto o mssql-python usam ? como marcador de parâmetro, então a maioria das consultas funciona sem alterações.

# Works with both sqlite3 and mssql-python
cursor.execute("SELECT * FROM Production.Product WHERE Name = ?", ("Adjustable Race",))

A única diferença: SQLite permite parâmetros nomeados com :name sintaxe. O driver mssql-python usa %(name)s em vez disso.

SQLite:

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

MSSQL-Python:

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

Migrar dados existentes

Considerações importantes ao migrar dados do SQLite para o Microsoft SQL:

  • O Microsoft SQL armazena os valores nvarchar(max) e varbinary(max) como objetos grandes (LOBs), que são mais lentos para ler e gravar do que os dados em linha. Mantenha as colunas de texto em nvarchar(4000) ou inferior sempre que os seus dados permitirem. O SQL Server armazena esses valores diretamente na linha de dados, evitando a sobrecarga do LOB.
  • O mapeamento de tipos segue as regras de afinidade de tipos do SQLite. Revise as tabelas geradas após a migração para apertar o tamanho das colunas (por exemplo, nvarchar(100) em vez de nvarchar(4000)) ou adicionar restrições que o SQLite não aplicou.
  • Para tabelas SQLite que usam TEXT para armazenar datas, talvez seja necessário analisar os valores em objetos Python datetime antes de inserir. O Microsoft SQL espera valores corretos de data-hora, não strings de texto.

Use este script para ler o esquema e os dados do SQLite e criar tabelas correspondentes no Microsoft SQL:

import sqlite3
import mssql_python

# SQLite type affinity -> Microsoft SQL type
# Keywords follow SQLite's type affinity rules
TYPE_MAP = {
    "INT": "bigint",
    "CHAR": "nvarchar(4000)",
    "CLOB": "nvarchar(max)",
    "TEXT": "nvarchar(4000)",
    "BLOB": "varbinary(max)",
    "REAL": "float",
    "FLOA": "float",
    "DOUB": "float",
}


def map_type(sqlite_type: str) -> str:
    """Map a SQLite column type to a Microsoft SQL type."""
    upper = (sqlite_type or "TEXT").upper()
    for prefix, sql_type in TYPE_MAP.items():
        if prefix in upper:
            return sql_type
    return "decimal(18,6)"  # NUMERIC affinity (default)


# Connect to both databases
sqlite_conn = sqlite3.connect("myapp.db")
sql_conn = mssql_python.connect(
    "Server=<server>.database.windows.net;"
    "Database=<database>;"
    "Authentication=ActiveDirectoryDefault;"
    "Encrypt=yes;"
)

# Get list of tables from SQLite
sqlite_cur = sqlite_conn.cursor()
sqlite_cur.execute(
    "SELECT name FROM sqlite_master "
    "WHERE type='table' AND name NOT LIKE 'sqlite_%'"
)
tables = [row[0] for row in sqlite_cur.fetchall()]

sql_cursor = sql_conn.cursor()

for table in tables:
    # Read column info from SQLite
    sqlite_cur.execute(f"PRAGMA table_info([{table}])")
    columns = sqlite_cur.fetchall()
    # columns: (cid, name, type, notnull, default_value, pk)

    # Build CREATE TABLE statement
    col_defs = []
    for col in columns:
        name, col_type, notnull, pk = col[1], col[2], col[3], col[5]
        sql_type = map_type(col_type)
        parts = [f"[{name}] {sql_type}"]
        if notnull:
            parts.append("NOT NULL")
        if pk:
            parts.append("PRIMARY KEY")
        col_defs.append(" ".join(parts))

    create_sql = f"CREATE TABLE [{table}] ({', '.join(col_defs)})"
    sql_cursor.execute(
        f"IF OBJECT_ID('{table}','U') IS NULL " + create_sql
    )
    sql_conn.commit()

    # Read all rows from SQLite
    sqlite_cur.execute(f"SELECT * FROM [{table}]")
    rows = sqlite_cur.fetchall()

    if not rows:
        print(f"  {table}: created (empty)")
        continue

    # Use bulkcopy for fast insert
    result = sql_cursor.bulkcopy(table, rows)
    print(f"  {table}: {result['rows_copied']} rows copied")

sql_conn.commit()
sqlite_conn.close()
sql_conn.close()

Diferenças entre funcionalidades

Após a migração, seu aplicativo ganha acesso a recursos do Microsoft SQL que o SQLite não suporta:

Característica SQLite SQL Server
Gravações simultâneas Escritor único por vez Concorrência total com bloqueio em nível de linha
Authentication Apenas permissões de arquivo autenticação SQL, autenticação do Windows, Microsoft Entra ID
Procedimentos armazenados Sem suporte Programabilidade total em T-SQL
Encryption Não integrado TLS em trânsito, TDE em repouso
Transactions Pontos de salvamento, níveis básicos de isolamento Níveis de isolamento total, transações distribuídas
Suporte a JSON json_extract() OPENJSON(), JSON_VALUE(), FOR JSON
Pesquisa de texto completo Extensão FTS5 Indexação de texto completo embutida
Tamanho máximo do banco de dados ~281 TB (limite prático menor) 524 Petabytes
Agrupamento de conexões N/A (em processo) Integrado ao mssql-python