Banco de dados com SQLite e sqlite3

1. Introdução ao SQLite e ao módulo sqlite3

SQLite é um banco de dados relacional embutido, leve e sem servidor, que armazena todo o banco em um único arquivo no disco. Diferente de sistemas como PostgreSQL ou MySQL, não há processo separado para gerenciar conexões — a biblioteca lê e escreve diretamente no arquivo. Isso o torna ideal para aplicações desktop, protótipos, dispositivos embarcados e cenários onde a simplicidade e a portabilidade são mais importantes que a concorrência massiva.

O módulo sqlite3 faz parte da biblioteca padrão do Python desde a versão 2.5, permitindo interagir com bancos SQLite sem instalar dependências externas. Ele segue a especificação DB-API 2.0 (PEP 249), oferecendo uma interface consistente para executar comandos SQL, gerenciar transações e manipular resultados.

Use SQLite quando:
- O volume de dados for moderado (até centenas de milhares de registros)
- A aplicação não exigir concorrência intensa de escrita
- Você precisar de um banco portátil (um único arquivo)
- O deploy simplificado for prioridade

Evite SQLite quando:
- Múltiplos usuários precisarem escrever simultaneamente
- A aplicação exigir recursos avançados como replicação ou stored procedures complexas
- O volume de dados ultrapassar dezenas de gigabytes

2. Configuração e primeiros passos

Para começar, importe o módulo e crie uma conexão com sqlite3.connect(). Se o arquivo do banco não existir, ele será criado automaticamente.

import sqlite3

# Cria (ou abre) o banco de dados
conexao = sqlite3.connect('exemplo.db')

# Cria um cursor para executar comandos SQL
cursor = conexao.cursor()

# Executa um comando simples
cursor.execute("SELECT sqlite_version();")
print(cursor.fetchone())  # Exemplo: ('3.45.1',)

# Fecha a conexão
conexao.close()

A melhor prática é usar o gerenciador de contexto with, que garante o fechamento automático da conexão, mesmo em caso de erro:

with sqlite3.connect('exemplo.db') as conexao:
    cursor = conexao.cursor()
    cursor.execute("SELECT sqlite_version();")
    print(cursor.fetchone())
# A conexão é fechada automaticamente ao sair do bloco

3. Criando tabelas e definindo esquemas

SQLite suporta os tipos INTEGER, TEXT, REAL (ponto flutuante) e BLOB (dados binários). Você pode adicionar restrições como PRIMARY KEY, NOT NULL, UNIQUE e DEFAULT.

with sqlite3.connect('exemplo.db') as conexao:
    cursor = conexao.cursor()

    # Cria tabela usuarios
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS usuarios (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            nome TEXT NOT NULL,
            email TEXT UNIQUE NOT NULL,
            idade INTEGER DEFAULT 18,
            data_cadastro TEXT DEFAULT CURRENT_TIMESTAMP
        )
    """)

    # Cria tabela pedidos
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS pedidos (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            usuario_id INTEGER NOT NULL,
            produto TEXT NOT NULL,
            valor REAL NOT NULL,
            quantidade INTEGER DEFAULT 1,
            FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
        )
    """)

4. Inserindo, consultando e modificando dados

Para inserir dados, use INSERT INTO com placeholders ? para evitar problemas de segurança:

with sqlite3.connect('exemplo.db') as conexao:
    cursor = conexao.cursor()

    # Inserção com placeholders
    cursor.execute(
        "INSERT INTO usuarios (nome, email, idade) VALUES (?, ?, ?)",
        ('Alice', 'alice@email.com', 30)
    )

    # Inserção em lote
    usuarios = [
        ('Bob', 'bob@email.com', 25),
        ('Carol', 'carol@email.com', 22)
    ]
    cursor.executemany(
        "INSERT INTO usuarios (nome, email, idade) VALUES (?, ?, ?)",
        usuarios
    )

    # Consulta com filtro e ordenação
    cursor.execute(
        "SELECT nome, email, idade FROM usuarios WHERE idade > ? ORDER BY nome LIMIT 10",
        (20,)
    )

    # fetchall() retorna todos os resultados como lista de tuplas
    resultados = cursor.fetchall()
    for nome, email, idade in resultados:
        print(f"{nome} ({idade}): {email}")

    # fetchone() retorna apenas o primeiro resultado
    cursor.execute("SELECT COUNT(*) FROM usuarios")
    total = cursor.fetchone()[0]
    print(f"Total de usuários: {total}")

Atualização e exclusão seguem o mesmo padrão:

with sqlite3.connect('exemplo.db') as conexao:
    cursor = conexao.cursor()

    # Atualiza um registro
    cursor.execute(
        "UPDATE usuarios SET idade = ? WHERE nome = ?",
        (31, 'Alice')
    )

    # Exclui registros
    cursor.execute(
        "DELETE FROM usuarios WHERE idade < ?",
        (18,)
    )

5. Segurança e prevenção de SQL Injection

Nunca formate strings SQL diretamente com dados do usuário. Isso abre brecha para ataques de SQL Injection.

Código vulnerável (NÃO USE):

nome_usuario = "'; DROP TABLE usuarios; --"
cursor.execute(f"SELECT * FROM usuarios WHERE nome = '{nome_usuario}'")
# O banco seria deletado!

Código seguro com placeholders:

nome_usuario = "'; DROP TABLE usuarios; --"
cursor.execute("SELECT * FROM usuarios WHERE nome = ?", (nome_usuario,))
# O SQLite trata o valor como string literal, ignorando caracteres especiais

Use executemany() para inserir ou atualizar múltiplos registros de forma segura e eficiente:

dados = [('Produto A', 29.90, 3), ('Produto B', 49.90, 1)]
cursor.executemany(
    "INSERT INTO pedidos (usuario_id, produto, valor, quantidade) VALUES (?, ?, ?, ?)",
    [(1, *item) for item in dados]
)

6. Transações e controle de concorrência

Por padrão, o SQLite opera em modo autocommit: cada execute() é confirmado imediatamente. Para controle explícito, use commit() e rollback():

conexao = sqlite3.connect('exemplo.db')
cursor = conexao.cursor()

try:
    cursor.execute("INSERT INTO usuarios (nome, email) VALUES (?, ?)", ('Daniel', 'daniel@email.com'))
    cursor.execute("INSERT INTO usuarios (nome, email) VALUES (?, ?)", ('Eva', 'eva@email.com'))
    conexao.commit()  # Confirma as duas inserções
except sqlite3.Error:
    conexao.rollback()  # Desfaz qualquer alteração
finally:
    conexao.close()

O gerenciador de contexto da conexão também gerencia transações: ao sair do bloco sem erros, faz commit(); em caso de exceção, faz rollback().

Para melhor performance em cenários de leitura intensa, ative o modo WAL (Write-Ahead Logging), que permite leituras simultâneas durante escritas:

conexao.execute("PRAGMA journal_mode=WAL;")

7. Trabalhando com funções SQL e tipos especiais

SQLite oferece funções agregadas poderosas:

with sqlite3.connect('exemplo.db') as conexao:
    cursor = conexao.cursor()

    # Agrupamento e agregação
    cursor.execute("""
        SELECT u.nome, COUNT(p.id) AS total_pedidos, SUM(p.valor * p.quantidade) AS total_gasto
        FROM usuarios u
        LEFT JOIN pedidos p ON u.id = p.usuario_id
        GROUP BY u.id
        HAVING total_pedidos > 0
        ORDER BY total_gasto DESC
    """)

    for nome, total, valor in cursor.fetchall():
        print(f"{nome}: {total} pedido(s), R$ {valor:.2f}")

Para trabalhar com datas, SQLite armazena como TEXT (ISO 8601), REAL (dias julianos) ou INTEGER (timestamp Unix):

cursor.execute("""
    SELECT nome, data_cadastro 
    FROM usuarios 
    WHERE data_cadastro >= DATE('now', '-7 days')
""")

Você pode criar funções Python personalizadas e registrá-las no SQLite:

def nivel_usuario(idade):
    if idade < 18:
        return "Jovem"
    elif idade < 60:
        return "Adulto"
    return "Sênior"

conexao.create_function("nivel", 1, nivel_usuario)

cursor.execute("SELECT nome, nivel(idade) FROM usuarios")
for nome, nivel in cursor.fetchall():
    print(f"{nome}: {nivel}")

8. Migrações e manutenção do banco

SQLite suporta ALTER TABLE de forma limitada: você pode renomear tabelas ou adicionar colunas, mas não pode remover ou modificar colunas existentes.

Para alterações complexas, a estratégia recomendada é recriar a tabela:

def migrar_adicionar_telefone():
    with sqlite3.connect('exemplo.db') as conexao:
        cursor = conexao.cursor()

        # 1. Cria nova tabela com o esquema desejado
        cursor.execute("""
            CREATE TABLE usuarios_nova (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                nome TEXT NOT NULL,
                email TEXT UNIQUE NOT NULL,
                telefone TEXT,
                idade INTEGER DEFAULT 18,
                data_cadastro TEXT DEFAULT CURRENT_TIMESTAMP
            )
        """)

        # 2. Copia dados da tabela antiga
        cursor.execute("""
            INSERT INTO usuarios_nova (id, nome, email, idade, data_cadastro)
            SELECT id, nome, email, idade, data_cadastro FROM usuarios
        """)

        # 3. Substitui a tabela antiga
        cursor.execute("DROP TABLE usuarios")
        cursor.execute("ALTER TABLE usuarios_nova RENAME TO usuarios")

Para otimizar o banco, use VACUUM para recuperar espaço não utilizado:

conexao.execute("VACUUM")

Monitore o desempenho com EXPLAIN QUERY PLAN:

cursor.execute("EXPLAIN QUERY PLAN SELECT * FROM usuarios WHERE idade > ?", (20,))
for linha in cursor.fetchall():
    print(linha)

O módulo sqlite3 oferece uma solução completa e robusta para armazenamento persistente em Python, combinando simplicidade de uso com segurança contra injeção SQL e controle transacional. Para a maioria dos projetos que não exigem concorrência massiva, SQLite é a escolha ideal — e a biblioteca padrão do Python já fornece tudo que você precisa.

Referências