---
title: "Como Usar SQLite com Zig: CRUD Completo e Queries"
url: "https://ziglang.com.br/receitas/zig-sqlite-database/"
markdown_url: "https://ziglang.com.br/receitas/zig-sqlite-database.MD"
description: "Tutorial prático de SQLite em Zig. Aprenda a criar banco de dados, tabelas, inserir, consultar, atualizar e deletar dados com exemplos prontos."
date: "2026-02-24"
author: ""
---

# Como Usar SQLite com Zig: CRUD Completo e Queries

Tutorial prático de SQLite em Zig. Aprenda a criar banco de dados, tabelas, inserir, consultar, atualizar e deletar dados com exemplos prontos.


SQLite é o banco de dados embutido mais utilizado do mundo. Presente em smartphones, navegadores, sistemas operacionais e aplicações desktop, ele oferece um banco relacional completo em um único arquivo, sem necessidade de servidor. Combinar SQLite com **[Zig](/tutoriais/o-que-e-zig/)** resulta em aplicações extremamente rápidas, com binários pequenos e sem dependências externas de runtime.

Neste tutorial, você vai aprender a integrar SQLite em projetos Zig do zero: desde abrir um banco de dados até operações CRUD completas, prepared statements, transações e tratamento robusto de erros.

## Pré-requisitos

- Zig instalado (versão 0.13+). Veja o [guia de instalação](/tutoriais/como-instalar-zig/)
- Conhecimento básico de Zig. Consulte a [introdução ao Zig](/tutoriais/introducao-ao-zig/)
- Familiaridade com SQL básico (SELECT, INSERT, UPDATE, DELETE)

## Por que SQLite com Zig?

A combinação de Zig e SQLite é poderosa por vários motivos:

- **Interoperabilidade nativa com C**: Zig importa a API C do SQLite diretamente, sem wrappers ou bindings manuais
- **Sem overhead de runtime**: Nenhum garbage collector ou runtime pesado entre seu código e o banco de dados
- **Binário único**: SQLite é compilado estaticamente junto com seu programa Zig
- **Controle de memória**: Use os [allocators de Zig](/receitas/zig-arena-allocator/) para gerenciar memória de forma previsível
- **Cross-compilation**: Compile seu app + SQLite para qualquer plataforma com um único comando

## Configurando o Projeto

### Estrutura do Projeto

Crie um novo projeto Zig com a seguinte estrutura:

```text
meu-projeto-sqlite/
├── build.zig
├── build.zig.zon
├── src/
│   └── main.zig
└── libs/
    └── sqlite3/
        ├── sqlite3.c
        └── sqlite3.h
```

### Obtendo o SQLite

Baixe o código-fonte amalgamation do SQLite:

```bash
mkdir -p libs/sqlite3
cd libs/sqlite3
curl -O https://www.sqlite.org/2024/sqlite-amalgamation-3450000.zip
unzip sqlite-amalgamation-3450000.zip
cp sqlite-amalgamation-3450000/sqlite3.c .
cp sqlite-amalgamation-3450000/sqlite3.h .
rm -rf sqlite-amalgamation-3450000 sqlite-amalgamation-3450000.zip
```

### Configurando o build.zig

Configure o [build system de Zig](/tutoriais/zig-build-system/) para compilar o SQLite junto com seu projeto:

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

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "meu-app-sqlite",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // Compilar SQLite como biblioteca C
    exe.addCSourceFile(.{
        .file = b.path("libs/sqlite3/sqlite3.c"),
        .flags = &.{
            "-DSQLITE_THREADSAFE=1",
            "-DSQLITE_ENABLE_FTS5",
            "-DSQLITE_ENABLE_JSON1",
        },
    });
    exe.addIncludePath(b.path("libs/sqlite3"));
    exe.linkLibC();

    b.installArtifact(exe);

    // Comando para executar
    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());

    const run_step = b.step("run", "Executar a aplicação");
    run_step.dependOn(&run_cmd.step);
}
```

## Importando e Abrindo o Banco de Dados

O primeiro passo é importar a API C do SQLite e abrir (ou criar) um banco de dados:

```zig
const std = @import("std");
const c = @cImport({
    @cInclude("sqlite3.h");
});

const SqliteError = error{
    OpenFailed,
    PrepareFailed,
    StepFailed,
    BindFailed,
    ExecFailed,
    TransactionFailed,
};

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

    /// Abre ou cria um banco de dados SQLite.
    /// O arquivo é criado automaticamente se não existir.
    pub fn open(path: [*:0]const u8) !Database {
        var db: ?*c.sqlite3 = null;
        const result = c.sqlite3_open(path, &db);

        if (result != c.SQLITE_OK) {
            if (db) |d| {
                std.log.err("Erro ao abrir banco: {s}", .{c.sqlite3_errmsg(d)});
                _ = c.sqlite3_close(d);
            }
            return SqliteError.OpenFailed;
        }

        return Database{ .db = db };
    }

    /// Fecha a conexão com o banco de dados.
    pub fn close(self: *Database) void {
        if (self.db) |db| {
            _ = c.sqlite3_close(db);
            self.db = null;
        }
    }
};
```

### Exemplo de Uso Básico

```zig
pub fn main() !void {
    // Abrir banco de dados (cria o arquivo se não existir)
    var db = try Database.open("meu_banco.db");
    defer db.close();

    std.debug.print("Banco de dados aberto com sucesso!\n", .{});

    // Para usar banco de dados em memória (testes):
    // var db_mem = try Database.open(":memory:");
    // defer db_mem.close();
}
```

## Executando SQL Direto

Para comandos SQL que não retornam dados (CREATE TABLE, INSERT simples, etc.), use `sqlite3_exec`:

```zig
pub const Database = struct {
    db: ?*c.sqlite3,

    // ... (open e close definidos acima)

    /// Executa uma query SQL sem retorno de dados.
    /// Ideal para CREATE TABLE, INSERT, UPDATE, DELETE simples.
    pub fn exec(self: *Database, sql: [*:0]const u8) !void {
        var err_msg: ?[*:0]u8 = null;
        const result = c.sqlite3_exec(
            self.db,
            sql,
            null,
            null,
            &err_msg,
        );

        if (result != c.SQLITE_OK) {
            if (err_msg) |msg| {
                std.log.err("Erro SQL: {s}", .{msg});
                c.sqlite3_free(msg);
            }
            return SqliteError.ExecFailed;
        }
    }
};
```

## Criando Tabelas

Vamos criar uma tabela de usuários para demonstrar todas as operações CRUD:

```zig
fn criarTabelas(db: *Database) !void {
    try db.exec(
        \\CREATE TABLE IF NOT EXISTS usuarios (
        \\    id INTEGER PRIMARY KEY AUTOINCREMENT,
        \\    nome TEXT NOT NULL,
        \\    email TEXT NOT NULL UNIQUE,
        \\    idade INTEGER,
        \\    ativo BOOLEAN DEFAULT 1,
        \\    criado_em DATETIME DEFAULT CURRENT_TIMESTAMP
        \\);
    );

    try db.exec(
        \\CREATE TABLE IF NOT EXISTS posts (
        \\    id INTEGER PRIMARY KEY AUTOINCREMENT,
        \\    usuario_id INTEGER NOT NULL,
        \\    titulo TEXT NOT NULL,
        \\    conteudo TEXT,
        \\    publicado_em DATETIME DEFAULT CURRENT_TIMESTAMP,
        \\    FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
        \\);
    );

    try db.exec(
        \\CREATE INDEX IF NOT EXISTS idx_posts_usuario
        \\ON posts(usuario_id);
    );

    std.debug.print("Tabelas criadas com sucesso!\n", .{});
}
```

## Prepared Statements

Prepared statements são a forma segura e eficiente de executar queries com parâmetros. Eles previnem SQL injection e permitem reutilização de queries compiladas.

```zig
pub const Statement = struct {
    stmt: ?*c.sqlite3_stmt,

    /// Prepara uma query SQL para execução.
    pub fn prepare(db: *Database, sql: [*:0]const u8) !Statement {
        var stmt: ?*c.sqlite3_stmt = null;
        const result = c.sqlite3_prepare_v2(
            db.db,
            sql,
            -1,
            &stmt,
            null,
        );

        if (result != c.SQLITE_OK) {
            if (db.db) |d| {
                std.log.err("Erro ao preparar: {s}", .{c.sqlite3_errmsg(d)});
            }
            return SqliteError.PrepareFailed;
        }

        return Statement{ .stmt = stmt };
    }

    /// Vincula um inteiro a um parâmetro da query (1-indexed).
    pub fn bindInt(self: *Statement, index: c_int, value: c_int) !void {
        const result = c.sqlite3_bind_int(self.stmt, index, value);
        if (result != c.SQLITE_OK) return SqliteError.BindFailed;
    }

    /// Vincula um inteiro de 64 bits a um parâmetro da query.
    pub fn bindInt64(self: *Statement, index: c_int, value: i64) !void {
        const result = c.sqlite3_bind_int64(self.stmt, index, value);
        if (result != c.SQLITE_OK) return SqliteError.BindFailed;
    }

    /// Vincula um texto a um parâmetro da query.
    pub fn bindText(self: *Statement, index: c_int, text: [*:0]const u8) !void {
        const result = c.sqlite3_bind_text(
            self.stmt,
            index,
            text,
            -1,
            c.SQLITE_TRANSIENT,
        );
        if (result != c.SQLITE_OK) return SqliteError.BindFailed;
    }

    /// Executa um passo da query. Retorna true se há uma linha de resultado.
    pub fn step(self: *Statement) !bool {
        const result = c.sqlite3_step(self.stmt);
        if (result == c.SQLITE_ROW) return true;
        if (result == c.SQLITE_DONE) return false;
        return SqliteError.StepFailed;
    }

    /// Obtém uma coluna como inteiro.
    pub fn columnInt(self: *Statement, index: c_int) c_int {
        return c.sqlite3_column_int(self.stmt, index);
    }

    /// Obtém uma coluna como inteiro de 64 bits.
    pub fn columnInt64(self: *Statement, index: c_int) i64 {
        return c.sqlite3_column_int64(self.stmt, index);
    }

    /// Obtém uma coluna como texto.
    pub fn columnText(self: *Statement, index: c_int) ?[*:0]const u8 {
        return @ptrCast(c.sqlite3_column_text(self.stmt, index));
    }

    /// Reseta o statement para reutilização.
    pub fn reset(self: *Statement) void {
        _ = c.sqlite3_reset(self.stmt);
        _ = c.sqlite3_clear_bindings(self.stmt);
    }

    /// Libera os recursos do statement.
    pub fn finalize(self: *Statement) void {
        if (self.stmt) |stmt| {
            _ = c.sqlite3_finalize(stmt);
            self.stmt = null;
        }
    }
};
```

## Operações CRUD Completas

### CREATE: Inserindo Dados

```zig
const Usuario = struct {
    id: i64,
    nome: []const u8,
    email: []const u8,
    idade: c_int,
    ativo: bool,
};

fn inserirUsuario(db: *Database, nome: [*:0]const u8, email: [*:0]const u8, idade: c_int) !i64 {
    var stmt = try Statement.prepare(db,
        \\INSERT INTO usuarios (nome, email, idade)
        \\VALUES (?1, ?2, ?3)
    );
    defer stmt.finalize();

    try stmt.bindText(1, nome);
    try stmt.bindText(2, email);
    try stmt.bindInt(3, idade);

    _ = try stmt.step();

    // Retorna o ID gerado
    const id = c.sqlite3_last_insert_rowid(db.db);
    std.debug.print("Usuário inserido com ID: {d}\n", .{id});
    return id;
}

fn inserirVariosUsuarios(db: *Database) !void {
    const usuarios = [_]struct { nome: [*:0]const u8, email: [*:0]const u8, idade: c_int }{
        .{ .nome = "Ana Silva", .email = "ana@exemplo.com", .idade = 28 },
        .{ .nome = "Carlos Souza", .email = "carlos@exemplo.com", .idade = 35 },
        .{ .nome = "Maria Oliveira", .email = "maria@exemplo.com", .idade = 22 },
        .{ .nome = "João Santos", .email = "joao@exemplo.com", .idade = 41 },
        .{ .nome = "Beatriz Lima", .email = "bia@exemplo.com", .idade = 30 },
    };

    // Usar prepared statement reutilizável para múltiplas inserções
    var stmt = try Statement.prepare(db,
        \\INSERT INTO usuarios (nome, email, idade)
        \\VALUES (?1, ?2, ?3)
    );
    defer stmt.finalize();

    for (usuarios) |u| {
        try stmt.bindText(1, u.nome);
        try stmt.bindText(2, u.email);
        try stmt.bindInt(3, u.idade);

        _ = try stmt.step();
        stmt.reset();
    }

    std.debug.print("Inseridos {d} usuários com sucesso.\n", .{usuarios.len});
}
```

### READ: Consultando Dados

```zig
fn buscarUsuarioPorId(db: *Database, id: c_int) !void {
    var stmt = try Statement.prepare(db,
        \\SELECT id, nome, email, idade, ativo
        \\FROM usuarios
        \\WHERE id = ?1
    );
    defer stmt.finalize();

    try stmt.bindInt(1, id);

    if (try stmt.step()) {
        const nome = stmt.columnText(1) orelse "N/A";
        const email = stmt.columnText(2) orelse "N/A";
        const idade = stmt.columnInt(3);
        const ativo = stmt.columnInt(4);

        std.debug.print(
            \\Usuário encontrado:
            \\  ID:    {d}
            \\  Nome:  {s}
            \\  Email: {s}
            \\  Idade: {d}
            \\  Ativo: {s}
            \\
        , .{
            stmt.columnInt(0),
            nome,
            email,
            idade,
            if (ativo == 1) "Sim" else "Não",
        });
    } else {
        std.debug.print("Usuário com ID {d} não encontrado.\n", .{id});
    }
}

fn listarTodosUsuarios(db: *Database) !void {
    var stmt = try Statement.prepare(db,
        \\SELECT id, nome, email, idade, ativo
        \\FROM usuarios
        \\ORDER BY nome ASC
    );
    defer stmt.finalize();

    std.debug.print("\n--- Lista de Usuários ---\n", .{});
    std.debug.print("{s:<5} {s:<20} {s:<25} {s:<6} {s:<6}\n", .{
        "ID", "Nome", "Email", "Idade", "Ativo",
    });
    std.debug.print("{s}\n", .{"-" ** 65});

    var count: u32 = 0;
    while (try stmt.step()) {
        const id = stmt.columnInt(0);
        const nome = stmt.columnText(1) orelse "N/A";
        const email = stmt.columnText(2) orelse "N/A";
        const idade = stmt.columnInt(3);
        const ativo = stmt.columnInt(4);

        std.debug.print("{d:<5} {s:<20} {s:<25} {d:<6} {s:<6}\n", .{
            id,
            nome,
            email,
            idade,
            if (ativo == 1) "Sim" else "Não",
        });
        count += 1;
    }

    std.debug.print("\nTotal: {d} usuários\n", .{count});
}

fn buscarUsuariosPorIdade(db: *Database, idade_min: c_int, idade_max: c_int) !void {
    var stmt = try Statement.prepare(db,
        \\SELECT nome, email, idade
        \\FROM usuarios
        \\WHERE idade BETWEEN ?1 AND ?2
        \\ORDER BY idade ASC
    );
    defer stmt.finalize();

    try stmt.bindInt(1, idade_min);
    try stmt.bindInt(2, idade_max);

    std.debug.print("\nUsuários com idade entre {d} e {d}:\n", .{ idade_min, idade_max });

    while (try stmt.step()) {
        const nome = stmt.columnText(0) orelse "N/A";
        const email = stmt.columnText(1) orelse "N/A";
        const idade = stmt.columnInt(2);

        std.debug.print("  {s} ({s}) - {d} anos\n", .{ nome, email, idade });
    }
}
```

### UPDATE: Atualizando Dados

```zig
fn atualizarEmail(db: *Database, id: c_int, novo_email: [*:0]const u8) !void {
    var stmt = try Statement.prepare(db,
        \\UPDATE usuarios
        \\SET email = ?1
        \\WHERE id = ?2
    );
    defer stmt.finalize();

    try stmt.bindText(1, novo_email);
    try stmt.bindInt(2, id);

    _ = try stmt.step();

    const alterados = c.sqlite3_changes(db.db);
    if (alterados > 0) {
        std.debug.print("Email do usuário {d} atualizado para: {s}\n", .{ id, novo_email });
    } else {
        std.debug.print("Nenhum usuário encontrado com ID {d}.\n", .{id});
    }
}

fn desativarUsuario(db: *Database, id: c_int) !void {
    var stmt = try Statement.prepare(db,
        \\UPDATE usuarios
        \\SET ativo = 0
        \\WHERE id = ?1
    );
    defer stmt.finalize();

    try stmt.bindInt(1, id);
    _ = try stmt.step();

    const alterados = c.sqlite3_changes(db.db);
    std.debug.print("Usuários desativados: {d}\n", .{alterados});
}
```

### DELETE: Removendo Dados

```zig
fn deletarUsuario(db: *Database, id: c_int) !void {
    var stmt = try Statement.prepare(db,
        \\DELETE FROM usuarios
        \\WHERE id = ?1
    );
    defer stmt.finalize();

    try stmt.bindInt(1, id);
    _ = try stmt.step();

    const deletados = c.sqlite3_changes(db.db);
    if (deletados > 0) {
        std.debug.print("Usuário {d} deletado com sucesso.\n", .{id});
    } else {
        std.debug.print("Nenhum usuário encontrado com ID {d}.\n", .{id});
    }
}

fn deletarUsuariosInativos(db: *Database) !void {
    try db.exec(
        \\DELETE FROM usuarios WHERE ativo = 0
    );

    const deletados = c.sqlite3_changes(db.db);
    std.debug.print("Removidos {d} usuários inativos.\n", .{deletados});
}
```

## Transações

Transações garantem que múltiplas operações sejam executadas atomicamente. Se qualquer operação falhar, todas são revertidas:

```zig
pub const Transaction = struct {
    db: *Database,

    /// Inicia uma transação.
    pub fn begin(db: *Database) !Transaction {
        try db.exec("BEGIN TRANSACTION");
        return Transaction{ .db = db };
    }

    /// Confirma todas as operações da transação.
    pub fn commit(self: *Transaction) !void {
        try self.db.exec("COMMIT");
    }

    /// Reverte todas as operações da transação.
    pub fn rollback(self: *Transaction) void {
        self.db.exec("ROLLBACK") catch |err| {
            std.log.err("Erro ao fazer rollback: {}", .{err});
        };
    }
};

fn transferirDados(db: *Database) !void {
    var tx = try Transaction.begin(db);
    errdefer tx.rollback();

    // Múltiplas operações dentro da transação
    try db.exec(
        \\INSERT INTO usuarios (nome, email, idade) VALUES ('Teste 1', 'teste1@ex.com', 25)
    );
    try db.exec(
        \\INSERT INTO usuarios (nome, email, idade) VALUES ('Teste 2', 'teste2@ex.com', 30)
    );
    try db.exec(
        \\INSERT INTO posts (usuario_id, titulo, conteudo)
        \\VALUES (1, 'Meu primeiro post', 'Conteúdo do post...')
    );

    // Se tudo deu certo, confirma
    try tx.commit();
    std.debug.print("Transação concluída com sucesso!\n", .{});
}
```

Note o uso de `errdefer` para garantir o rollback automático em caso de erro. Esse é um padrão idiomático de Zig para gerenciamento de recursos. Veja mais sobre esse padrão em [errdefer em Zig](/receitas/zig-errdefer-pattern/).

## Inserções em Lote com Performance

Para inserir grandes volumes de dados, combine transações com prepared statements reutilizáveis:

```zig
fn inserirEmLote(db: *Database, allocator: std.mem.Allocator) !void {
    _ = allocator;

    var tx = try Transaction.begin(db);
    errdefer tx.rollback();

    var stmt = try Statement.prepare(db,
        \\INSERT INTO usuarios (nome, email, idade)
        \\VALUES (?1, ?2, ?3)
    );
    defer stmt.finalize();

    // Simulando inserção de 1000 registros
    var i: u32 = 0;
    while (i < 1000) : (i += 1) {
        // Em um caso real, os dados viriam de outra fonte
        try stmt.bindText(1, "Usuário Lote");
        try stmt.bindText(2, "lote@exemplo.com");
        try stmt.bindInt(3, @intCast(i % 60 + 18));

        _ = try stmt.step();
        stmt.reset();
    }

    try tx.commit();
    std.debug.print("Inseridos 1000 registros em lote.\n", .{});
}
```

Essa abordagem pode ser **100x mais rápida** do que inserções individuais sem transação, pois o SQLite não precisa fazer fsync no disco para cada operação.

## Queries com JOIN

Consultas que combinam dados de múltiplas tabelas:

```zig
fn listarPostsComAutor(db: *Database) !void {
    var stmt = try Statement.prepare(db,
        \\SELECT
        \\    p.id,
        \\    p.titulo,
        \\    u.nome AS autor,
        \\    p.publicado_em
        \\FROM posts p
        \\INNER JOIN usuarios u ON u.id = p.usuario_id
        \\ORDER BY p.publicado_em DESC
    );
    defer stmt.finalize();

    std.debug.print("\n--- Posts Recentes ---\n", .{});

    while (try stmt.step()) {
        const id = stmt.columnInt(0);
        const titulo = stmt.columnText(1) orelse "Sem título";
        const autor = stmt.columnText(2) orelse "Desconhecido";
        const data = stmt.columnText(3) orelse "N/A";

        std.debug.print("[{d}] \"{s}\" por {s} em {s}\n", .{
            id, titulo, autor, data,
        });
    }
}

fn contarPostsPorUsuario(db: *Database) !void {
    var stmt = try Statement.prepare(db,
        \\SELECT u.nome, COUNT(p.id) AS total_posts
        \\FROM usuarios u
        \\LEFT JOIN posts p ON p.usuario_id = u.id
        \\GROUP BY u.id
        \\ORDER BY total_posts DESC
    );
    defer stmt.finalize();

    std.debug.print("\n--- Posts por Usuário ---\n", .{});

    while (try stmt.step()) {
        const nome = stmt.columnText(0) orelse "N/A";
        const total = stmt.columnInt(1);

        std.debug.print("  {s}: {d} posts\n", .{ nome, total });
    }
}
```

## Tratamento Robusto de Erros

Um wrapper de erro mais detalhado que captura informações do SQLite:

```zig
pub const SqliteDetailedError = struct {
    code: c_int,
    message: []const u8,

    pub fn format(
        self: SqliteDetailedError,
        comptime fmt: []const u8,
        options: std.fmt.FormatOptions,
        writer: anytype,
    ) !void {
        _ = fmt;
        _ = options;
        try writer.print("SQLite Error {d}: {s}", .{ self.code, self.message });
    }
};

fn getLastError(db: *Database) SqliteDetailedError {
    const code = c.sqlite3_errcode(db.db);
    const msg = c.sqlite3_errmsg(db.db);
    return .{
        .code = code,
        .message = std.mem.span(msg),
    };
}

fn execComRetry(db: *Database, sql: [*:0]const u8, max_tentativas: u32) !void {
    var tentativa: u32 = 0;
    while (tentativa < max_tentativas) : (tentativa += 1) {
        var err_msg: ?[*:0]u8 = null;
        const result = c.sqlite3_exec(db.db, sql, null, null, &err_msg);

        if (result == c.SQLITE_OK) return;

        if (err_msg) |msg| {
            std.log.warn(
                "Tentativa {d}/{d} falhou: {s}",
                .{ tentativa + 1, max_tentativas, msg },
            );
            c.sqlite3_free(msg);
        }

        // Se o banco está travado, espera e tenta novamente
        if (result == c.SQLITE_BUSY or result == c.SQLITE_LOCKED) {
            std.time.sleep(100 * std.time.ns_per_ms);
            continue;
        }

        return SqliteError.ExecFailed;
    }

    return SqliteError.ExecFailed;
}
```

## Configuração de Performance

Otimize o SQLite para diferentes cenários de uso:

```zig
fn configurarPerformance(db: *Database) !void {
    // WAL mode: permite leituras concorrentes durante escritas
    try db.exec("PRAGMA journal_mode=WAL");

    // Sincronização: NORMAL é um bom equilíbrio entre segurança e velocidade
    try db.exec("PRAGMA synchronous=NORMAL");

    // Cache em memória: 64MB (em páginas de 4KB)
    try db.exec("PRAGMA cache_size=-65536");

    // Tamanho da página: 4KB (padrão otimizado)
    try db.exec("PRAGMA page_size=4096");

    // Habilitar chaves estrangeiras
    try db.exec("PRAGMA foreign_keys=ON");

    // Armazenar temporários em memória
    try db.exec("PRAGMA temp_store=MEMORY");

    // Timeout de busy (5 segundos)
    _ = c.sqlite3_busy_timeout(db.db, 5000);

    std.debug.print("Configurações de performance aplicadas.\n", .{});
}
```

## Exemplo Completo: Aplicação CRUD

Reunindo tudo em uma aplicação funcional:

```zig
const std = @import("std");
const c = @cImport({
    @cInclude("sqlite3.h");
});

// Inclua aqui as definições de Database, Statement e Transaction
// mostradas nas seções anteriores

pub fn main() !void {
    // 1. Abrir banco de dados
    var db = try Database.open("app.db");
    defer db.close();

    // 2. Configurar performance
    try configurarPerformance(&db);

    // 3. Criar tabelas
    try criarTabelas(&db);

    // 4. Inserir dados
    const id_ana = try inserirUsuario(&db, "Ana Silva", "ana@exemplo.com", 28);
    _ = try inserirUsuario(&db, "Carlos Souza", "carlos@exemplo.com", 35);
    _ = try inserirUsuario(&db, "Maria Oliveira", "maria@exemplo.com", 22);

    // 5. Consultar dados
    try buscarUsuarioPorId(&db, @intCast(id_ana));
    try listarTodosUsuarios(&db);
    try buscarUsuariosPorIdade(&db, 25, 35);

    // 6. Atualizar dados
    try atualizarEmail(&db, 1, "ana.silva@novodominio.com");

    // 7. Transação
    try transferirDados(&db);

    // 8. Listar posts com autores
    try listarPostsComAutor(&db);
    try contarPostsPorUsuario(&db);

    // 9. Deletar dados
    try desativarUsuario(&db, 3);
    try deletarUsuariosInativos(&db);

    // 10. Listar resultado final
    try listarTodosUsuarios(&db);

    std.debug.print("\nAplicação CRUD concluída com sucesso!\n", .{});
}
```

## Boas Práticas

Ao trabalhar com SQLite em Zig, siga estas recomendações:

1. **Sempre use prepared statements** para queries com parâmetros. Nunca concatene strings SQL diretamente -- isso evita SQL injection e melhora a performance.

2. **Use `defer` para liberar recursos**: Sempre chame `stmt.finalize()` e `db.close()` com `defer` para garantir a liberação mesmo em caso de erro.

3. **Transações para múltiplas escritas**: Envolva inserções e atualizações em lote em uma transação. A diferença de performance é dramática.

4. **`errdefer` para rollback**: Use `errdefer tx.rollback()` para reverter transações automaticamente quando uma operação falhar.

5. **WAL mode em produção**: O modo WAL (Write-Ahead Logging) melhora significativamente a concorrência de leitura/escrita.

6. **Timeout de busy**: Configure `sqlite3_busy_timeout` para evitar erros de "database is locked" em ambientes concorrentes.

7. **Valide o retorno**: Sempre verifique os códigos de retorno das funções SQLite. A API de Zig com error unions torna isso natural.

## Dicas de Debug

Para debugar problemas com SQLite em Zig:

```zig
fn debugQuery(db: *Database, sql: [*:0]const u8) void {
    // Mostrar o plano de execução da query
    const explain_sql = std.fmt.allocPrintZ(
        std.heap.page_allocator,
        "EXPLAIN QUERY PLAN {s}",
        .{sql},
    ) catch return;
    defer std.heap.page_allocator.free(explain_sql);

    var stmt = Statement.prepare(db, explain_sql) catch return;
    defer stmt.finalize();

    std.debug.print("\nPlano de execução:\n", .{});
    while (stmt.step() catch false) {
        const detail = stmt.columnText(3) orelse "N/A";
        std.debug.print("  {s}\n", .{detail});
    }
}
```

## Alternativas para Bancos de Dados em Zig

Se o SQLite não atende às suas necessidades, considere:

- **[PostgreSQL com Zig](/tutoriais/zig-postgres-driver/)**: Para aplicações que precisam de um banco de dados cliente-servidor robusto com suporte a JSON, arrays e tipos customizados.
- **[Redis com Zig](/receitas/zig-redis-client/)**: Para cache em memória, filas de mensagens e pub/sub de alta performance.
- **Arquivos mapeados em memória**: Para dados simples, Zig oferece excelente suporte a mmap via a [stdlib](/stdlib/).

## Próximos Passos

Agora que você domina SQLite com Zig, explore estes tópicos relacionados:

1. **[PostgreSQL com Zig](/tutoriais/zig-postgres-driver/)**: Conecte-se a bancos de dados PostgreSQL para aplicações distribuídas
2. **[Redis com Zig](/receitas/zig-redis-client/)**: Implemente cache e pub/sub de alta performance
3. **[Servidor HTTP em Zig](/tutoriais/zig-http-server/)**: Combine SQLite com uma API HTTP para criar serviços web completos
4. **[Tratamento de erros](/tutoriais/tratamento-de-erros-em-zig/)**: Aprofunde-se nos error unions e errdefer
5. **[Gerenciamento de memória](/tutoriais/gerenciamento-de-memoria-zig/)**: Entenda allocators para otimizar o uso de memória com bancos de dados
6. **[Testes em Zig](/tutoriais/testes-zig/)**: Escreva testes automatizados para suas operações de banco de dados
7. **[Cross-compilation](/tutoriais/zig-cross-compilation/)**: Compile sua aplicação SQLite para qualquer plataforma com um comando

SQLite com Zig é uma combinação poderosa para aplicações que precisam de um banco de dados confiável sem a complexidade de um servidor dedicado. Com o código deste tutorial como base, você pode construir desde ferramentas CLI até servidores web completos com persistência de dados.
