MySQL, MariaDB, PostgreSQL e MongoDB

MySQL, MariaDB, PostgreSQL e MongoDB

MySQL, MariaDB, PostgreSQL e MongoDB a partir do Python, com os drivers de cada um e o SQLAlchemy. Com o que muda entre eles sem avisar: o with que fecha sem commit no PyMySQL e faz commit sem fechar no psycopg2, o Decimal que recusa float e a nota em texto que some do filtro do Mongo.
Python

• • 22 min de leitura

No artigo Banco de Dados com SQLite e SQLAlchemy trabalhamos com SQLite — excelente para desenvolvimento local. Em produção, a maioria dos sistemas utiliza bancos mais robustos: MySQL e MariaDB para aplicações web tradicionais, PostgreSQL para sistemas que exigem recursos avançados, e MongoDB quando os dados têm estrutura variável ou documento-orientada. Neste artigo veremos como conectar e operar cada um deles a partir do Python.

MySQL e MariaDB com mysql-connector-python

MySQL e MariaDB são compatíveis em termos de API — o mesmo driver funciona para ambos.

pip install mysql-connector-python

Conectando

import mysql.connector
from mysql.connector import Error

def criar_conexao(host, usuario, senha, banco=None):
    try:
        conn = mysql.connector.connect(
            host=host,
            user=usuario,
            password=senha,
            database=banco,
            charset="utf8mb4",
            collation="utf8mb4_unicode_ci"
        )
        if conn.is_connected():
            print(f"Conectado ao MySQL — versão: {conn.get_server_info()}")
        return conn
    except Error as e:
        print(f"Erro ao conectar: {e}")
        return None


conn = criar_conexao("localhost", "root", "senha123", "escola")

Criando banco e tabelas

def configurar_banco(conn):
    cursor = conn.cursor()


    cursor.execute("CREATE DATABASE IF NOT EXISTS escola CHARACTER SET utf8mb4")
    cursor.execute("USE escola")

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS alunos (
            id        INT AUTO_INCREMENT PRIMARY KEY,
            nome      VARCHAR(100) NOT NULL,
            email     VARCHAR(100) UNIQUE NOT NULL,
            nota      DECIMAL(4,2) DEFAULT 0.00,
            ativo     TINYINT(1)   DEFAULT 1,
            criado_em TIMESTAMP    DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    """)

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS disciplinas (
            id   INT AUTO_INCREMENT PRIMARY KEY,
            nome VARCHAR(80) NOT NULL
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    """)

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS notas (
            aluno_id      INT,
            disciplina_id INT,
            valor         DECIMAL(4,2),
            PRIMARY KEY (aluno_id, disciplina_id),
            FOREIGN KEY (aluno_id)      REFERENCES alunos(id),
            FOREIGN KEY (disciplina_id) REFERENCES disciplinas(id)
        ) ENGINE=InnoDB
    """)

    conn.commit()
    print("Banco configurado.")

Dois cuidados com esse começo. O configurar_banco cria o banco escola, mas o criar_conexao do bloco anterior já se conecta a ele, e numa instalação nova isso falha com Unknown database 'escola': para a primeira configuração, conecte com banco=None. E o criar_conexao engole o erro e devolve None, de modo que a falha real aparece uma linha depois, como AttributeError: 'NoneType' object has no attribute 'cursor', longe da causa. Em código de produção, deixe a exceção de conexão subir.

CRUD

def inserir_aluno(conn, nome, email, nota):
    cursor = conn.cursor()
    sql    = "INSERT INTO alunos (nome, email, nota) VALUES (%s, %s, %s)"
    cursor.execute(sql, (nome, email, nota))
    conn.commit()
    return cursor.lastrowid   # ID gerado


def buscar_alunos(conn, nota_minima=0.0):
    cursor = conn.cursor(dictionary=True)   # retorna dicts
    sql    = """
        SELECT id, nome, email, nota
        FROM   alunos
        WHERE  nota >= %s AND ativo = 1
        ORDER  BY nota DESC
    """
    cursor.execute(sql, (nota_minima,))
    return cursor.fetchall()


def atualizar_nota(conn, aluno_id, nova_nota):
    cursor = conn.cursor()
    cursor.execute(
        "UPDATE alunos SET nota = %s WHERE id = %s",
        (nova_nota, aluno_id)
    )
    conn.commit()
    return cursor.rowcount   # linhas afetadas


# Uso
conn = criar_conexao("localhost", "root", "senha123", "escola")

if conn:
    id1 = inserir_aluno(conn, "Ana Silva",   "ana@email.com",   9.5)
    id2 = inserir_aluno(conn, "Bruno Costa", "bruno@email.com", 7.0)
    id3 = inserir_aluno(conn, "Carla Souza", "carla@email.com", 5.5)

    print("\nAlunos com nota >= 7.0:")
    for aluno in buscar_alunos(conn, 7.0):
        print(f"  [{aluno['id']}] {aluno['nome']:15} — {aluno['nota']}")

    conn.close()

Usando PyMySQL como alternativa

pip install pymysql
import pymysql

conn = pymysql.connect(
    host="localhost",
    user="root",
    password="senha123",
    database="escola",
    charset="utf8mb4",
    cursorclass=pymysql.cursors.DictCursor
)

with conn:
    with conn.cursor() as cursor:
        cursor.execute("SELECT * FROM alunos WHERE nota >= %s", (7.0,))
        for row in cursor.fetchall():
            print(row)

Os valores de coluna DECIMAL chegam ao Python como decimal.Decimal, e não como float — é o que preserva a precisão, e é também por isso que a nota aparece como 9.50. A consequência prática é que aluno["nota"] * 1.1 levanta TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float'; a conta se faz com Decimal("1.1"), ou com inteiros. O PostgreSQL faz o mesmo com NUMERIC.

Outro detalhe do PyMySQL que engana quem vem de outro driver: o with conn: dele fecha a conexão, mas não faz commit. Como o autocommit vem desligado, um INSERT feito dentro do bloco sem conn.commit() explícito é descartado ao fechar, sem erro. O mysql-connector-python se comporta do mesmo jeito. E, com o MySQL 8, cujo método de autenticação padrão é caching_sha2_password, o PyMySQL pode precisar do extra de criptografia: pip install "PyMySQL[rsa]".

PostgreSQL com psycopg2

PostgreSQL é o banco relacional de código aberto mais avançado — suporta JSON nativo, arrays, full-text search, transações robustas e muito mais.

pip install psycopg2-binary

Conectando e criando tabelas

import psycopg2
from psycopg2.extras import RealDictCursor, execute_values

def criar_conexao_pg():
    return psycopg2.connect(
        host="localhost",
        port=5432,
        dbname="escola",
        user="postgres",
        password="senha123"
    )


def configurar_banco_pg():
    with criar_conexao_pg() as conn:
        with conn.cursor() as cur:
            cur.execute("""
                CREATE TABLE IF NOT EXISTS alunos (
                    id        SERIAL PRIMARY KEY,
                    nome      VARCHAR(100) NOT NULL,
                    email     VARCHAR(100) UNIQUE NOT NULL,
                    nota      NUMERIC(4,2) DEFAULT 0.0,
                    ativo     BOOLEAN      DEFAULT TRUE,
                    tags      TEXT[],
                    metadata  JSONB,
                    criado_em TIMESTAMPTZ  DEFAULT NOW()
                )
            """)
            conn.commit()
            print("Banco PostgreSQL configurado.")

Recursos exclusivos do PostgreSQL

import json

def inserir_com_metadados(nome, email, nota, tags, metadata):
    with criar_conexao_pg() as conn:
        with conn.cursor(cursor_factory=RealDictCursor) as cur:
            cur.execute("""
                INSERT INTO alunos (nome, email, nota, tags, metadata)
                VALUES (%s, %s, %s, %s, %s)
                RETURNING id, nome, criado_em
            """, (
                nome, email, nota,
                tags,                         # array nativo
                json.dumps(metadata)          # JSONB
            ))
            conn.commit()
            return dict(cur.fetchone())


def buscar_por_tag(tag: str):
    """Busca usando operador de array do PostgreSQL."""
    with criar_conexao_pg() as conn:
        with conn.cursor(cursor_factory=RealDictCursor) as cur:
            cur.execute("""
                SELECT id, nome, nota, tags
                FROM   alunos
                WHERE  %s = ANY(tags)
                ORDER  BY nota DESC
            """, (tag,))
            return cur.fetchall()


def buscar_por_metadata(chave: str, valor: str):
    """Busca dentro de campo JSONB."""
    with criar_conexao_pg() as conn:
        with conn.cursor(cursor_factory=RealDictCursor) as cur:
            cur.execute("""
                SELECT id, nome, metadata
                FROM   alunos
                WHERE  metadata ->> %s = %s
            """, (chave, valor))
            return cur.fetchall()


def inserir_em_lote(alunos: list):
    """Inserção em lote com execute_values — muito mais rápido."""
    with criar_conexao_pg() as conn:
        with conn.cursor() as cur:
            execute_values(cur, """
                INSERT INTO alunos (nome, email, nota)
                VALUES %s
                ON CONFLICT (email) DO UPDATE SET nota = EXCLUDED.nota
            """, [(a["nome"], a["email"], a["nota"]) for a in alunos])
            conn.commit()


# Uso
resultado = inserir_com_metadados(
    nome="Ana Silva",
    email="ana@email.com",
    nota=9.5,
    tags=["monitor", "destaque", "bolsista"],
    metadata={"cidade": "Recife", "turno": "manhã"}
)
print(f"Inserido: {resultado}")

alunos_lote = [
    {"nome": "Bruno Costa", "email": "bruno@email.com", "nota": 7.0},
    {"nome": "Carla Souza", "email": "carla@email.com", "nota": 8.5},
    {"nome": "Diego Lima",  "email": "diego@email.com", "nota": 5.5},
]
inserir_em_lote(alunos_lote)

print("\nAlunos com tag 'monitor':")
for a in buscar_por_tag("monitor"):
    print(f"  {a['nome']} — {a['nota']}")

Aqui o with tem um terceiro significado. No psycopg2, with conn: delimita uma transação — faz commit na saída normal e rollback na exceção — e não fecha a conexão: medido, depois do bloco, conn.closed continua 0. Nas funções acima quem fecha é o coletor de lixo, quando a função retorna e a última referência some; basta guardar a conexão num atributo ou numa lista para ela ficar aberta até o servidor recusar novas conexões. Para fechar de forma garantida no psycopg2, use contextlib.closing(psycopg2.connect(...)) por fora do with da transação. No psycopg 3, a seguir, o with faz as duas coisas.

psycopg3 — a versão moderna

pip install psycopg[binary]
import psycopg

with psycopg.connect("host=localhost dbname=escola user=postgres password=senha123") as conn:
    with conn.cursor(row_factory=psycopg.rows.dict_row) as cur:
        cur.execute("SELECT * FROM alunos WHERE nota >= %s", (7.0,))
        for row in cur.fetchall():
            print(row)

MongoDB com pymongo

MongoDB é um banco de dados orientado a documentos — armazena dados em formato BSON (similar a JSON), sem esquema fixo. Ideal quando os dados têm estrutura variável ou evoluem rapidamente.

pip install pymongo

Conectando

from pymongo import MongoClient
from pymongo.errors import ConnectionFailure
from datetime import datetime, UTC

def criar_cliente():
    cliente = MongoClient(
        "mongodb://localhost:27017/",
        serverSelectionTimeoutMS=5000
    )
    try:
        cliente.admin.command("ping")
        print("Conectado ao MongoDB.")
    except ConnectionFailure:
        print("Falha na conexão com MongoDB.")
    return cliente


cliente = criar_cliente()
db      = cliente["escola"]
alunos  = db["alunos"]        # collection — cria automaticamente

CRUD no MongoDB

# CREATE — inserindo documentos
def inserir_aluno_mongo(nome, email, nota, tags=None):
    documento = {
        "nome":      nome,
        "email":     email,
        "nota":      nota,
        "ativo":     True,
        "tags":      tags or [],
        "criado_em": datetime.now(UTC),
        "historico": []
    }
    resultado = alunos.insert_one(documento)
    return str(resultado.inserted_id)


def inserir_varios(lista):
    resultado = alunos.insert_many(lista)
    return [str(id) for id in resultado.inserted_ids]


# READ — consultando documentos
def buscar_aprovados(nota_minima=6.0):
    cursor = alunos.find(
        {"nota": {"$gte": nota_minima}, "ativo": True},
        {"_id": 0, "nome": 1, "nota": 1, "tags": 1}  # projeção
    ).sort("nota", -1)
    return list(cursor)


def buscar_por_tag(tag: str):
    return list(alunos.find(
        {"tags": tag},
        {"nome": 1, "nota": 1, "tags": 1}
    ))


# UPDATE — atualizando documentos
def atualizar_nota_mongo(email, nova_nota):
    resultado = alunos.update_one(
        {"email": email},
        {
            "$set": {"nota": nova_nota},
            "$push": {
                "historico": {
                    "nota":  nova_nota,
                    "data":  datetime.now(UTC)
                }
            }
        }
    )
    return resultado.modified_count


def adicionar_tag(email, tag):
    alunos.update_one(
        {"email": email},
        {"$addToSet": {"tags": tag}}   # addToSet evita duplicatas
    )


# DELETE — removendo documentos
def desativar_aluno_mongo(email):
    alunos.update_one(
        {"email": email},
        {"$set": {"ativo": False}}
    )


# Uso
inserir_aluno_mongo("Ana Silva",   "ana@email.com",   9.5, ["monitor", "bolsista"])
inserir_aluno_mongo("Bruno Costa", "bruno@email.com", 7.0)
inserir_aluno_mongo("Carla Souza", "carla@email.com", 8.5, ["destaque"])

atualizar_nota_mongo("bruno@email.com", 7.5)
adicionar_tag("bruno@email.com", "monitor")

print("\nAprovados:")
for a in buscar_aprovados():
    print(f"  {a['nome']:15} — {a['nota']} {a.get('tags', [])}")

Sobre as datas: o BSON guarda o instante em UTC com precisão de milissegundos, e o pymongo, por padrão, devolve-o como datetime naive. Um valor gravado como 14h30 de São Paulo volta como 17:30 sem fuso, e os microssegundos são truncados (.123456 volta .123000). Para receber datas aware, crie o cliente com MongoClient(..., tz_aware=True). E como o MongoDB não impõe esquema, ele também não impõe tipo: um documento com "nota": "9.5", em texto, é aceito, e depois simplesmente não aparece num filtro {"nota": {"$gte": 6}}, porque o $gte numérico não compara com strings.

Aggregation Pipeline

O recurso mais poderoso do MongoDB para análises:

def estatisticas_mongo():
    pipeline = [
        {"$match": {"ativo": True}},
        {"$group": {
            "_id":       None,
            "total":     {"$sum": 1},
            "media":     {"$avg": "$nota"},
            "maior":     {"$max": "$nota"},
            "menor":     {"$min": "$nota"},
            "aprovados": {
                "$sum": {
                    "$cond": [{"$gte": ["$nota", 6.0]}, 1, 0]
                }
            }
        }}
    ]
    resultado = list(alunos.aggregate(pipeline))
    return resultado[0] if resultado else {}


def distribuicao_por_conceito():
    pipeline = [
        {"$match": {"ativo": True}},
        {"$project": {
            "nome": 1,
            "nota": 1,
            "conceito": {
                "$switch": {
                    "branches": [
                        {"case": {"$gte": ["$nota", 9]}, "then": "Excelente"},
                        {"case": {"$gte": ["$nota", 7]}, "then": "Bom"},
                        {"case": {"$gte": ["$nota", 6]}, "then": "Regular"},
                    ],
                    "default": "Insuficiente"
                }
            }
        }},
        {"$group": {
            "_id":       "$conceito",
            "quantidade": {"$sum": 1},
            "alunos":    {"$push": "$nome"}
        }},
        {"$sort": {"quantidade": -1}}
    ]
    return list(alunos.aggregate(pipeline))


stats = estatisticas_mongo()
print(f"\nEstatísticas MongoDB:")
print(f"  Total:     {stats.get('total')}")
print(f"  Média:     {stats.get('media', 0):.2f}")
print(f"  Aprovados: {stats.get('aprovados')}")

print("\nDistribuição por conceito:")
for grupo in distribuicao_por_conceito():
    print(f"  {grupo['_id']:15}: {grupo['quantidade']} aluno(s)")

Índices no MongoDB

from pymongo import ASCENDING, DESCENDING, TEXT

# Índice simples
alunos.create_index("email", unique=True)

# Índice composto
alunos.create_index([("nota", DESCENDING), ("ativo", ASCENDING)])

# Índice de texto para busca full-text
alunos.create_index([("nome", TEXT)])

# Buscando com índice de texto
resultados = list(alunos.find({"$text": {"$search": "Silva"}}))

# Listando índices existentes
for indice in alunos.list_indexes():
    print(indice["name"])

O índice único em email deveria vir antes dos dados, não depois. Sem ele, execute o bloco de CRUD duas vezes e a coleção aceita os mesmos três alunos de novo, sem erro. Criado depois, sobre dados já duplicados, o create_index(..., unique=True) falha com DuplicateKeyError. Com o índice no lugar, a segunda execução para no primeiro insert_one, com E11000 duplicate key error — que é o comportamento desejado.

Comparativo: Quando Usar Cada Banco

Critério MySQL/MariaDB PostgreSQL MongoDB
Modelo Relacional Relacional Documento
Esquema Fixo Fixo Flexível
JSON nativo Limitado Excelente (JSONB) Nativo
Transações Sim Sim (ACID completo) Sim (4.0+, exige replica set)
Escalabilidade horizontal Moderada Moderada Excelente
Full-text search Básico Bom Bom
Casos de uso Apps web, CMS Sistemas financeiros, analytics Catálogos, logs, IoT
Hospedagem gerenciada AWS RDS, PlanetScale AWS RDS, Supabase MongoDB Atlas

Usando com SQLAlchemy

SQLAlchemy funciona com MySQL e PostgreSQL — basta trocar a connection string:

from sqlalchemy import create_engine

# MySQL
engine_mysql = create_engine(
    "mysql+pymysql://usuario:senha@localhost:3306/escola",
    pool_size=5,
    max_overflow=10
)

# MariaDB
engine_maria = create_engine(
    "mariadb+pymysql://usuario:senha@localhost:3306/escola"
)

# PostgreSQL
engine_pg = create_engine(
    "postgresql+psycopg2://usuario:senha@localhost:5432/escola",
    pool_size=5,
    max_overflow=10,
    echo=False
)

Os modelos ORM do artigo Banco de Dados com SQLite e SQLAlchemy funcionam sem alteração — apenas a engine muda.

Variáveis de Ambiente para Credenciais

Nunca escreva senhas diretamente no código:

pip install python-dotenv
# arquivo .env
DB_HOST=localhost
DB_PORT=5432
DB_NAME=escola
DB_USER=postgres
DB_PASSWORD=senha_super_secreta
MONGO_URI=mongodb://localhost:27017/
from dotenv import load_dotenv
import os

load_dotenv()

DB_URL = (
    f"postgresql+psycopg2://{os.getenv('DB_USER')}:"
    f"{os.getenv('DB_PASSWORD')}@"
    f"{os.getenv('DB_HOST')}:"
    f"{os.getenv('DB_PORT')}/"
    f"{os.getenv('DB_NAME')}"
)

MONGO_URI = os.getenv("MONGO_URI")

Montar a URL com f-string tem um defeito que só aparece com a senha de produção: caracteres como @, :, / e # têm significado dentro de uma URL. Com a senha p@ss:w/rd#1, o SQLAlchemy lê o w como porta e levanta ValueError: invalid literal for int() with base 10: 'w'. O URL.create monta a URL a partir das partes e escapa cada uma:

from sqlalchemy import URL, create_engine

url = URL.create(
    "postgresql+psycopg2",
    username=os.getenv("DB_USER"),
    password=os.getenv("DB_PASSWORD"),   # escapada automaticamente
    host=os.getenv("DB_HOST"),
    port=int(os.getenv("DB_PORT", "5432")),
    database=os.getenv("DB_NAME"),
)
engine = create_engine(url)

Adicione .env ao .gitignore — nunca suba credenciais para o repositório.

A partir do Python, os três bancos relacionais se usam quase do mesmo jeito: conexão, cursor, consulta com parâmetros e commit. As diferenças que custam caro estão nos detalhes que parecem iguais. O marcador de parâmetro é %s no MySQL e no PostgreSQL, e não ? como no SQLite. DECIMAL e NUMERIC chegam como Decimal, que não se mistura com float. E o with conn: muda de sentido conforme o driver: no psycopg2 é transação sem fechamento, no PyMySQL é fechamento sem commit, e só no psycopg 3 é as duas coisas. O PostgreSQL acrescenta o que os outros não têm na mesma medida — arrays, JSONB indexável, RETURNING e ON CONFLICT —, e o SQLAlchemy permite que o mesmo modelo rode em qualquer um deles trocando a URL.

O MongoDB troca a tabela pelo documento, e com isso troca também quem garante a consistência. O banco aceita qualquer formato, então a regra passa a morar no código e no que se declara explicitamente: o índice único antes dos dados, um validador $jsonSchema quando o tipo importa, tz_aware=True para as datas não voltarem sem fuso, e um replica set quando for preciso transação. O Aggregation Pipeline faz no servidor o que em SQL seria GROUP BY. E, em qualquer dos quatro, a credencial vem do ambiente, nunca do código, e a URL de conexão se monta com URL.create, que escapa a senha.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Uma equipe migra um serviço de PostgreSQL para MySQL. O código usava psycopg2 no padrão with conn: with conn.cursor() as cur: cur.execute("INSERT ..."), sem nenhum commit() explícito, e funcionava. A troca foi para PyMySQL, mudando só a função de conexão. Os testes passam, não há exceção nenhuma, e em produção nenhum cadastro novo aparece no banco. Explique e diga como escrever o código para que ele se comporte igual nos dois drivers.

Ver resposta

✓ Resposta: O with conn: tem significados diferentes nos dois drivers, e a sintaxe idêntica esconde isso. No psycopg2, o bloco é uma transação: na saída normal ele faz commit, na exceção faz rollback, e a conexão continua aberta — medido, conn.closed vale 0 depois do bloco. O código original nunca chamou commit() porque o with fazia isso por ele. No PyMySQL, o __exit__ da conexão tem uma linha só, self.close(): ele fecha a conexão sem fazer commit. Como o autocommit vem desligado, o INSERT fica numa transação aberta, e fechar a conexão faz o servidor descartá-la. O mysql-connector-python faz o mesmo. Não há erro porque nada deu errado do ponto de vista do driver. Os testes provavelmente leram os dados na mesma conexão, antes de ela fechar, onde a transação ainda enxerga o próprio INSERT. O código portável não depende do with para a transação: faz conn.commit() explícito ao fim da unidade de trabalho e conn.rollback() no except, e fecha a conexão com contextlib.closing(...), que chama close() em qualquer driver. Para completar o quadro: no sqlite3 o with é transação sem fechamento, como no psycopg2, e no psycopg 3 é transação e fechamento. Ou se usa o SQLAlchemy, cujo with engine.begin() as conn: tem o mesmo significado para qualquer banco.

Exercício 2

Uma rotina sincroniza cadastros de uma planilha para o PostgreSQL com execute_values e ON CONFLICT (email) DO UPDATE SET nota = EXCLUDED.nota, em lotes de mil. Num dia, o lote inteiro falha com CardinalityViolation: ON CONFLICT DO UPDATE command cannot affect row a second time, e nenhum dos mil registros é gravado. Os dados parecem normais. O que pode haver na planilha, e como tornar a rotina robusta?

Ver resposta

✓ Resposta: A planilha tem o mesmo e-mail duas vezes dentro do mesmo lote — por exemplo, um aluno que corrigiu a nota e ganhou uma segunda linha. O ON CONFLICT resolve conflito entre a linha nova e o que já está na tabela, mas um único comando INSERT não pode atualizar a mesma linha duas vezes, porque o resultado dependeria da ordem, e o PostgreSQL se recusa a escolher. Medido com dois registros de eva@x, notas 6.0 e 8.0, no mesmo execute_values: o comando falha, e a contagem de eva@x na tabela fica em 0. Como o comando é atômico, o erro derruba o lote inteiro, inclusive as 998 linhas corretas. A correção é deduplicar antes de enviar, decidindo qual linha vale: com um dicionário indexado pelo e-mail, {r["email"].strip().lower(): r for r in lote}, a última ocorrência vence. A normalização de caixa e espaços faz parte da mesma decisão: sem ela, Ana@email.com e ana@email.com passam pela deduplicação e entram como dois alunos. Se a regra de negócio for outra — manter a maior nota, ou a mais recente por data —, ela deve ficar explícita nesse passo. Vale também registrar as duplicatas encontradas, porque uma segunda linha na planilha costuma indicar um problema na origem que alguém deveria saber.

Exercício 3

Uma API Flask com SQLAlchemy e PostgreSQL funciona o dia inteiro. Toda manhã, a primeira requisição depois da madrugada falha com OperationalError: server closed the connection unexpectedly, e as seguintes funcionam. A equipe de infraestrutura informa que um firewall derruba conexões ociosas depois de uma hora. Explique por que só a primeira falha e como resolver no código.

Ver resposta

✓ Resposta: O create_engine mantém um pool de conexões abertas para não pagar o custo de conectar a cada requisição. Durante a madrugada, nenhuma é usada, e o firewall — ou o próprio servidor, como faz o wait_timeout do MySQL, que por padrão é de oito horas — encerra essas conexões do lado de lá. O pool não sabe disso: para ele, a conexão continua disponível. A primeira requisição da manhã pega uma delas, tenta usá-la, e descobre só então que está morta. O SQLAlchemy descarta a conexão ruim ao ver o erro, mas a requisição já falhou; as seguintes recebem conexões novas e funcionam. Medido com o PostgreSQL, encerrando a conexão do pool com pg_terminate_backend para simular o firewall: sem proteção, o uso seguinte levanta OperationalError: server closed the connection unexpectedly; com create_engine(url, pool_pre_ping=True), a mesma sequência devolve o resultado normalmente. O pool_pre_ping faz um teste barato na conexão cada vez que ela sai do pool e troca a que não responde, ao custo de uma ida ao servidor por checkout. A alternativa complementar é pool_recycle=1800, que descarta conexões com mais de meia hora antes que o firewall o faça — útil quando se conhece o limite, como aqui. O pool_pre_ping cobre também o que não se conhece, como o reinício do banco.

Exercício 4

A coluna metadata, em JSONB, passou a guardar também o ano de ingresso, como número: {"cidade": "Recife", "ano": 2024}. Um desenvolvedor reaproveita buscar_por_metadata("ano", 2024) deste artigo e recebe UndefinedFunction: operator does not exist: text = integer. Com "2024", entre aspas, funciona. Explique o erro, avalie o contorno e proponha uma consulta melhor.

Ver resposta

✓ Resposta: O operador ->> extrai o valor do JSON sempre como texto, qualquer que seja o tipo guardado. metadata ->> 'ano' devolve a string '2024', e o driver, recebendo o inteiro 2024 do Python, envia um inteiro. O PostgreSQL não converte tipos implicitamente numa comparação entre text e integer, e acusa a falta de operador.Passar "2024" funciona, mas é um contorno frágil: compara textos, então "2024" e "2024.0" são diferentes, e uma busca por faixa, como >= "2020", vira comparação alfabética. Há duas consultas melhores. Para igualdade, o operador de contenção compara JSON com JSON, preservando o tipo: WHERE metadata @> %s com json.dumps({"ano": 2024}) encontra o registro, medido, e é o operador que um índice GIN sobre a coluna inteira atende; o ->> precisaria de um índice de expressão próprio para cada chave. Para faixas numéricas, a conversão fica explícita: WHERE (metadata ->> 'ano')::int >= %s, e aí o cuidado é com documentos em que ano não seja numérico, que farão a consulta inteira falhar. A lição de fundo é a mesma dos outros bancos sem esquema: JSON dentro do relacional é flexível, mas o tipo de cada campo continua sendo uma decisão que alguém precisa tomar e respeitar.

Exercício 5

Um relatório de aprovados feito sobre o MongoDB, com find({"nota": {"$gte": 6}}), mostra 1.240 alunos, e a secretaria sabe que são mais. Descobre-se que parte dos documentos veio de um importador antigo que gravava a nota como texto, "9.5". Não houve erro em momento nenhum. Explique por que esses alunos somem, como encontrá-los e como impedir que aconteça de novo.

Ver resposta

✓ Resposta: O MongoDB compara valores de tipos diferentes por uma ordem fixa de tipos, e não por conversão. Um $gte com número só seleciona números: a string "9.5" não é maior nem menor que 6, simplesmente não entra. Medido com três documentos, "9.5", 7 e 8.5, o filtro devolve só os dois numéricos. A ordenação segue a mesma ordem de tipos, e por isso um sort decrescente põe as strings antes de todos os números, o que dá a impressão de ranking mesmo com tipos misturados. Nenhum erro aparece porque, sem esquema, o banco não tem como saber que "9.5" está errado. Para encontrá-los, o operador $type: count_documents({"nota": {"$type": "string"}}) conta os documentos afetados, e uma atualização com pipeline, usando $toDouble, converte todos de uma vez — depois de conferir que nenhum tem texto não numérico, que faria a conversão falhar. Para impedir que volte, o MongoDB aceita um validador na coleção: com $jsonSchema exigindo "bsonType": ["double", "int", "decimal"] e limites de 0 a 10 para nota, a inserção de "9.5" passa a falhar com WriteError: Document failed validation, medido. "Sem esquema fixo" quer dizer que o banco não impõe esquema por conta própria, e não que a aplicação possa viver sem um: a regra que o relacional garante na definição da tabela, aqui, precisa ser declarada.

Comentários

Mais em Python

Estruturas de Controle: if, elif e else
Estruturas de Controle: if, elif e else

Decidir é o que separa um script de um programa. O if, o elif e o else, a…

Autenticação e Segurança em APIs Python
Autenticação e Segurança em APIs Python

Senhas com bcrypt, API Keys, JWT e OAuth2 no FastAPI, e as brechas que o…

Strings em Profundidade: métodos, formatação e expressões regulares
Strings em Profundidade: métodos, formatação e expressões regulares

Texto é o dado que mais chega torto. Além do repertório de métodos, formatação…