Dados relacionais, SQL e transações
Uma aplicação pode produzir JSON correto e ainda perder dados ou misturar clientes. A base relacional oferece uma forma de descrever identidades, relações e restrições que continuam verificáveis quando várias operações ocorrem. Nesta unidade, duas lojas usam os mesmos identificadores locais. Esse detalhe pequeno revela por que um campo id isolado não basta e por que uma consulta aparentemente simples pode vazar informação.
O caso também acompanha uma reserva de estoque: reduzir saldo e gravar recibo precisam ocorrer juntos. Vamos estudar modelagem, normalização, joins, transações e isolamento com SQLite real, usando apenas a biblioteca padrão do Python. Não faremos uma comparação de todos os bancos. O objetivo é construir raciocínio suficiente para reconhecer contratos de dados nos laboratórios de memória, RAG e agentes do curso principal.
SQLAo terminar esta aula
- Modele a identidade completa em chaves e referências.
- Normalize fatos de acordo com suas dependências.
- Transação local não é transação distribuída.
Antes de continuar: Laboratório: Algoritmos, estruturas e raciocínio de complexidade
Entidades, chaves e dependências dos fatos
FundamentosO cliente tem um nome; o pedido pertence a um cliente; um item liga pedido a produto e possui quantidade. Colocar todos esses fatos numa única linha repetida parece conveniente, mas cria perguntas difíceis: onde atualizar o nome do cliente, como guardar um cliente sem pedido e qual cópia prevalece quando os nomes divergem? Modelar entidades separadas atribui uma origem ao fato. A chave identifica a linha; uma foreign key estabelece a referência entre fatos, sem copiar toda a entidade referenciada.
A normalização examina dependências para reduzir anomalias de atualização, inserção e remoção. No caso, o nome depende da identidade do cliente, não da combinação pedido/produto. Movê-lo para clientes evita atualizar várias linhas de itens quando o nome muda. Isso não significa separar cada coluna em uma tabela nem proibir toda redundância. Uma cópia deliberada, como nome exibido no momento histórico da compra, tem outro significado e deve ser nomeada como snapshot. A distinção entre fato atual e fato histórico precisa nascer do requisito.
Uma leitura inicial das formas normais ajuda a nomear o raciocínio. A primeira forma evita guardar coleções repetidas num campo como lista textual de SKUs. A segunda examina dependências de apenas parte de uma chave composta; por exemplo, a descrição do produto não depende de qual pedido o contém. A terceira considera dependências transitivas entre fatos não chave: um nome de categoria associado ao código da categoria pode pertencer a outra entidade. Essas descrições introdutórias não substituem analisar todas as chaves candidatas e dependências do esquema; elas orientam quais perguntas fazer quando uma atualização exige corrigir muitas cópias.
JOIN compõe fatos; não cria autoridade
FundamentosEm cada loja existe C1. A chave de clientes é (tenant,id); pedidos mantém tenant e cliente como referência composta. Fazer JOIN somente em c.id=p.cliente associa o pedido às duas pessoas. Mesmo um WHERE p.tenant='A' não corrige isso: ele filtra pedidos, mas a ligação já admite o cliente de B. O predicado correto exige igualdade de tenant e de id. O erro não depende de um atacante habilidoso; surge naturalmente quando duas contas usam o mesmo identificador local.
INNER JOIN devolve somente combinações que satisfazem a condição. Se o relatório precisa mostrar clientes sem pedido, um LEFT JOIN conserva as linhas da esquerda com campos ausentes à direita. Escolher o tipo de join muda a pergunta, e colocar um filtro da tabela direita no WHERE pode eliminar justamente essas linhas preservadas. Antes de executar, construa uma tabela pequena e preveja as linhas resultantes. Esse desenho é mais útil do que usar DISTINCT para esconder duplicação causada por uma relação incorreta.
O resultado não possui ordem garantida sem ORDER BY. LIMIT também não define sozinho quais linhas são as primeiras. Um relatório determinístico precisa especificar ordenação e desempate, especialmente quando será comparado em teste. No laboratório, uma única linha esperada simplifica a evidência. Em cenários maiores, verifique tanto a quantidade de linhas quanto os campos que provam a relação. Uma contagem correta ainda pode esconder que a linha contém o nome de outra pessoa.
Atomicidade protege uma mudança composta
FundamentosUma reserva altera produtos e recibos. Se o update ocorre e o processo falha antes do insert, fica uma mudança sem registro correspondente. A transação agrupa essas instruções: commit torna a operação concluída; rollback desfaz a tentativa incompleta. Isso protege o conjunto de alterações no mesmo banco. Não inclui automaticamente uma chamada a API externa. Se um pagamento remoto ocorre antes do rollback local, o banco não tem como desfazer aquele pagamento apenas por encerrar a transação.
Concorrência exige um modelo de observação
FundamentosAtomicidade trata o conjunto de uma operação; isolamento trata o que outras operações podem observar enquanto ela ocorre. No SQLite em modo WAL, uma leitura pode conservar a visão do início da transação enquanto um escritor confirma novas alterações. A conexão leitora precisa encerrar a transação para iniciar uma visão posterior. Isso explica por que duas consultas em conexões diferentes podem apresentar saldos distintos sem que o banco esteja "errado": elas estão observando momentos diferentes sob um contrato definido.
Exercício aplicado
Duas lojas usam cliente C1 e pedido P1. Uma consulta junta apenas id e mistura pessoas; uma reserva reduz estoque, falha antes de gravar recibo e deixa saldo incorreto. Modele identidade composta, corrija a consulta e torne saldo/recibo uma operação atômica.
- Antes da solução, desenhe entidades e dependências; diga quais identificadores são locais ao tenant.
- Escreva joins que incluem tenant e teste um valor malicioso como dado parametrizado.
- Provoque falha entre update e recibo; confirme rollback dos dois efeitos.
- Abra duas conexões, observe snapshot WAL e escritor concorrente; explique o alcance do isolamento.
Abrir resolução comentada
A identidade é (tenant,id), portanto cada referência e join precisa conservar os dois componentes. Clientes, pedidos, produtos e itens armazenam fatos diferentes; nome não é repetido em cada item. Foreign keys e checks tornam parte do contrato verificável no banco, mas não substituem a autenticação da aplicação.
BEGIN IMMEDIATE delimita a reserva: update condicional, recibo vinculado ao conteúdo e commit. A falha provoca rollback, enquanto replay com a mesma chave e conteúdo devolve o registro sem reduzir saldo novamente. A segunda conexão lê um snapshot até encerrar sua transação. WAL permite esse leitor durante o commit do escritor, mas SQLite continua admitindo um escritor por vez.
import sqlite3, tempfile, os, json
with tempfile.TemporaryDirectory() as pasta:
arquivo=os.path.join(pasta, 'loja.sqlite')
a=sqlite3.connect(arquivo, isolation_level=None, timeout=0.1)
b=sqlite3.connect(arquivo, isolation_level=None, timeout=0.1)
try:
for c in (a,b):
c.execute('PRAGMA foreign_keys=ON')
a.execute('PRAGMA journal_mode=WAL')
a.executescript('''
CREATE TABLE clientes(tenant TEXT NOT NULL,id TEXT NOT NULL,nome TEXT NOT NULL,PRIMARY KEY(tenant,id));
CREATE TABLE pedidos(tenant TEXT NOT NULL,id TEXT NOT NULL,cliente TEXT NOT NULL,PRIMARY KEY(tenant,id),FOREIGN KEY(tenant,cliente) REFERENCES clientes(tenant,id));
CREATE TABLE produtos(tenant TEXT NOT NULL,sku TEXT NOT NULL,saldo INTEGER NOT NULL CHECK(saldo>=0),PRIMARY KEY(tenant,sku));
CREATE TABLE itens(tenant TEXT NOT NULL,pedido TEXT NOT NULL,sku TEXT NOT NULL,quantidade INTEGER NOT NULL CHECK(quantidade>0),PRIMARY KEY(tenant,pedido,sku),FOREIGN KEY(tenant,pedido) REFERENCES pedidos(tenant,id),FOREIGN KEY(tenant,sku) REFERENCES produtos(tenant,sku));
CREATE TABLE recibos(tenant TEXT NOT NULL,chave TEXT NOT NULL,conteudo TEXT NOT NULL,PRIMARY KEY(tenant,chave));
INSERT INTO clientes VALUES('A','C1','Ana'),('B','C1','Bia');
INSERT INTO pedidos VALUES('A','P1','C1'),('B','P1','C1');
INSERT INTO produtos VALUES('A','I2',3),('B','I2',7);
INSERT INTO itens VALUES('A','P1','I2',2),('B','P1','I2',1);
''')
consulta='''SELECT p.tenant,p.id,c.nome,i.sku,i.quantidade FROM pedidos p
JOIN clientes c ON c.tenant=p.tenant AND c.id=p.cliente
JOIN itens i ON i.tenant=p.tenant AND i.pedido=p.id WHERE p.tenant=?'''
assert a.execute(consulta,('A',)).fetchall()==[('A','P1','Ana','I2',2)]
# O parametro e dado, mesmo se parecer uma instrucao SQL.
assert a.execute(consulta,("A' OR 1=1 --",)).fetchall()==[]
def saldo(c):return c.execute("SELECT saldo FROM produtos WHERE tenant='A' AND sku='I2'").fetchone()[0]
def reservar(tenant,sku,q,chave,falhar=False):
if type(q) is not int or q<=0: raise ValueError('quantidade invalida')
conteudo=json.dumps([tenant,sku,q],separators=(',',':'))
a.execute('BEGIN IMMEDIATE')
try:
r=a.execute('SELECT conteudo FROM recibos WHERE tenant=? AND chave=?',(tenant,chave)).fetchone()
if r:
if r[0]!=conteudo:raise ValueError('chave com outro conteudo')
a.execute('COMMIT');return 'repetido'
cur=a.execute('UPDATE produtos SET saldo=saldo-? WHERE tenant=? AND sku=? AND saldo>=?',(q,tenant,sku,q))
if cur.rowcount!=1:raise ValueError('saldo insuficiente ou produto ausente')
if falhar:raise RuntimeError('falha antes do recibo')
a.execute('INSERT INTO recibos VALUES(?,?,?)',(tenant,chave,conteudo))
a.execute('COMMIT');return 'reservado'
except Exception:
a.execute('ROLLBACK');raise
try:reservar('A','I2',1,'FALHA',True)
except RuntimeError:pass
else:raise AssertionError('falha nao aconteceu')
assert saldo(a)==3
assert a.execute("SELECT count(*) FROM recibos WHERE chave='FALHA'").fetchone()[0]==0
# A leitura em WAL permanece no snapshot ate encerrar sua transacao.
b.execute('BEGIN');assert saldo(b)==3
assert reservar('A','I2',2,'OP1')=='reservado'
assert saldo(a)==1 and saldo(b)==3
b.execute('COMMIT');assert saldo(b)==1
assert reservar('A','I2',2,'OP1')=='repetido';assert saldo(a)==1
try:reservar('A','I2',1,'OP1')
except ValueError:pass
else:raise AssertionError('conteudo alterado aceito')
try:reservar('A','I2',2,'OP2')
except ValueError:pass
else:raise AssertionError('saldo insuficiente aceito')
a.execute('BEGIN IMMEDIATE')
try:b.execute('BEGIN IMMEDIATE')
except sqlite3.OperationalError:pass
else:raise AssertionError('dois escritores ativos')
finally:a.execute('ROLLBACK')
try:a.execute("INSERT INTO pedidos VALUES('A','P2','C99')")
except sqlite3.IntegrityError:pass
else:raise AssertionError('referencia inexistente aceita')
assert saldo(a)==1
print('joins, parametros, rollback, snapshot, replay e writer lock verificados')
print('Python sqlite:',sqlite3.sqlite_version)
finally:
b.close();a.close()
Como conferir seu resultado
- Toda identidade e referência mantém tenant.
- Join devolve apenas Ana para A, sem linhas de B.
- Falha antes do recibo conserva saldo e não deixa recibo parcial.
- Replay vincula chave ao conteúdo; conteúdo diferente e saldo insuficiente não alteram dados.
- Snapshot e bloqueio de escritor são observados por conexões reais.
Teste sua compreensão
Responda com suas palavras antes de abrir o comentário. Saber explicar uma decisão é parte do domínio.
1. WHERE p.tenant=A torna seguro um join só por cliente id?
Não; o join ainda pode associar cliente de outro tenant.
A condição de ligação precisa conservar a identidade composta.
2. Rollback local desfaz um pagamento remoto?
Não.
A transação cobre os participantes do banco; efeito remoto precisa de protocolo próprio.
3. Um leitor WAL verá imediatamente todo novo commit?
Não enquanto mantiver a transação de leitura anterior.
Ele conserva o snapshot; outra transação pode observar uma versão posterior.
Seu progresso fica salvo neste navegador. Concluir a leitura não substitui demonstrar o domínio nos exercícios.
Referências e aprofundamento
Documentação oficial e trabalhos originais. As referências registram o escopo e as limitações para você conferir o que sustentam.
- Database Design2nd Edition: Normalization
Adrienne Watt / BCcampus • consulta: 2026-10-06
FundamentosDependências funcionais e anomalias de dados.
Limites: Livro aberto; o exemplo local ilustra dependências sem esgotar formas normais.
- SQLite: SELECT
SQLite • consulta: 2026-10-06
FundamentosJoins, filtros, ordenação e processamento de SELECT.
Limites: Dialeto SQLite; relações corretas dependem das chaves e predicados escolhidos.
- SQLite: Foreign Key Support
SQLite • consulta: 2026-10-06
FundamentosReferências e ativação de foreign_keys por conexão.
Limites: Restrições não substituem autorização do chamador.
- SQLite: Transaction
SQLite • consulta: 2026-10-06
FundamentosBEGIN, COMMIT, ROLLBACK e modos de transação.
Limites: Transação cobre banco local, não efeitos em serviços externos.
- SQLite: Isolation
SQLite • consulta: 2026-10-06
FundamentosSnapshot em WAL e serialização de escritores.
Limites: Contrato depende do modo e configuração; não extrapolar para outros bancos.
- Python: sqlite3
Python Software Foundation • consulta: 2026-10-06
PythonConexoes, placeholders e API sqlite3 padrão.
Limites: Documentação consultada3.14; runtime localPython3.14.4 e versaoSQLite registrada no teste.