SQLite é o banco de dados mais instalado do mundo — está presente em todos os smartphones Android e iPhone, em todos os navegadores, no sistema operacional macOS, em centenas de milhões de dispositivos embarcados. Mas na web com PHP, é frequentemente ignorado ou mal compreendido.
SQLite não é um banco de dados "de brinquedo". É uma escolha deliberada e poderosa para cenários específicos: aplicações de pequeno a médio porte com tráfego moderado, ferramentas de linha de comando, ambientes de teste, cache local, e qualquer situação onde você não queira (ou não possa) manter um servidor de banco de dados separado.
O que é o SQLite e como ele funciona
Ao contrário do MySQL e PostgreSQL, que são sistemas cliente-servidor, o SQLite é uma biblioteca que se incorpora diretamente à sua aplicação. O banco de dados inteiro é um único arquivo em disco. Não há servidor para iniciar, não há configuração de usuários e senhas, não há processo separado.
Isso traz consequências importantes:
Vantagens: zero configuração, portabilidade total (copie o arquivo e leve o banco junto), perfeito para desenvolvimento e testes, funciona em qualquer hospedagem sem dependências externas.
Limitações: escritas concorrentes são serialized (apenas uma escrita por vez, embora o WAL mode melhore isso), não é adequado para aplicações com alta concorrência de escrita, não suporta alguns tipos de dados avançados.
Criando e conectando ao banco
<?php
declare(strict_types=1);
// O banco é criado automaticamente se não existir
// Use :memory: para um banco em memória (ideal para testes)
$pdo = new PDO('sqlite:banco.db', options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// Ativar WAL mode: melhora performance de leitura concorrente
$pdo->exec('PRAGMA journal_mode = WAL');
// Ativar foreign keys (desativado por padrão no SQLite!)
$pdo->exec('PRAGMA foreign_keys = ON');
// Ajustar tamanho do cache
$pdo->exec('PRAGMA cache_size = -64000'); // 64MB
echo "SQLite " . $pdo->query('SELECT sqlite_version()')->fetchColumn() . "\n";
O detalhe do PRAGMA foreign_keys = ON é crítico e frequentemente esquecido. Por padrão, o SQLite não verifica integridade referencial — chaves estrangeiras são aceitas mas não validadas. Sempre ative esse pragma na abertura da conexão.
Schema e tipos de dados
O SQLite usa um sistema de tipos chamado "type affinity" — diferente de outros bancos, ele é bastante flexível (você pode inserir um texto numa coluna INTEGER). Na prática, use os tipos normalmente e o SQLite fará o melhor:
<?php
$pdo->exec("
CREATE TABLE IF NOT EXISTS notas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
conteudo TEXT,
criada_em TEXT NOT NULL DEFAULT (datetime('now')),
atualizada_em TEXT,
arquivada INTEGER NOT NULL DEFAULT 0 -- 0 = false, 1 = true
)
");
$pdo->exec("
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL UNIQUE
)
");
$pdo->exec("
CREATE TABLE IF NOT EXISTS notas_tags (
nota_id INTEGER NOT NULL REFERENCES notas(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (nota_id, tag_id)
)
");
// Índice para busca por texto
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_notas_titulo ON notas(titulo)");
Full-Text Search — busca de texto com FTS5
Uma das joias do SQLite é o módulo FTS5 (Full-Text Search version 5), que permite busca textual eficiente sem precisar de Elasticsearch ou Typesense para casos simples:
<?php
// Criando tabela virtual de FTS
$pdo->exec("
CREATE VIRTUAL TABLE IF NOT EXISTS notas_fts
USING fts5(titulo, conteudo, content='notas', content_rowid='id')
");
// Trigger para manter o índice atualizado
$pdo->exec("
CREATE TRIGGER IF NOT EXISTS notas_fts_insert
AFTER INSERT ON notas BEGIN
INSERT INTO notas_fts(rowid, titulo, conteudo)
VALUES (new.id, new.titulo, new.conteudo);
END
");
$pdo->exec("
CREATE TRIGGER IF NOT EXISTS notas_fts_delete
AFTER DELETE ON notas BEGIN
INSERT INTO notas_fts(notas_fts, rowid, titulo, conteudo)
VALUES ('delete', old.id, old.titulo, old.conteudo);
END
");
// Inserindo e buscando
$pdo->prepare("INSERT INTO notas (titulo, conteudo) VALUES (?, ?)")
->execute(['PHP e SQLite', 'SQLite é excelente para aplicações PHP de médio porte']);
$pdo->prepare("INSERT INTO notas (titulo, conteudo) VALUES (?, ?)")
->execute(['PostgreSQL avançado', 'JSONB e arrays nativos tornam o Postgres poderoso']);
// Busca FTS com ranking por relevância
$stmt = $pdo->prepare("
SELECT n.id, n.titulo, n.conteudo,
bm25(notas_fts) AS relevancia
FROM notas_fts
JOIN notas n ON n.id = notas_fts.rowid
WHERE notas_fts MATCH :termo
ORDER BY relevancia
LIMIT 10
");
$stmt->execute([':termo' => 'PHP SQLite']);
$resultados = $stmt->fetchAll();
SQLite para testes — o caso de uso mais valioso
O uso mais poderoso do SQLite em projetos profissionais é como banco de dados para testes automatizados. Em vez de criar um banco MySQL real para os testes (lento, com estado compartilhado entre execuções), use SQLite em memória:
<?php
// Em um TestCase base (PHPUnit)
abstract class DatabaseTestCase extends TestCase
{
protected PDO $pdo;
protected function setUp(): void
{
// Banco em memória: criado do zero a cada teste, destruído ao final
$this->pdo = new PDO('sqlite::memory:', options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$this->pdo->exec('PRAGMA foreign_keys = ON');
$this->migrarSchema();
$this->inserirDadosDeFixture();
}
private function migrarSchema(): void
{
// Roda as mesmas migrations do projeto — garantindo paridade
$sql = file_get_contents(__DIR__ . '/../schema.sql');
$this->pdo->exec($sql);
}
private function inserirDadosDeFixture(): void
{
$this->pdo->exec("
INSERT INTO usuarios (nome, email) VALUES
('Alice', 'alice@teste.com'),
('Bob', 'bob@teste.com')
");
}
}
class UsuarioRepositorioTest extends DatabaseTestCase
{
public function test_buscar_por_email_retorna_usuario_existente(): void
{
$repo = new UsuarioRepositorio($this->pdo);
$usuario = $repo->buscarPorEmail('alice@teste.com');
$this->assertNotNull($usuario);
$this->assertSame('Alice', $usuario->nome);
}
public function test_buscar_por_email_retorna_null_para_inexistente(): void
{
$repo = new UsuarioRepositorio($this->pdo);
$usuario = $repo->buscarPorEmail('inexistente@teste.com');
$this->assertNull($usuario);
}
}
Cada teste começa com um banco limpo, sem afetar outros testes. Muito mais rápido que MySQL ou PostgreSQL em memória real.
Resumo
| Característica | SQLite |
|---|---|
| Configuração | Zero — um arquivo |
| Concorrência de escrita | Limitada (WAL mode ajuda) |
| Full-Text Search | FTS5 nativo e eficiente |
| Testes automatizados | Ideal — banco em :memory: |
| Portabilidade | Total — o arquivo é o banco |
| Foreign keys | Desativadas por padrão — sempre ative |
Exercício da semana
-
Crie uma aplicação de notas pessoais com SQLite: CRUD completo (criar, listar, editar, arquivar, deletar) com tags. Use
PRAGMA foreign_keys = ONe WAL mode. -
Implemente FTS5 na tabela de notas. Escreva uma função
buscar(string $termo): arrayque useMATCHe retorne resultados ordenados por relevância (bm25). -
Implemente a mesma aplicação usando MySQL (ou PostgreSQL). Crie uma interface
NotaRepositorioInterfaceque ambas as implementações satisfaçam. Escreva testes que rodem contra SQLite:memory:mas garantam que o código funciona. -
Desafio: implemente backup automático do SQLite: uma função
backup(string $destinoPath): voidque usa a API de backup do SQLite via$pdo->sqliteCreateFunction()ou via linha de comando.dump.
Referências
- Documentação oficial do SQLite: https://www.sqlite.org/docs.html
- SQLite FTS5: https://www.sqlite.org/fts5.html
- PHP Manual — PDO_SQLite: https://www.php.net/manual/pt_BR/ref.pdo-sqlite.php
- Artigo "Consider SQLite" de Ben Johnson: https://litestream.io/alternatives/