---
title: "Zig com Banco de Dados: SQLite, PostgreSQL, Redis e Pooling"
url: "https://ziglang.com.br/artigos/zig-banco-dados-integracoes/"
markdown_url: "https://ziglang.com.br/artigos/zig-banco-dados-integracoes.MD"
description: "Guia prático para integrar Zig com bancos de dados: SQLite, PostgreSQL/libpq, Redis, queries parametrizadas, pooling, erros, testes e produção."
date: "2026-02-21"
author: "Zig Brasil"
---

# Zig com Banco de Dados: SQLite, PostgreSQL, Redis e Pooling

Guia prático para integrar Zig com bancos de dados: SQLite, PostgreSQL/libpq, Redis, queries parametrizadas, pooling, erros, testes e produção.


# Zig com Banco de Dados: SQLite, PostgreSQL, Redis e Pooling

Zig ainda não tem um ecossistema de ORMs comparável ao de Java, Python ou Node.js. Isso não é necessariamente um problema. Em projetos Zig, a integração com banco de dados costuma seguir uma linha mais explícita: SQL escrito à mão, tipos pequenos para representar linhas, erros tratados no retorno e bibliotecas C maduras chamadas diretamente por `@cImport`.

A decisão prática é simples:

| Caso de uso | Caminho recomendado em Zig |
|---|---|
| Aplicação local, CLI, desktop, cache persistente | SQLite via `sqlite3` |
| API, SaaS, serviço multiusuário | PostgreSQL via `libpq` ou driver Zig maduro |
| Cache, rate limit, fila leve, sessão | Redis via cliente RESP ou `hiredis` |
| Protótipo rápido | SQLite primeiro, migração para PostgreSQL quando necessário |
| Domínio crítico | SQL explícito, migrations versionadas, testes de integração |

O ganho é previsibilidade. Zig deixa claro quem aloca memória, quem fecha conexão, quando a query falhou e onde a conversão de tipos acontece.

## Modelo mental: banco como borda do sistema

Em Zig, trate banco de dados como uma borda externa, não como magia no centro da aplicação. Uma arquitetura comum fica assim:

```text
HTTP/CLI/worker
    -> validação de entrada
    -> serviço de domínio
    -> repositório Zig pequeno
    -> sqlite3/libpq/redis
```

O repositório expõe funções específicas:

```zig
pub fn buscarUsuarioPorEmail(repo: *UserRepo, email: []const u8) !?Usuario
pub fn criarUsuario(repo: *UserRepo, input: CriarUsuarioInput) !UsuarioId
pub fn marcarLogin(repo: *UserRepo, id: UsuarioId, agora: i64) !void
```

Evite espalhar SQL por handlers HTTP. Isso facilita testes, revisão de segurança e troca de SQLite para PostgreSQL no futuro.

## SQLite: a integração mais natural

SQLite combina muito bem com Zig porque é uma biblioteca C pequena, estável e embutível. Para CLIs, ferramentas internas, aplicativos locais e jobs de ingestão, ele costuma ser suficiente.

```zig
const std = @import("std");

const c = @cImport({
    @cInclude("sqlite3.h");
});

const DbError = error{
    OpenFailed,
    PrepareFailed,
    StepFailed,
    BindFailed,
};

const Database = struct {
    db: *c.sqlite3,

    pub fn open(path: [:0]const u8) !Database {
        var db: ?*c.sqlite3 = null;
        if (c.sqlite3_open(path.ptr, &db) != c.SQLITE_OK) {
            return DbError.OpenFailed;
        }
        return .{ .db = db.? };
    }

    pub fn close(self: *Database) void {
        _ = c.sqlite3_close(self.db);
    }

    pub fn exec(self: *Database, sql: [:0]const u8) !void {
        var err_msg: ?[*:0]u8 = null;
        defer if (err_msg) |msg| c.sqlite3_free(msg);

        if (c.sqlite3_exec(self.db, sql.ptr, null, null, &err_msg) != c.SQLITE_OK) {
            return DbError.StepFailed;
        }
    }
};
```

O ponto importante é o `defer`: toda conexão aberta precisa ser fechada. Todo erro retornado por C precisa virar erro Zig controlado. Não deixe ponteiros C soltos em camadas altas da aplicação.

## Queries parametrizadas em SQLite

Nunca monte SQL concatenando entrada de usuário. Mesmo em ferramenta interna, use `prepare`, `bind` e `step`.

```zig
pub fn inserirUsuario(db: *Database, nome: []const u8, email: []const u8) !void {
    const sql =
        \\INSERT INTO usuarios (nome, email)
        \\VALUES (?1, ?2)
    ;

    var stmt: ?*c.sqlite3_stmt = null;
    if (c.sqlite3_prepare_v2(db.db, sql, -1, &stmt, null) != c.SQLITE_OK) {
        return DbError.PrepareFailed;
    }
    defer _ = c.sqlite3_finalize(stmt);

    if (c.sqlite3_bind_text(stmt, 1, nome.ptr, @intCast(nome.len), c.SQLITE_TRANSIENT) != c.SQLITE_OK) {
        return DbError.BindFailed;
    }
    if (c.sqlite3_bind_text(stmt, 2, email.ptr, @intCast(email.len), c.SQLITE_TRANSIENT) != c.SQLITE_OK) {
        return DbError.BindFailed;
    }

    if (c.sqlite3_step(stmt) != c.SQLITE_DONE) {
        return DbError.StepFailed;
    }
}
```

Esse padrão é repetitivo, mas é revisável. Em Zig, vale criar helpers pequenos para bind de string, inteiro e timestamp, sem esconder completamente a query.

## PostgreSQL com libpq

Para serviços web e dados compartilhados, PostgreSQL é a escolha mais comum. A rota conservadora em Zig é usar `libpq`, a biblioteca C oficial do Postgres.

```zig
const c = @cImport({
    @cInclude("libpq-fe.h");
});

const PgError = error{
    ConnectionFailed,
    QueryFailed,
    UnexpectedRows,
};

const PgConn = struct {
    conn: *c.PGconn,

    pub fn connect(conninfo: [:0]const u8) !PgConn {
        const conn = c.PQconnectdb(conninfo.ptr) orelse return PgError.ConnectionFailed;
        if (c.PQstatus(conn) != c.CONNECTION_OK) {
            c.PQfinish(conn);
            return PgError.ConnectionFailed;
        }
        return .{ .conn = conn };
    }

    pub fn close(self: *PgConn) void {
        c.PQfinish(self.conn);
    }
};
```

Em produção, não deixe a string de conexão hardcoded. Leia de variável de ambiente ou arquivo de configuração injetado pelo deploy.

```zig
const conninfo = std.posix.getenv("DATABASE_URL") orelse return error.MissingDatabaseUrl;
```

## Queries parametrizadas no PostgreSQL

Com `libpq`, use `PQexecParams` para separar SQL e valores.

```zig
pub fn buscarUsuario(conn: *PgConn, email: [:0]const u8) !?Usuario {
    const sql =
        \\SELECT id, nome, email
        \\FROM usuarios
        \\WHERE email = $1
        \\LIMIT 1
    ;

    const values = [_][*c]const u8{email.ptr};
    const lengths = [_]c_int{@intCast(email.len)};
    const formats = [_]c_int{0};

    const res = c.PQexecParams(
        conn.conn,
        sql,
        1,
        null,
        &values,
        &lengths,
        &formats,
        0,
    ) orelse return PgError.QueryFailed;
    defer c.PQclear(res);

    if (c.PQresultStatus(res) != c.PGRES_TUPLES_OK) {
        return PgError.QueryFailed;
    }

    const rows = c.PQntuples(res);
    if (rows == 0) return null;
    if (rows > 1) return PgError.UnexpectedRows;

    // Em código real, copie os valores para memória Zig controlada.
    return Usuario{
        .id = try parseInt(c.PQgetvalue(res, 0, 0)),
        .nome = try copyPgText(c.PQgetvalue(res, 0, 1)),
        .email = try copyPgText(c.PQgetvalue(res, 0, 2)),
    };
}
```

A parte crítica é propriedade de memória. Valores retornados por `PQgetvalue` vivem enquanto `PGresult` existir. Se você chama `PQclear`, precisa ter copiado os dados que serão usados depois.

## Pool de conexões simples

Abrir uma conexão PostgreSQL por request é caro. Em um serviço Zig, um pool pequeno resolve a maior parte dos casos.

```zig
const Pool = struct {
    allocator: std.mem.Allocator,
    conns: std.ArrayList(*PgConn),
    mutex: std.Thread.Mutex = .{},

    pub fn acquire(self: *Pool) !*PgConn {
        self.mutex.lock();
        defer self.mutex.unlock();

        if (self.conns.items.len == 0) return error.PoolExhausted;
        return self.conns.pop();
    }

    pub fn release(self: *Pool, conn: *PgConn) !void {
        self.mutex.lock();
        defer self.mutex.unlock();
        try self.conns.append(conn);
    }
};
```

Esse exemplo é intencionalmente pequeno. Em produção, acrescente:

- limite máximo de conexões;
- health check antes de devolver conexão antiga;
- timeout de aquisição;
- métricas de conexões ocupadas e erros;
- fechamento limpo no shutdown;
- estratégia para transações longas.

Se o deploy roda em Kubernetes ou serverless, cuidado para não multiplicar conexões por réplica até derrubar o Postgres. Combine pool local com PgBouncer quando necessário.

## Transações explícitas

Transação em Zig deve aparecer no fluxo do código. O padrão básico é `BEGIN`, `COMMIT` e `ROLLBACK` com `errdefer`.

```zig
pub fn transferirSaldo(conn: *PgConn, origem: i64, destino: i64, centavos: i64) !void {
    try conn.exec("BEGIN");
    errdefer conn.exec("ROLLBACK") catch {};

    try debitar(conn, origem, centavos);
    try creditar(conn, destino, centavos);

    try conn.exec("COMMIT");
}
```

`errdefer` é uma das melhores ferramentas de Zig para banco de dados. Ele documenta que qualquer falha no meio da operação desfaz a transação.

## Redis: cache e coordenação leve

Redis entra bem em Zig quando o papel é limitado:

- cache de resposta;
- rate limit;
- sessão curta;
- fila simples;
- lock com TTL;
- contador de eventos.

Evite transformar Redis em banco principal de dados críticos. Use PostgreSQL ou SQLite como fonte de verdade e Redis como aceleração.

Para clientes, há dois caminhos: implementar RESP diretamente para comandos simples ou usar `hiredis` via C. Em ambos os casos, modele o retorno como erro Zig, não como strings soltas.

```zig
const CacheError = error{Unavailable, InvalidResponse};

pub fn cacheKeyUsuario(allocator: std.mem.Allocator, id: i64) ![]u8 {
    return std.fmt.allocPrint(allocator, "usuario:{d}", .{id});
}
```

Chaves previsíveis e TTL explícito importam mais do que abstrações grandes.

## Migrations: não deixe para depois

Projetos Zig pequenos costumam começar com SQL manual. Tudo bem. O erro é não versionar schema.

Uma estrutura simples funciona:

```text
migrations/
  001_create_users.sql
  002_add_login_audit.sql
  003_create_api_tokens.sql
```

Na inicialização ou em um comando administrativo, rode as migrations pendentes dentro de uma transação e grave o histórico:

```sql
CREATE TABLE IF NOT EXISTS schema_migrations (
  version TEXT PRIMARY KEY,
  applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

Para SQLite, a mesma ideia vale com `TEXT` e timestamp ISO. O importante é que deploy e banco avancem juntos.

## Testes de integração

Banco de dados precisa de teste real. Mock ajuda pouco quando o risco está em SQL, índice, constraint e transação.

Para SQLite:

- use banco em arquivo temporário para testar persistência;
- rode migrations antes do teste;
- apague o arquivo no fim;
- teste constraints e casos de erro.

Para PostgreSQL:

- use container local ou banco dedicado de teste;
- crie schema isolado por execução;
- rode migrations;
- faça rollback ou drop no final.

```zig
test "cria e busca usuario por email" {
    var db = try Database.open(":memory:");
    defer db.close();

    try db.exec("CREATE TABLE usuarios (id INTEGER PRIMARY KEY, nome TEXT, email TEXT UNIQUE)");
    try inserirUsuario(&db, "Ana", "ana@example.com");

    const usuario = try buscarUsuario(&db, "ana@example.com");
    try std.testing.expect(usuario != null);
}
```

## Observabilidade e erros

Banco lento vira sistema lento. Instrumente desde cedo:

- tempo por query;
- quantidade de queries por request;
- erros por tipo;
- pool esgotado;
- tentativas de reconnect;
- transações abertas por muito tempo.

Se você já usa Zig para [observabilidade](/artigos/zig-observabilidade/), trate queries como spans ou eventos estruturados. Logue nome lógico da query, não dados sensíveis.

```text
query=user.find_by_email duration_ms=12 rows=1 status=ok
query=invoice.create duration_ms=91 status=error error=unique_violation
```

Nunca logue senha, token, documento, cartão ou payload privado.

## Segurança: o checklist mínimo

Antes de colocar Zig com banco em produção, confira:

- queries parametrizadas em toda entrada externa;
- usuário do banco com permissões mínimas;
- `DATABASE_URL` fora do repositório;
- TLS quando o banco está fora da rede privada;
- timeouts de conexão e query;
- migrations revisadas;
- backups testados;
- índices para consultas críticas;
- logs sem dados sensíveis;
- limites de pool compatíveis com o banco.

A vantagem de Zig é que muita coisa fica explícita. A desvantagem é que você precisa construir as proteções que um framework grande traria por padrão.

## Quando usar driver Zig nativo?

Drivers nativos podem ser bons quando o projeto está ativo, cobre o protocolo necessário e tem manutenção compatível com sua exigência de produção. Para sistemas críticos, avalie:

- suporte à versão atual do Zig;
- autenticação e TLS;
- queries parametrizadas;
- transações;
- testes de integração;
- tratamento de reconnect;
- licença e atividade do mantenedor.

Se a resposta for incerta, `sqlite3`, `libpq` e `hiredis` continuam sendo escolhas conservadoras.

## Relação com outros tópicos Zig

Banco de dados cruza vários assuntos do ecossistema:

- [HTTP server em produção](/artigos/zig-http-server-producao/) precisa de pool, timeout e shutdown limpo.
- [Tratamento de erros](/artigos/zig-error-handling-boas-praticas/) define como falhas SQL sobem até a API.
- [Containers Docker](/artigos/zig-docker-containers/) precisam incluir bibliotecas C quando o binário não é totalmente estático.
- [OpenTelemetry em Zig](/artigos/zig-opentelemetry/) ajuda a rastrear query lenta por request.
- [Code review em Zig](/artigos/zig-code-review-checklist/) deve incluir SQL injection, ownership de memória C e rollback.

## FAQ

### Zig tem ORM oficial?

Não. O caminho mais comum em Zig é usar SQL explícito, bibliotecas C maduras como `sqlite3` e `libpq`, ou adaptar clientes Zig menores quando o projeto aceita a maturidade deles.

### SQLite combina com Zig?

Sim. SQLite é uma das integrações mais naturais porque é uma biblioteca C estável, pequena e fácil de embutir junto de binários Zig. Para CLI, desktop, edge e ferramentas internas, costuma ser a primeira opção.

### Como usar PostgreSQL em Zig hoje?

A rota mais conservadora é `libpq` via `@cImport`, com queries parametrizadas, tratamento explícito de erros, timeout e um pool simples no lado da aplicação. Drivers Zig nativos podem ser avaliados caso a maturidade atenda ao projeto.

### Vale usar Redis com Zig?

Vale quando Redis é cache, fila simples, rate limit ou sessão. Para dados críticos, mantenha PostgreSQL ou SQLite como fonte de verdade e use Redis como camada auxiliar com TTL e falhas toleráveis.

## Conclusão

Integrar Zig com banco de dados é menos sobre escolher uma abstração famosa e mais sobre assumir controle: SQL claro, memória controlada, conexões fechadas, erros explícitos e testes reais. Para produção, comece conservador: SQLite quando o banco é local, PostgreSQL via `libpq` quando há múltiplos usuários, Redis apenas como camada auxiliar.

Esse estilo combina com a filosofia da linguagem. Zig não promete esconder a complexidade do banco; ele dá ferramentas para deixar essa complexidade visível, revisável e rápida.
