O Modelo de Camadas do Banco de Dados em Python
Um serviço Python que se comunica com um banco de dados, na verdade, opera através de várias camadas empilhadas, mesmo quando uma única linha session.query(...) faz parecer que é um único passo.
Na base, há um driver de banco de dados que fala o protocolo de comunicação real desse banco de dados; acima dele, fica um pool de conexões que decide quantas dessas conexões em nível de protocolo permanecem abertas por vez; acima disso, fica o SQLAlchemy Core, que traduz expressões Python em SQL; e acima do Core, fica o ORM, que rastreia a identidade do objeto e gera esse SQL para você.
Noções Básicas de Bancos de Dados mostra o código funcional de cada uma dessas camadas; esta página é sobre por que a pilha tem essa forma e onde o síncrono vs. assíncrono e o pooling de conexões realmente se encaixam nela.
Resumo
- O acesso a banco de dados em Python é uma pilha de camadas - um driver DB-API, um pool de conexões, a linguagem de expressão do SQLAlchemy Core e o ORM - cada uma adicionando estrutura sobre a camada abaixo dela, em vez de substituí-la.
- Por que Importa: Confundir qual camada é responsável por qual tarefa (o ORM é lento, o pool está subdimensionado, ou o driver está bloqueando?) é a causa mais comum de problemas de desempenho de banco de dados mal diagnosticados em serviços Python.
- Conceitos Chave: DB-API 2.0, pool de conexões, SQLAlchemy Core, unidade de trabalho do ORM, drivers síncronos vs. assíncronos, consultas N+1.
- Quando Usar: Escolher entre DB-API bruto, Core e ORM para um novo serviço; diagnosticar se uma lentidão é no nível do driver, no nível do pool ou no nível do padrão de consulta; decidir se uma reescrita assíncrona realmente ajudará.
- Limitações / Compromissos: Cada camada adicionada oferece conveniência e portabilidade ao custo de alguma visibilidade sobre o SQL exato e o tempo de execução que ocorrem abaixo dela.
- Tópicos Relacionados: dimensionamento do pool de conexões, o padrão de consulta N+1, migrações Alembic, diferenças entre drivers síncronos e assíncronos.
Fundamentos
PEP 249, comumente chamado de DB-API 2.0, é uma especificação, não uma biblioteca: ela define uma forma comum (connect(), um objeto cursor(), .execute(), .fetchall()) que a maioria dos drivers de banco de dados Python (psycopg, sqlite3, mysqlclient) implementam de forma independente.
Essa forma compartilhada é o motivo pelo qual o código da aplicação que fala com o Postgres através do psycopg se parece estruturalmente com o código que fala com o MySQL através de um driver diferente, mesmo que os dois drivers não compartilhem nenhum código e os bancos de dados falem protocolos de comunicação completamente diferentes por baixo.
Uma analogia útil é um formato de plugue padrão: DB-API não torna todos os bancos de dados idênticos, assim como uma tomada comum não torna todos os aparelhos idênticos, mas significa que qualquer coisa construída para se conectar a essa forma - como o SQLAlchemy - não precisa de uma integração separada para as peculiaridades de cada driver.
O SQLAlchemy Core fica diretamente acima dos drivers DB-API: ele adiciona um engine (que possui um pool de conexões) e uma linguagem de expressão Python (select(), insert(), Table) que compila para o dialeto SQL específico que um determinado banco de dados espera.
O ORM é uma camada adicional construída sobre o Core, não um caminho separado para o banco de dados: as classes declarativas ainda executam através do engine e das conexões do Core, mas o ORM adiciona uma Session que rastreia quais objetos Python correspondem a quais linhas do banco de dados (o mapa de identidade) e agrupa as alterações em um único flush em vez de uma instrução por alteração de atributo.
Mecânicas e Interações
O pooling de conexões existe porque abrir uma conexão de banco de dados é caro em relação à execução de uma consulta - geralmente significa um handshake TCP, às vezes TLS, e uma troca de autenticação antes que a primeira instrução possa ser executada.
O pool padrão do SQLAlchemy mantém um pequeno número de conexões DB-API reais abertas e as entrega a quem chama engine.connect() ou executa uma consulta, devolvendo a conexão ao pool (sem fechá-la) quando o bloco with circundante ou a sessão termina.
requests ──▶ engine.connect() ──▶ [pool: N connections] ──▶ driver ──▶ database
▲ emprestado / ocioso
request N+1 ── espera na fila ──────┘ (todos os N atualmente emprestados)
Isso significa que o tamanho do pool é um teto de concorrência que você define, não um controle de velocidade - um pool de 10 limita as consultas concorrentes em andamento a 10, independentemente de quão rápido o código circundante é executado, e esse teto se multiplica em cada processo de worker: cinco workers Gunicorn, cada um mantendo um pool de 10, podem apresentar 50 conexões simultâneas a um banco de dados que talvez permita apenas 100 no total.
O acesso assíncrono é onde essa pilha ganha uma nova ruga genuína, não apenas uma versão mais rápida do mesmo caminho.
asyncpg é um driver nativo assíncrono com sua própria implementação de protocolo; ele não implementa DB-API 2.0 de forma alguma, porque DB-API é inerentemente uma especificação síncrona.
O create_async_engine do SQLAlchemy faz a ponte para isso usando greenlet nos bastidores: ele permite que o código de unidade de trabalho do ORM, tradicionalmente de aparência síncrona, seja executado contra um driver assíncrono, trocando transparentemente o contexto em pontos de I/O, de modo que await session.execute(...) não seja literalmente o mesmo caminho de código que a Session síncrona, mas uma versão interligada dela.
# create_async_engine fala com asyncpg (nativo assíncrono, não DB-API)
engine = create_async_engine("postgresql+asyncpg://user:pass@host/db")
async with AsyncSession(engine) as session:
result = await session.execute(select(Item))
# greenlet permite que esta chamada estilo ORM aguarde um driver nativo assíncrono por baixoEssa interligação é precisamente por que o SQLAlchemy assíncrono tem seus próprios problemas (carregar preguiçosamente um relacionamento fora de um contexto await explícito gera um erro, em vez de bloquear silenciosamente) - a API ORM de estilo síncrono e o driver assíncrono por baixo são reconciliados pelo SQLAlchemy, não sendo naturalmente compatíveis por si só.
Considerações Avançadas e Aplicações
A escolha entre driver, Core e ORM é realmente sobre onde você quer que a visibilidade viva, e vale a pena separá-la da escolha síncrona vs. assíncrona, porque as duas decisões interagem de forma diferente em cada camada.
O DB-API bruto oferece controle total sobre exatamente qual SQL é executado e quando, ao custo de escrever e manter esse SQL manualmente, incluindo sua parametrização.
O SQLAlchemy Core mantém esse nível de controle SQL enquanto adiciona portabilidade de dialeto e protege contra injeção através de sua linguagem de expressão em vez de formatação de string.
O ORM troca parte dessa visibilidade por velocidade de desenvolvimento e código com formato de objeto tipado, ao custo de ocultar padrões potencialmente caros - mais notavelmente o problema de consulta N+1, onde o acesso a um relacionamento carregado preguiçosamente dentro de um loop transforma uma consulta pretendida em uma ou mais N viagens de ida e volta.
| Camada | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
| Driver DB-API bruto | Controle máximo sobre SQL exato e tempo de execução | Sem portabilidade de dialeto; disciplina de parametrização manual necessária | Scripts estreitos e críticos para desempenho ou uma ferramenta de banco de dados único |
| SQLAlchemy Core | SQL portátil por dialeto, gerenciamento de pool, parametrização segura | Ainda significa escrever a lógica da consulta manualmente, apenas em objetos Python | Operações em lote, metadados de migração, consultas sensíveis ao desempenho |
| SQLAlchemy ORM | Mapa de identidade, modelos tipados, menos código repetitivo para código pesado em CRUD | Pode ocultar padrões N+1 e viagens de ida e volta extras por trás de sintaxe conveniente | Código da aplicação com relacionamentos de objeto ricos e fluxos pesados em CRUD |
| SQLModel | Ergonomia do ORM mais modelos de requisição/resposta baseados em Pydantic em uma única classe | Projeto mais novo, acoplamento mais apertado entre o esquema da API e o esquema do DB | Serviços FastAPI que desejam uma definição de modelo para ambas as camadas |
Nem síncrono nem assíncrono é universalmente "o rápido": assíncrono ajuda quando um serviço é limitado por I/O e precisa manter muitas requisições concorrentes sem bloquear uma thread de worker em cada uma, mas não faz nada para trabalho limitado por CPU, e um engine assíncrono com um pool superdimensionado ainda atinge o mesmo teto de conexão de banco de dados que um engine síncrono atingiria.
Ferramentas de migração (Alembic) ficam ao lado dessa pilha em vez de dentro dela: elas leem os mesmos metadados Core/ORM para gerar diferenças de esquema e aplicá-las como migrações. Isso não muda como a aplicação fala com o banco de dados em tempo de execução.
Concepções Errôneas Comuns
- "Acesso a banco de dados assíncrono é sempre mais rápido." Ajuda na concorrência sob carga limitada por I/O, liberando um worker enquanto espera pelo banco de dados, mas não oferece benefício para trabalho limitado por CPU e não aumenta o teto de conexões do próprio banco de dados.
- "O ORM substitui o Core, e o Core substitui o driver." Cada camada fica em cima da camada abaixo dela - a
Sessiondo ORM ainda executa através do engine do Core e de uma conexão DB-API (ou nativa assíncrona) com pool por baixo. - "Um pool de conexões maior é sempre mais seguro." Além de um ponto, ele apenas transfere o gargalo para o limite de conexões do próprio banco de dados, que cada processo de worker compartilha - pools superdimensionados em vários workers são uma causa comum de incidentes de "banco de dados recusando conexões".
- "
asyncpgé apenas uma versão assíncrona dopsycopg." São drivers implementados independentemente com diferentes manipulações de protocolo;psycopg(v3) na verdade suporta modos síncrono e assíncrono em um único driver, enquantoasyncpgé apenas assíncrono e nunca foi uma implementação DB-API para começar. - "É seguro compartilhar a
Sessiondo SQLAlchemy entre requisições concorrentes." UmaSessionnão é thread-safe por padrão e destina-se a ser escopada a uma unidade de trabalho (tipicamente uma requisição); compartilhá-la entre requisições concorrentes arrisca corromper seu mapa de identidade.
FAQs
O que é DB-API 2.0 e por que é importante?
É o PEP 249, uma especificação que define uma forma comum (connect(), cursor(), .execute()) que a maioria dos drivers de banco de dados Python implementa independentemente. É por isso que ferramentas como SQLAlchemy podem suportar muitos bancos de dados sem uma integração personalizada para os detalhes internos de cada driver.
O SQLAlchemy Core substitui o driver do banco de dados?
Não - o Core fica em cima de um driver DB-API, adicionando um engine, um pool de conexões e uma linguagem de expressão SQL. O driver ainda faz a comunicação real do protocolo de comunicação por baixo.
O ORM é uma maneira separada de acessar o banco de dados a partir do Core?
Não - a Session do ORM executa através do mesmo engine e das conexões com pool que o Core usa. Ele adiciona um mapa de identidade e rastreamento de unidade de trabalho por cima, não um caminho de conexão alternativo.
Por que o acesso assíncrono a banco de dados precisa de algo como greenlet?
Porque a API ORM do SQLAlchemy é escrita em um estilo tradicionalmente síncrono, mas drivers nativos assíncronos como asyncpg não são implementações DB-API de forma alguma. Greenlet permite que esse código ORM de estilo síncrono seja executado contra um driver assíncrono trocando o contexto em pontos de I/O, em vez de os dois serem naturalmente compatíveis.
O que realmente determina quanto trabalho concorrente no banco de dados meu serviço pode fazer?
Geralmente o tamanho do pool de conexões, não a velocidade bruta do driver ou da consulta - um pool de N conexões limita as consultas em andamento a N, independentemente de quão rápido o código ao redor é executado. Esse teto também se multiplica em cada processo de worker que executa seu próprio pool.
Por que cinco workers com pool de tamanho 10 ainda podem sobrecarregar um banco de dados que permite 100 conexões?
Porque 5 workers vezes um pool de 10 cada é igual a 50 conexões possíveis, e isso é antes de considerar outros serviços ou réplicas compartilhando o limite de conexões do mesmo banco de dados. A matemática tem que levar em conta as instâncias totais, não apenas a configuração do pool de um único processo.
O que é o problema de consulta N+1, concretamente?
Buscar uma lista de N linhas e, em seguida, buscar separadamente os dados de cada linha um por um, transformando uma consulta pretendida em N+1 viagens de ida e volta. É mais comum quando um relacionamento ORM carregado preguiçosamente é acessado dentro de um loop sem carregamento antecipado.
Devo sempre optar pelo ORM em vez de Core ou DB-API bruto?
Não necessariamente - o ORM troca alguma visibilidade sobre o SQL exato por menos código repetitivo em código pesado em CRUD e rico em relacionamentos de objeto. Operações em lote, metadados de migração ou consultas críticas para desempenho geralmente se encaixam melhor no Core ou DB-API bruto.
O `psycopg` é o mesmo tipo de driver que o `asyncpg`?
Não - psycopg (versão 3) implementa DB-API 2.0 e suporta modos síncrono e assíncrono em um único driver, enquanto asyncpg é um driver nativo assíncrono que nunca implementou DB-API de forma alguma, já que DB-API é inerentemente síncrono.
Posso compartilhar uma `Session` do SQLAlchemy entre múltiplas requisições concorrentes?
Não - uma Session não é thread-safe por padrão e destina-se a ser escopada a uma unidade de trabalho, tipicamente uma requisição ou uma tarefa. Compartilhá-la entre requisições concorrentes arrisca corromper seu mapa de identidade e estado da transação.
O pooling de conexões significa que não preciso fechar sessões ou conexões explicitamente?
Não - você ainda precisa usar gerenciadores de contexto ou fechar explicitamente uma sessão/conexão para que ela seja devolvida ao pool. Ignorar isso deixa uma conexão emprestada indefinidamente, o que priva o pool de outras requisições, mesmo que o socket subjacente permaneça aberto.
Onde o Alembic se encaixa neste modelo de camadas?
Ele fica ao lado da pilha em vez de dentro dela, lendo os mesmos metadados Core ou ORM para gerar diferenças de esquema e aplicá-las como migrações. Ele não altera como a aplicação fala com o banco de dados em tempo de execução.
Relacionados
- Noções Básicas de Bancos de Dados - exemplos práticos de DB-API, engine e ORM
- SQLAlchemy Core - a linguagem de expressão e o pool de conexões em detalhes
- SQLAlchemy ORM 2.0 - Session, mapa de identidade e modelos declarativos
- Acesso Assíncrono a Banco de Dados - engines assíncronos, sessões assíncronas e interligação com greenlet
- Gerenciamento de Conexões - dimensionamento de pool, retentativas e escopo de transação
- Padrões de Consulta e N+1 - diagnóstico e correção do padrão N+1
Versões da Pilha: Esta página foi escrita para Python 3.14.0 (estável 3.14, manutenção 3.13).