SQLite
O banco que vem junto: tabelas, transações, índices, busca textual e migrações — sem instalar nada.
SQLite é um banco dentro do processo: um arquivo, sem servidor, sem porta, sem senha. Ele vem com o Python e portanto com a DataForge — adopt Arcane.Database e já existe banco.
É a escolha certa para muito mais coisa do que costuma parecer: ferramenta de linha de comando, aplicação de uma máquina, cache local, teste automatizado, e sites de leitura pesada com escrita moderada.
Abrir, criar, inserir, consultar#
adopt Arcane.Database as DB
db := DB.memory() // some ao fim do programa
// db := DB.connect("dados.db") // um arquivo
DB.create_table(db, "pedidos", {
"id": "INTEGER PRIMARY KEY",
"cliente": "TEXT",
"total": "REAL"
})
DB.insert(db, "pedidos", {"cliente": "Ana", "total": 99.9})
DB.insert_many(db, "pedidos", [
{"cliente": "Bruno", "total": 45.0},
{"cliente": "Carla", "total": 12.5}
])
out DB.count(db, "pedidos")
out DB.query(db, "SELECT cliente, total FROM pedidos ORDER BY total DESC", [])
DB.close(db)A consulta devolve um cluster de vaults — a mesma forma que o resto da linguagem usa, e a mesma que a análise de dados espera.
Parâmetro, sempre#
// NÃO: um nome com aspas quebra a consulta, e um nome malicioso a reescreve
DB.query(db, $"SELECT * FROM pedidos WHERE cliente = '{nome}'", [])
// SIM: o valor vai por fora do SQL
DB.query(db, "SELECT * FROM pedidos WHERE cliente = ?", [nome])Transação: tudo ou nada#
DB.transacao(db, lambda:
[DB.insert(db, "itens", i) cycle i in itens])Sem transação, cada escrita confirma sozinha — e uma falha no meio deixa metade dentro. Com ela, as duas únicas saídas são "tudo" e "nada".
E a transação também é desempenho. Mil insert soltos são mil confirmações em disco; dentro de uma transação, é uma — a diferença costuma ser de duas ordens de grandeza.
Índice: de varrer para buscar#
DB.create_index(db, "pedidos", ["cliente"])
out DB.explain(db, "SELECT * FROM pedidos WHERE cliente = ?", ["Ana"]){passos: [SEARCH pedidos USING INDEX idx_pedidos_cliente (cliente=?)],
varre_tabela: no, aviso: }Antes do índice, o mesmo explain responde varre_tabela: yes. Medido com 20 mil linhas e 200 consultas: 73,6 ms → 4,4 ms, 16,6×. Ver complexidade em dados.
Busca textual#
DB.create_search(db, "pedidos", ["cliente"])
out DB.search(db, "pedidos", "ana")Duas armadilhas que a implementação já pagou, as duas silenciosas: o * de prefixo vai fora das aspas ("livr"*, e não "livr*"), e o nome da tabela junto de um apelido devolvia vazio — as duas devolviam lista vazia sem erro.
Migrações#
DB.migrate(db, [
{"nome": "001_pedidos",
"up": "CREATE TABLE pedidos (id INTEGER PRIMARY KEY, cliente TEXT)",
"down": "DROP TABLE pedidos"},
{"nome": "002_total",
"up": "ALTER TABLE pedidos ADD COLUMN total REAL DEFAULT 0",
"down": "ALTER TABLE pedidos DROP COLUMN total"}
])
out DB.migrations_applied(db)O down não é opcional por preguiça: sem ele, desfazer exige editar o banco à mão — e a hora de precisar disso é sempre a pior possível.
Concorrência: o que o SQLite faz e o que não faz#
| SQLite | |
|---|---|
| muitos leitores ao mesmo tempo | sim |
| um escritor por vez | sim — o banco inteiro trava na escrita |
| muitos escritores ao mesmo tempo | não |
| acesso pela rede | não — é um arquivo local |
O Arcane.Database serializa o acesso à conexão: sem isso, a primeira consulta de qualquer servidor estoura, porque a conexão do SQLite não atravessa thread. Num Kiln com carga de escrita alta, essa serialização vira o gargalo — e é o momento de trocar por um banco cliente-servidor.
Quando trocar de banco#
- Vários processos escrevendo — não é o caso de uso do SQLite.
- Acesso pela rede — ele não tem; um arquivo em disco compartilhado corrompe.
- Escrita concorrente alta — o travamento no nível do banco passa a doer.
- Dados maiores que o disco de uma máquina — a resposta aí não é banco, é lago.
Fora esses quatro casos, trocar SQLite por um servidor costuma acrescentar operação sem acrescentar capacidade.
Testar com banco#
adopt Arcane.Crucible as C
adopt Arcane.Database as DB
crucible "pedidos":
trial "insere e conta":
db := DB.memory()
DB.create_table(db, "p", {"id": "INTEGER PRIMARY KEY", "v": "REAL"})
DB.insert(db, "p", {"v": 1.0})
expect DB.count(db, "p") is 1
DB.close(db)
C.run()DB.memory() é o que torna teste com banco barato: nada em disco, nada para limpar, e cada trial começa do zero. Para testar sobre um banco real sem sujá-lo, Crucible.banco(db) abre transação e a desfaz no fim.