Pular para o conteúdo

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#

dataforge
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#

dataforge
// 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#

dataforge
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#

dataforge
DB.create_index(db, "pedidos", ["cliente"])
out DB.explain(db, "SELECT * FROM pedidos WHERE cliente = ?", ["Ana"])
text
{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#

dataforge
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#

dataforge
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 temposim
um escritor por vezsim — o banco inteiro trava na escrita
muitos escritores ao mesmo temponão
acesso pela redenã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#

dataforge
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.

Por onde seguir#