# Por que eu uso SQLite como banco de dados em 99% dos meus projetos

Por que usar SQLite: tipos simples com STRICT, um arquivo só, node:sqlite nativo no Node.js e WAL para velocidade. E quando ele não é a melhor escolha.

Curso: [Banco de Dados SQLite](https://www.origamid.com/curso/banco-de-dados-sqlite/) · Aula: [Banco de Dados SQLite](https://www.origamid.com/curso/banco-de-dados-sqlite/0101-banco-de-dados-sqlite/), [Tipos de Dados](https://www.origamid.com/curso/banco-de-dados-sqlite/0301-tipos-de-dados/), [Strict](https://www.origamid.com/curso/banco-de-dados-sqlite/0302-strict/), [Pragma 1](https://www.origamid.com/curso/banco-de-dados-sqlite/0505-pragma-1/), [Pragma 2](https://www.origamid.com/curso/banco-de-dados-sqlite/0505-pragma-2/)

URL: https://www.origamid.com/artigos/por-que-usar-sqlite/

O SQLite é o banco de dados de quase todos os sistemas e ferramentas que eu crio (e olha que eu crio muitas para uso interno), o motivo é simples, o SQLite é extremamente rápido e com certeza o banco de dados mais simples de se configurar. Ele tem poucos tipos de dados (torna mais simples a definição das tabelas), o banco inteiro é um arquivo só, o Node.js já conecta nele sem instalar nenhum pacote e ele é muito rápido. O 1% que sobra é o sistema com vários servidores escrevendo no mesmo banco ao mesmo tempo, e é nesse caso que eu não usaria o SQLite, que é o caso da Origamid onde eu uso o PostgreSQL.

O SQLite é usado em produção no mundo inteiro, e se você acordou hoje e mexeu no celular, provavelmente algum aplicativo já leu ou gravou um dado numa tabela SQLite. Por isso, quando alguém diz que SQLite não serve e que tem que ser MySQL ou PostgreSQL, eu sinceramente acho que essa pessoa não conhece o SQLite.

*Figura: A tabela cursos do SQLite tem três colunas, id INTEGER, nome TEXT e preco INTEGER, e duas linhas: 1, HTML, 1000 e 2, CSS, 2050. Os rótulos mostram que a coluna tem nome e tipo, que cada linha é um curso e que o banco inteiro é um arquivo só, o db.sqlite.*

Vamos ver um pouco mais sobre o SQLite nesse artigo. Todos os exemplos usam a mesma tabela, `cursos`, com três colunas: o `id` do curso, o `nome` e o `preco`.

## Poucos tipos de dados

O SQLite guarda os valores em um destes cinco tipos:

- `INTEGER`: número inteiro
- `REAL`: número com casas decimais (ponto flutuante)
- `TEXT`: texto
- `BLOB`: dado binário, como uma imagem
- `NULL`: sem valor

Essa simplicidade é, para mim, o que torna o SQLite especial. Quanto menos decisão você precisa tomar antes de começar, mais fácil fica usar a ferramenta, e quanto mais fácil fica, maiores as chances de você realmente criar alguma coisa com ela.

Quando você vai modelar um banco de dados pela primeira vez usando MySQL ou PostgreSQL, é comum você ficar paralisado pela quantidade de tipos de dados diferentes. Você se pergunta, será que uso int, tinyint ou bigint? No SQLite essa pergunta não existe, você vai usar INTEGER pois é o que têm e isso facilita muito para quem está iniciando.

### O SQLite não impõe o tipo

Existe um detalhe que pega muita gente. Por padrão, o tipo que você escreve no `CREATE TABLE` não é uma regra, mas sim uma afinidade, ou seja, uma preferência. O SQLite tenta converter o valor para o tipo da coluna e, se não consegue, guarda do jeito que veio.

```sql
CREATE TABLE "cursos" (
  "id" INTEGER,
  "nome" TEXT,
  "preco" INTEGER
);

INSERT INTO "cursos" ("id", "nome", "preco")
VALUES ('a', 'HTML', 1000);

SELECT typeof("id")
FROM "cursos";
-- text
```

A coluna `id` foi criada como `INTEGER`, mas aceitou a letra `'a'` e guardou como texto. Se o valor fosse `'1'`, entre aspas, ele converteria para o inteiro `1`, porque o texto parece um número. O problema é que nada impede uma mesma coluna de ter texto numa linha e número na outra, e é isso que o `STRICT` resolve.

## STRICT: o banco recusa o tipo errado

Uma tabela `STRICT` (estrita) passa a exigir o tipo que você definiu. Para criar, basta escrever `STRICT` depois do parêntese que fecha a tabela:

```sql
CREATE TABLE "cursos" (
  "id" INTEGER,
  "nome" TEXT,
  "preco" INTEGER
) STRICT;

INSERT INTO "cursos" ("id", "nome", "preco")
VALUES ('a', 'HTML', 1000);
-- Erro: cannot store TEXT value in INTEGER column cursos.id

INSERT INTO "cursos" ("id", "nome", "preco")
VALUES ('1', 'HTML', 1000);
-- Funciona, '1' vira o inteiro 1
```

O SQLite continua convertendo o que dá para converter sem perder nada, como o `'1'`, mas a letra `'a'` não tem como virar inteiro e o insert dá erro. A restrição vale também na criação da tabela, porque numa tabela estrita só entram `INT`, `INTEGER`, `REAL`, `TEXT`, `BLOB` e `ANY` (que aceita qualquer valor e não converte nada). Um `"nome" VARCHAR(100)` com `STRICT` nem chega a criar a tabela.

O `STRICT` é definido por tabela, e não no banco inteiro, então você pode ter tabelas estritas e não estritas no mesmo arquivo. Eu crio todas as minhas como estritas. Restringir, seja em design ou em programação, deixa o trabalho mais fácil, porque eu sei exatamente quais tipos podem entrar numa coluna e quais tipos vão sair dela quando eu fizer a consulta.

## Um banco de dados em arquivo único

O MySQL e o PostgreSQL são servidores: você instala, configura, cria um usuário com senha e só depois a sua aplicação conecta neles pela rede. O SQLite não tem nada disso, pois o banco é um arquivo, como `db.sqlite`, e você só precisa ter o SQLite na máquina para abrir esse arquivo e começar a criar tabelas. As chances são grandes de ele já estar instalado, porque vem junto em muitos sistemas e dispositivos.

E esse arquivo não tem mágica dentro. O SQLite organiza os dados em páginas de tamanho fixo, e o comando `PRAGMA` (que eu explico melhor mais adiante) mostra quantas são e de que tamanho:

```sql
PRAGMA page_size;
-- 4096

PRAGMA page_count;
-- 20
```

Por ser um arquivo, o backup é copiar o arquivo, e levar o banco para outro servidor é levar o arquivo junto (seguro se o banco não estiver ativo, ou seja, nada sendo escrito no mesmo no momento da cópia):

```bash
# copia o arquivo (só com a aplicação parada)
cp db.sqlite backup/db-2026-10-16.sqlite

# mais seguro: o próprio SQLite faz a cópia, mesmo com o banco em uso
sqlite3 db.sqlite ".backup 'backup/db-2026-10-16.sqlite'"
```

## node:sqlite: a conexão nativa no Node.js

Durante muito tempo, usar SQLite no Node.js exigia instalar um pacote. Hoje o Node já vem com o módulo `node:sqlite`. A conexão é uma linha (se o arquivo não existe, ele é criado):

```js title="db.js"
import { Database } from "node:sqlite";

const db = new Database("./db.sqlite");
```

Para rodar comandos SQL que não devolvem nada, como criar a tabela, existe você pode usar a função `exec`:

```js title="db.js"
db.exec(`
  CREATE TABLE IF NOT EXISTS "cursos" (
    "id" INTEGER,
    "nome" TEXT,
    "preco" INTEGER
  ) STRICT;
`);
```

Quando o comando precisa receber valores ou devolver dados, use o `prepare`. Ele prepara a instrução SQL e você pode reaproveitar ela quantas vezes quiser. Cada **?** na instrução marca o lugar de um valor, e os valores são passados na mesma ordem na hora de executar.

Para executar, você tem três métodos: o `run` é para comandos que alteram o banco, como o **INSERT**, e devolve quantas linhas mudaram e o último rowid inserido. O `get` devolve só a primeira linha encontrada como objeto (ou undefined, se não achar nada). E o `all` devolve uma array com todas as linhas.

```js title="db.js"
const inserir = db.prepare(
  `INSERT INTO "cursos" ("id", "nome", "preco") VALUES (?, ?, ?)`,
);
inserir.run(1, "HTML", 1000); // { changes: 1, lastInsertRowid: 1 }
inserir.run(2, "CSS", 2050);

const curso = db.prepare(`SELECT * FROM "cursos" WHERE "id" = ?`).get(1);
// { id: 1, nome: 'HTML', preco: 1000 }

const cursos = db.prepare(`SELECT * FROM "cursos"`).all();
// [ { id: 1, nome: 'HTML', preco: 1000 }, { id: 2, nome: 'CSS', preco: 2050 } ]

inserir.run("a", "PHP", 1000);
// Erro: cannot store TEXT value in INTEGER column cursos.id
```

O último `run` mostra o `STRICT` funcionando, porque o valor errado vira um erro no JavaScript em vez de entrar quieto no banco. Todos os **métodos são síncronos**, por isso nenhum deles precisa de **await**.

## Por que usar SQLite: ele é rápido

O SQLite não é um servidor separado da sua aplicação. Ele roda dentro do mesmo processo do Node e lê o arquivo direto do disco, sem rede entre o back end e o banco, então cada consulta é uma chamada de função, e não uma mensagem que vai até outro servidor e volta.

A outra parte da velocidade vem da configuração. O `PRAGMA` é o comando que lê e muda as configurações do SQLite: `PRAGMA journal_mode;` mostra o valor atual e `PRAGMA journal_mode = WAL;` muda. O detalhe importante é que, das configurações abaixo, só o `journal_mode` fica gravado no arquivo. Todas as outras se perdem quando a conexão fecha, e por isso eu rodo o bloco inteiro logo depois de abrir o banco, sempre:

```js title="db.js"
db.exec(`
  PRAGMA foreign_keys = 1;
  PRAGMA journal_mode = WAL;
  PRAGMA synchronous = NORMAL;
  PRAGMA busy_timeout = 5000;
  PRAGMA cache_size = 2000;
  PRAGMA temp_store = memory;
  PRAGMA analysis_limit = 1000;
`);
```

O `foreign_keys` faz o banco respeitar as chaves estrangeiras, e sem ele você consegue inserir uma aula com o `id` de um curso que não existe, criando dados inconsistentes sem perceber.

O `journal_mode` define como o SQLite escreve no arquivo. O WAL mode (Write-Ahead Logging) muda a forma como o SQLite grava as alterações. Em vez de escrever direto no arquivo do banco, ele grava primeiro num log separado.

Com o WAL ligado, o SQLite recomenda usar o `synchronous` como `NORMAL`. O `busy_timeout` diz quanto tempo, em milissegundos, o SQLite espera quando o banco está ocupado antes de devolver erro, e 5000 são 5 segundos.

O `cache_size` controla quanto do banco fica em memória, o seu valor é em páginas, e 2000 páginas de 4096 bytes dão cerca de 8 MB. O `temp_store = memory` coloca as tabelas e os índices temporários na memória em vez do disco, o que é seguro porque eles são descartáveis.

## Quando o SQLite não é a melhor escolha

O SQLite não foi feito para receber muitas escritas por segundo vindas de vários lugares ao mesmo tempo. Ele aceita quantos leitores você quiser, mas só uma escrita de cada vez. Um Netflix ou um Instagram, com vários servidores escrevendo no mesmo banco o tempo todo, não usariam SQLite.

O banco também precisa ficar perto do back end. Ele é um arquivo, e o ideal é que esse arquivo esteja no mesmo servidor onde a aplicação roda. Se você precisa de várias máquinas acessando os mesmos dados pela rede, um banco cliente-servidor como o PostgreSQL resolve melhor.

E não dá para restringir acesso. Isso não quer dizer que qualquer pessoa pode abrir o seu banco, quer dizer que o back end que conecta no arquivo tem acesso total a ele. No PostgreSQL ou no MySQL você cria usuários e define quem pode ler ou escrever em cada tabela, e no SQLite essa camada não existe, justamente porque ele é um arquivo simples, sem configuração.

Agora, a grande maioria dos sites da web, eu diria que 95% ou mais, não chega nem perto do tráfego que transformaria isso num problema. Então por que não usar o banco mais simples? É por isso que eu começo todo projeto com SQLite. E se um dia o sistema crescer a ponto de precisar de vários servidores escrevendo juntos, nada do que você aprendeu se perde, porque cerca de 90% do SQL que você escreve no SQLite, como os [comandos de criar tabela, inserir e selecionar](/artigos/comandos-sql-sqlite/), são os mesmos no MySQL e no PostgreSQL.

Agora se tem uma coisa que eu crio constantemente são ferramentas para uso local apenas, como gerador de notas fiscais, gerenciador de anúncios, planejador de tarefas e outros. Para todas essas ferramentas eu sempre uso SQLite.

## Referências

- [Appropriate Uses For SQLite](https://www.sqlite.org/whentouse.html)
- [Pragma statements supported by SQLite](https://www.sqlite.org/pragma.html)
- [SQLite | Node.js v26.12.0 Documentation](https://nodejs.org/api/sqlite.html)

Quer aprender mais? Conheça o curso [Banco de Dados SQLite](https://www.origamid.com/curso/banco-de-dados-sqlite/).
