SQLITE 3 – Guia rápido

Saudações.

Vou ensinar como usar o banco de dados em arquivo chamado SQLITE3.

Pré-requisitos (constam em outros artigos aqui do blog):

  • Instalação do Linux (Debian);
  • Internet no servidor para instalar os programas;

Links úteis:

1 – Sobre o SQLITE

O SQLITE é, em uma definição simples, um banco de dados SQL de arquivo único embutido (embedded), auto-contido, sem servidor (serverless) e de configuração zero.

Diferente do PostgreSQL ou MySQL, o SQLite não requer um processo de servidor separado – ele lê e escreve diretamente em arquivos de disco.

Crie o arquivo “.db” ou “.sqlite” com pelo menos 1 tabela e você ja pode fazer uso imediato e armazenar dados.

Toda a política de banco de dados, tabelas, relações e execução é implementado pela biblioteca SQLITE executada pelo software cliente. Esse desafio impõe na libsqlite um peso grande, exigindo dela perfeição e algoritmos de última geração.

Ao alcançar estabilidade máxima ele se tornou o banco de dados preferido por milhões de aplicativos para smartphone (Whatsapp por exemplo) e por softwares modernos (padrão do N8N).

Principais características:

  • Serverless: Não requer instalação ou configuração de servidor;
  • Self-contained: Uma única biblioteca C sem dependências externas;
  • Zero-configuration: Não precisa de setup ou administração;
  • Cross-platform: Funciona em qualquer sistema operacional;
  • Transacional: Suporte completo a ACID (Atomicidade, Consistência, Isolamento, Durabilidade);
  • Compacto: Biblioteca com menos de 1MB.
  • Segurança: ele NÃO possui sistema de usuários ou permissões interna, a segurança é gerenciada pelo sistema de arquivos do sistema operacional.

Instalando no Linux:

Bash
apt-get -y install sqlite3;

2 – Primeiro contato

Vamos criar um banco (arquivo):

Bash
# "Conectar" ao banco via terminal, se o arquivo não existir ele será criado:
sqlite3 /tmp/meubanco.db;

Ao executar o comando sqlite3, você estará no shell do SQLITE (linha de comando).

Use o comando “.quit” ou “.exit” para sair:

SQLITE3 Shell
# Sair do shell
.quit

O arquivo será criado vazio (zero bytes). Outra forma é entrar no comando sqlite3 sem especificar o arquivo do banco e carregá-lo pelo shell SQLITE:

Bash
# Entrar no shell sem informar o arquivo do banco:
sqlite3
SQLITE3 Shell
-- Carregar arquivo, abrir/criar banco de dados
.open /tmp/meubanco.db

-- Exibir banco atual
.databases
  -- main: /tmp/meubanco.db r/w

-- Verificar integridade do banco
PRAGMA integrity_check;
    -- ok

-- Verificação rápida
PRAGMA quick_check;
    -- ok

-- Salvar banco atual no mesmo arquivo
.save /tmp/meubanco.db

-- Salvar banco atual em arquivo versionado (backup manual)
.save /tmp/meubanco-v01.db

.exit

3 – Comandos rápidos do SQLITE3

Quando estiver no shell do SQLITE, use o “.help” para pedir ajuda, guia completo de referência dos comandos de ponto:

SQLITE3 Shell
-- COMANDOS DE PONTO (dot commands) - específicos do shell sqlite3

.help                       -- Ajuda geral dos comandos
.help COMMAND               -- Ajuda para comando específico
.quit                       -- Sair do SQLite (ou .exit)
.exit                       -- Sair do SQLite (ou .quit)

.databases                  -- Listar bancos de dados conectados
.tables                     -- Listar tabelas do banco atual
.tables pattern             -- Listar tabelas que correspondem ao padrão
.schema                     -- Mostrar estrutura de todas as tabelas
.schema tabela              -- Mostrar estrutura de uma tabela específica
.fullschema                 -- Schema completo incluindo estatísticas

.indices                    -- Listar todos os índices
.indices tabela             -- Listar índices de uma tabela específica

.headers on                 -- Mostrar cabeçalhos nas consultas
.headers off                -- Ocultar cabeçalhos nas consultas
.mode column                -- Modo de exibição em colunas alinhadas
.mode table                 -- Modo de exibição em tabela formatada
.mode box                   -- Modo de exibição em caixa (bordas)
.mode list                  -- Modo de exibição em lista (separado por |)
.mode csv                   -- Modo de exibição CSV
.mode json                  -- Modo de exibição JSON
.mode line                  -- Modo de exibição uma coluna por linha
.mode markdown              -- Modo de exibição markdown
.mode html                  -- Modo de exibição HTML
.width N1 N2 ...            -- Definir largura das colunas

.nullvalue 'NULL'           -- Como exibir valores NULL
.separator ','              -- Definir separador para modo list/csv

.timer on                   -- Mostrar tempo de execução das queries
.timer off                  -- Ocultar tempo de execução

.open arquivo.db            -- Abrir/criar banco de dados
.save arquivo.db            -- Salvar banco atual em arquivo
.backup arquivo.db          -- Fazer backup do banco atual
.restore arquivo.db         -- Restaurar banco de backup

.read script.sql            -- Executar comandos de um arquivo SQL
.output arquivo.txt         -- Redirecionar saída para arquivo
.output stdout              -- Voltar saída para tela

.dump                       -- Exportar banco inteiro como SQL
.dump tabela                -- Exportar tabela específica como SQL

.import arquivo.csv tabela  -- Importar CSV para tabela
.excel                      -- Abrir resultado no Excel (se disponível)

.show                       -- Mostrar configurações atuais
.stats on                   -- Mostrar estatísticas de execução
.stats off                  -- Ocultar estatísticas

.shell COMMAND              -- Executar comando do sistema
.system COMMAND             -- Executar comando do sistema (alias)
! COMMAND                   -- Executar comando do sistema (atalho)

.changes on                 -- Mostrar número de linhas alteradas
.changes off                -- Ocultar número de linhas alteradas

.eqp on                     -- Mostrar plano de execução das queries
.eqp off                    -- Ocultar plano de execução
.explain on                 -- Modo de exibição para EXPLAIN
.explain off                -- Voltar ao modo normal

.print texto                -- Imprimir texto na saída
.once arquivo               -- Próximo resultado vai para arquivo
.log arquivo                -- Registrar todas as operações em arquivo
.log off                    -- Parar de registrar

-- PRAGMA - Comandos de configuração e informação

PRAGMA database_list;       -- Listar bancos de dados anexados
PRAGMA table_info(tabela);  -- Informações das colunas de uma tabela
PRAGMA table_xinfo(tabela); -- Informações estendidas das colunas
PRAGMA index_list(tabela);  -- Lista de índices de uma tabela
PRAGMA index_info(indice);  -- Informações de um índice
PRAGMA foreign_key_list(t); -- Lista de chaves estrangeiras

PRAGMA foreign_keys;        -- Verificar se FK está habilitada
PRAGMA foreign_keys = ON;   -- Habilitar verificação de FK
PRAGMA foreign_keys = OFF;  -- Desabilitar verificação de FK

PRAGMA journal_mode;        -- Ver modo de journal atual
PRAGMA journal_mode=WAL;    -- Usar Write-Ahead Logging (recomendado)
PRAGMA journal_mode=DELETE; -- Modo padrão (delete journal após commit)

PRAGMA synchronous;         -- Ver modo de sincronização
PRAGMA synchronous=NORMAL;  -- Modo normal (equilíbrio)
PRAGMA synchronous=FULL;    -- Modo seguro (mais lento)
PRAGMA synchronous=OFF;     -- Modo rápido (risco de corrupção)

PRAGMA cache_size;          -- Ver tamanho do cache
PRAGMA cache_size=10000;    -- Definir cache (páginas, negativo=KB)

PRAGMA page_size;           -- Ver tamanho da página
PRAGMA page_count;          -- Número de páginas no banco
PRAGMA freelist_count;      -- Páginas livres (não usadas)

PRAGMA integrity_check;     -- Verificar integridade do banco
PRAGMA quick_check;         -- Verificação rápida de integridade

PRAGMA optimize;            -- Otimizar banco de dados
VACUUM;                     -- Reconstruir banco (compactar)

PRAGMA busy_timeout=5000;   -- Timeout para banco bloqueado (ms)
PRAGMA encoding;            -- Ver encoding do banco (UTF-8, etc.)

-- TRANSAÇÕES

BEGIN;                      -- Iniciar transação
BEGIN TRANSACTION;          -- Iniciar transação (explícito)
BEGIN IMMEDIATE;            -- Iniciar com lock imediato
BEGIN EXCLUSIVE;            -- Iniciar com lock exclusivo
COMMIT;                     -- Confirmar transação
ROLLBACK;                   -- Desfazer transação
SAVEPOINT nome;             -- Criar ponto de salvamento
RELEASE nome;               -- Liberar savepoint
ROLLBACK TO nome;           -- Voltar ao savepoint

-- ATTACH/DETACH - Múltiplos bancos

ATTACH 'outro.db' AS outro; -- Anexar outro banco de dados
DETACH outro;               -- Desanexar banco de dados

4 – Usando SQLITE em scripts shell/bash

Essa é a maneira mais usada para inicializar bancos SQLITE, observe:

Bash
# Criar diretório para bancos
mkdir -p /var/lib/sqlite;

# Criar banco de dados do sistema
sqlite3 /var/lib/sqlite/sistema.db << 'EOF'
.headers on
.mode column
CREATE TABLE IF NOT EXISTS config (
    chave TEXT PRIMARY KEY,
    valor TEXT
);
INSERT INTO config VALUES ('versao', '1.0.0');
INSERT INTO config VALUES ('nome', 'MeuSistema');
SELECT * FROM config;
EOF


# Ou  no meu formato:
# - arquivo com esquema de tabelas
(
    echo '.headers on';
    echo '.mode column';
    echo 'CREATE TABLE IF NOT EXISTS config (';
    echo '    chave TEXT PRIMARY KEY,';
    echo '    valor TEXT';
    echo ');';
) > /var/lib/sqlite/001-schema.sql;

# - arquivo com dados iniciais
(
    echo "INSERT INTO config VALUES ('versao', '1.0.0');";
    echo "INSERT INTO config VALUES ('nome', 'MeuSistema');";
) > /var/lib/sqlite/010-startup-config.sql;

# Aplicar:
sqlite3 /var/lib/sqlite/sistema.db < /var/lib/sqlite/001-schema.sql;
sqlite3 /var/lib/sqlite/sistema.db < /var/lib/sqlite/010-startup-config.sql;


# Conferindo:
sqlite3 /var/lib/sqlite/sistema.db "SELECT * FROM config;";

# Verificar arquivo criado
ls -lah /var/lib/sqlite/sistema.db;
    # -rw-r--r-- 1 root root 12K Dec 20 11:04 /var/lib/sqlite/sistema.db

# Conferir o esquema aplicado e as tabelas
sqlite3 /var/lib/sqlite/sistema.db ".schema";
    # CREATE TABLE config (
    #     chave TEXT PRIMARY KEY,
    #     valor TEXT
    # );

sqlite3 /var/lib/sqlite/sistema.db ".tables";
    # config

Exemplos de execução em linha de comando:

Bash
# Criar banco e executar comando único
sqlite3 teste.db "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);";

# Inserir dados via comando
sqlite3 teste.db "INSERT INTO users (name) VALUES ('João');";
sqlite3 teste.db "INSERT INTO users (name) VALUES ('Maria');";

# Consultar dados
sqlite3 teste.db "SELECT * FROM users;";

# Executar com formatação
sqlite3 -header -column teste.db "SELECT * FROM users;";

# Saída em CSV
sqlite3 -header -csv teste.db "SELECT * FROM users;" > users.csv;

# Saída em JSON
sqlite3 -json teste.db "SELECT * FROM users;";

# Executar arquivo SQL
cat > script.sql << 'EOF'
.headers on
.mode box
SELECT * FROM users;
SELECT COUNT(*) AS total FROM users;
EOF

sqlite3 teste.db < script.sql;

# Executar múltiplos comandos
sqlite3 teste.db << 'EOF'
.headers on
.mode column
SELECT * FROM users;
INSERT INTO users (name) VALUES ('Pedro');
SELECT * FROM users;
EOF

# Dump do banco
sqlite3 teste.db .dump > backup.sql;

# Restore do dump
sqlite3 novo_banco.db < backup.sql;

# Verificar integridade
sqlite3 teste.db "PRAGMA integrity_check;";

# Ver estrutura
sqlite3 teste.db ".schema";

# Modo interativo com configurações
sqlite3 -header -column -nullvalue NULL teste.db;

# One-liner para estatísticas
sqlite3 teste.db "SELECT COUNT(*) as total, MAX(id) as ultimo_id FROM users;";

# Limpar arquivos de teste
rm -f teste.db novo_banco.db script.sql backup.sql users.csv;

x

x

5 – Configuração de comportamento

O SQLITE possui vários parâmetros para flexibilizar o uso, permitir acesso simultâneo de escrita e leitura (WAL):

Bash
# Pasta para salvar arquivos
mkdir -p /var/lib/sqlite;

# Entrar no sqlite3
sqlite3;

Dentro do sqlite3:

SQLITE3 Shell
-- Configurar modo de exibição
.headers on
.mode box

-- Criar banco de aplicação
.open /var/lib/sqlite/certificados.db;

-- Verificar informações do banco
PRAGMA database_list;
PRAGMA page_size;
PRAGMA page_count;
PRAGMA encoding;

-- Otimizar para performance
PRAGMA journal_mode = WAL;           -- Write-Ahead Logging
PRAGMA synchronous = NORMAL;         -- Equilíbrio segurança/velocidade
PRAGMA cache_size = -64000;          -- 64MB de cache
PRAGMA temp_store = MEMORY;          -- Temporários em RAM
PRAGMA mmap_size = 268435456;        -- 256MB memory-mapped I/O

-- Verificar configurações
PRAGMA journal_mode;
PRAGMA synchronous;
PRAGMA cache_size;


-- Salvar explicitamente
.save /var/lib/sqlite/certificados.db;

.quit

5 – Tipos de colunas

Os tipos de colunas no SQLITE são:

AfinidadeTipos aceitosDescrição
INTEGERINT, INTEGER, TINYINT, SMALLINT, MEDIUMINT, BIGINT, INT2, INT8Números inteiros
REALREAL, DOUBLE, FLOAT, DOUBLE PRECISIONPonto flutuante
TEXTTEXT, VARCHAR, CHAR, CLOB, CHARACTERStrings de texto
BLOBBLOB, nenhum tipo especificadoDados binários
NUMERICNUMERIC, DECIMAL, BOOLEAN, DATE, DATETIMENumérico genérico
DATE/TIMEO SQLite NÃO possui tipo DATE/TIME nativo, datas são armazenadas como TEXT, INTEGER ou REAL*Não*

Praticando tipos:

SQLITE3 Shell
-- Criar banco
.open /var/lib/sqlite/tipos.db;


-- Tabela para testes de tipos
-- ============================================

-- O SQLite aceita QUALQUER dado em QUALQUER coluna (exceto INTEGER PRIMARY KEY)
CREATE TABLE IF NOT EXISTS teste_de_tipos (
    id INTEGER PRIMARY KEY,
    coluna_int INTEGER,
    coluna_text TEXT,
    coluna_real REAL
);

-- Inserir dados de tipos errados - SQLite aceita:
INSERT INTO teste_de_tipos VALUES (1, 'texto', 123, 'abc');
INSERT INTO teste_de_tipos VALUES (2, 3.14, NULL, 999);

-- Ver dados
SELECT * FROM teste_de_tipos;

-- Ver tipos reais armazenados
SELECT 
    id,
    typeof(coluna_int) AS tipo_int,
    typeof(coluna_text) AS tipo_text,
    typeof(coluna_real) AS tipo_real
FROM teste_de_tipos;




-- Tabela de teste de numeros
-- ============================================
CREATE TABLE teste_integer (
    id INTEGER PRIMARY KEY,
    pequeno INTEGER,           -- Qualquer inteiro
    contador INTEGER DEFAULT 0,
    booleano INTEGER CHECK (booleano IN (0, 1))
);

-- Inserir valores
INSERT INTO teste_integer (pequeno, contador, booleano) VALUES (42, 100, 1);
INSERT INTO teste_integer (pequeno, contador, booleano) VALUES (-999, 0, 0);
INSERT INTO teste_integer (pequeno, contador, booleano) VALUES (9223372036854775807, 1, 1);

-- Visualizar
SELECT * FROM teste_integer;

-- Operações matemáticas
SELECT 
    id,
    pequeno,
    pequeno * 2 AS dobro,
    pequeno + 100 AS mais_cem,
    ABS(pequeno) AS absoluto
FROM teste_integer;



-- Tabela de teste de numeros reais
-- ============================================
CREATE TABLE teste_real (
    id INTEGER PRIMARY KEY,
    preco REAL,
    taxa REAL DEFAULT 0.0,
    coordenada_lat REAL,
    coordenada_lng REAL
);

-- Inserir valores decimais
INSERT INTO teste_real (preco, taxa, coordenada_lat, coordenada_lng) 
VALUES (99.99, 0.15, -23.5505199, -46.6333094);

INSERT INTO teste_real (preco, taxa, coordenada_lat, coordenada_lng) 
VALUES (1234.56789, 0.0825, -22.9068467, -43.1728965);

-- Consultar com formatação
SELECT 
    id,
    printf('%.2f', preco) AS preco,
    printf('%.4f%%', taxa * 100) AS taxa_percentual,
    printf('%.8f', coordenada_lat) AS latitude,
    printf('%.8f', coordenada_lng) AS longitude
FROM teste_real;

-- Cálculos com REAL
SELECT 
    preco,
    taxa,
    printf('%.2f', preco * (1 + taxa)) AS preco_com_taxa,
    ROUND(preco, 1) AS preco_arredondado
FROM teste_real;




-- Tabela de teste de texto
-- ============================================
CREATE TABLE teste_text (
    id INTEGER PRIMARY KEY,
    nome TEXT NOT NULL,
    descricao TEXT,
    email TEXT UNIQUE,
    json_data TEXT,          -- Armazenar JSON como texto
    codigo TEXT CHECK (LENGTH(codigo) <= 10)
);

-- Inserir textos
INSERT INTO teste_text (nome, descricao, email, codigo) 
VALUES ('Produto A', 'Descrição longa do produto...', 'a@email.com', 'ABC123');

INSERT INTO teste_text (nome, email, json_data) 
VALUES ('Produto B', 'b@email.com', '{"cor": "azul", "tamanho": "M"}');

-- Consultas com funções de texto
SELECT 
    nome,
    LENGTH(nome) AS tamanho_nome,
    UPPER(nome) AS nome_maiusculo,
    LOWER(nome) AS nome_minusculo,
    SUBSTR(nome, 1, 5) AS primeiros_5
FROM teste_text;

-- Busca com LIKE
SELECT * FROM teste_text WHERE nome LIKE 'Produto%';
SELECT * FROM teste_text WHERE email LIKE '%@email.com';

-- Busca case-insensitive
SELECT * FROM teste_text WHERE nome LIKE '%produto%' COLLATE NOCASE;

-- Concatenação
SELECT nome || ' - ' || COALESCE(descricao, 'Sem descrição') AS nome_completo
FROM teste_text;

-- Extrair JSON (SQLite 3.38+)
SELECT 
    nome,
    json_data,
    json_extract(json_data, '$.cor') AS cor,
    json_extract(json_data, '$.tamanho') AS tamanho
FROM teste_text
WHERE json_data IS NOT NULL;




-- Tabela de teste de blob/binario
-- ============================================
CREATE TABLE teste_blob (
    id INTEGER PRIMARY KEY,
    nome TEXT NOT NULL,
    arquivo BLOB,
    hash_md5 BLOB
);

-- Inserir dados binários (hexadecimal)
INSERT INTO teste_blob (nome, arquivo, hash_md5) 
VALUES ('imagem.png', X'89504E470D0A1A0A', X'D41D8CD98F00B204E9800998ECF8427E');

-- Inserir via zeroblob (blob de zeros)
INSERT INTO teste_blob (nome, arquivo) 
VALUES ('placeholder', zeroblob(1024));  -- 1KB de zeros

-- Consultar
SELECT 
    id,
    nome,
    LENGTH(arquivo) AS tamanho_bytes,
    HEX(arquivo) AS arquivo_hex,
    HEX(hash_md5) AS hash_hex
FROM teste_blob;

-- Verificar tipo
SELECT nome, typeof(arquivo) AS tipo FROM teste_blob;

DROP TABLE teste_blob;




-- Tabela basica de controle de data/hora
-- ============================================
CREATE TABLE teste_datetime (
    id INTEGER PRIMARY KEY,
    evento TEXT NOT NULL,
    -- Diferentes formas de armazenar data/hora
    data_texto TEXT,              -- ISO8601: '2024-01-15 14:30:00'
    data_unix INTEGER,            -- Unix timestamp: 1705329000
    data_julian REAL              -- Dia Juliano: 2460325.10417
);

-- Inserir datas
INSERT INTO teste_datetime (evento, data_texto, data_unix, data_julian) VALUES 
    ('Evento 1', datetime('now'), strftime('%s', 'now'), julianday('now'));

INSERT INTO teste_datetime (evento, data_texto, data_unix, data_julian) VALUES 
    ('Evento 2', '2024-06-15 10:00:00', 1718445600, 2460476.91667);

INSERT INTO teste_datetime (evento, data_texto) VALUES 
    ('Evento 3', datetime('now', '+7 days'));

-- Consultar e formatar datas
SELECT 
    evento,
    data_texto,
    date(data_texto) AS apenas_data,
    time(data_texto) AS apenas_hora,
    strftime('%d/%m/%Y', data_texto) AS data_br,
    strftime('%H:%M', data_texto) AS hora_curta
FROM teste_datetime;

-- Converter Unix timestamp
SELECT 
    evento,
    data_unix,
    datetime(data_unix, 'unixepoch') AS data_legivel,
    datetime(data_unix, 'unixepoch', 'localtime') AS data_local
FROM teste_datetime;

-- Funções de data úteis
SELECT 
    datetime('now') AS agora,
    date('now') AS hoje,
    time('now') AS hora_atual,
    datetime('now', 'localtime') AS agora_local,
    datetime('now', '+1 day') AS amanha,
    datetime('now', '-1 month') AS mes_passado,
    datetime('now', 'start of month') AS inicio_mes,
    datetime('now', 'start of year') AS inicio_ano,
    strftime('%W', 'now') AS semana_ano,
    strftime('%j', 'now') AS dia_ano;

-- Calcular diferença entre datas
SELECT 
    evento,
    data_texto,
    CAST((julianday('now') - julianday(data_texto)) AS INTEGER) AS dias_passados,
    CAST((julianday('now') - julianday(data_texto)) * 24 AS INTEGER) AS horas_passadas
FROM teste_datetime;

DROP TABLE teste_datetime;




-- Tabela basica de controle de usuarios
-- ============================================
CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,   -- Auto incremento
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    status INTEGER DEFAULT 1,               -- Boolean como INTEGER
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
);

-- Ver estrutura
.schema users
PRAGMA table_info(users);

-- Inserir dados
INSERT INTO users (name, email) VALUES ('João', 'joao@email.com');
INSERT INTO users (name, email) VALUES ('Maria', 'maria@email.com');
INSERT INTO users (name, email, status) VALUES ('Pedro', 'pedro@email.com', 0);

-- Consultar
SELECT * FROM users;


-- Salvar explicitamente
.save /var/lib/sqlite/tipos.db;

.quit

6 – Índices

Índices ajudam a localizar registros mais rápidos.

SQLITE3 Shell
.open /var/lib/sqlite/indices.db
.headers on
.mode box

-- Mostrar tempo de execução
.timer on

-- Criar tabela de exemplo com muitos dados
-- ============================================
CREATE TABLE IF NOT EXISTS clientes (
    id INTEGER PRIMARY KEY,
    cpf TEXT UNIQUE NOT NULL,
    nome TEXT NOT NULL,
    email TEXT,
    cidade TEXT,
    estado TEXT,
    ativo INTEGER DEFAULT 1,
    criado_em TEXT DEFAULT CURRENT_TIMESTAMP
);

-- Inserir dados de teste (10.000 registros simulados)
WITH RECURSIVE cnt(x) AS (
    SELECT 1
    UNION ALL
    SELECT x + 1 FROM cnt WHERE x < 10000
)

INSERT INTO clientes (cpf, nome, email, cidade, estado)
SELECT 
    printf('%011d', x) AS cpf,
    'Cliente ' || x AS nome,
    'cliente' || x || '@email.com' AS email,
    CASE (x % 5) 
        WHEN 0 THEN 'São Paulo'
        WHEN 1 THEN 'Rio de Janeiro'
        WHEN 2 THEN 'Belo Horizonte'
        WHEN 3 THEN 'Curitiba'
        ELSE 'Porto Alegre'
    END AS cidade,
    CASE (x % 5) 
        WHEN 0 THEN 'SP'
        WHEN 1 THEN 'RJ'
        WHEN 2 THEN 'MG'
        WHEN 3 THEN 'PR'
        ELSE 'RS'
    END AS estado
FROM cnt;

-- Verificar quantidade
SELECT COUNT(*) AS total FROM clientes;

-- ============================================
-- CONSULTA SEM ÍNDICE
-- ============================================

-- Ver plano de execução (sem índice em cidade)
EXPLAIN QUERY PLAN SELECT * FROM clientes WHERE cidade = 'São Paulo';
    -- SCAN clientes (busca completa na tabela)

-- Executar consulta (note o tempo)
SELECT COUNT(*) FROM clientes WHERE cidade = 'São Paulo';

-- ============================================
-- CRIAR ÍNDICES
-- ============================================

-- Índice simples em uma coluna
CREATE INDEX idx_clientes_cidade ON clientes(cidade);

-- Ver plano de execução (com índice)
EXPLAIN QUERY PLAN SELECT * FROM clientes WHERE cidade = 'São Paulo';
    -- SEARCH clientes USING INDEX idx_clientes_cidade (usa índice)

-- Executar mesma consulta (compare o tempo)
SELECT COUNT(*) FROM clientes WHERE cidade = 'São Paulo';

-- Índice composto (múltiplas colunas)
CREATE INDEX idx_clientes_estado_cidade ON clientes(estado, cidade);

-- Índice com ordenação
CREATE INDEX idx_clientes_nome ON clientes(nome COLLATE NOCASE);

-- Índice parcial (apenas registros ativos)
CREATE INDEX idx_clientes_ativos ON clientes(email) WHERE ativo = 1;

-- Índice único (já existe no cpf por UNIQUE)
-- CREATE UNIQUE INDEX idx_clientes_cpf ON clientes(cpf);

-- ============================================
-- GERENCIAR ÍNDICES
-- ============================================

-- Listar todos os índices
.indices clientes

-- Ver estrutura de índice específico
PRAGMA index_info(idx_clientes_cidade);

-- Ver lista de índices com mais detalhes
PRAGMA index_list(clientes);

-- Ver SQL de criação do índice
SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = 'clientes';

-- Analisar estatísticas (otimiza planos de execução)
ANALYZE clientes;

-- Ver estatísticas
SELECT * FROM sqlite_stat1 WHERE tbl = 'clientes';

-- ============================================
-- REMOVER ÍNDICES
-- ============================================

-- Remover índice
DROP INDEX IF EXISTS idx_clientes_cidade;
DROP INDEX IF EXISTS idx_clientes_estado_cidade;
DROP INDEX IF EXISTS idx_clientes_nome;
DROP INDEX IF EXISTS idx_clientes_ativos;

-- Reindexar tabela
REINDEX clientes;

-- Destruir tabela
DROP TABLE clientes;

-- Forcar salvamento
.save /var/lib/sqlite/indices.db

7 – Views

Views são tabelas virtuais construídas em tempo de execução para produzir registros em um formato especifico, extraindo dados de outras tabelas:

SQLITE3 Shell
.open /var/lib/sqlite/views.db
.headers on
.mode box

-- Criar tabelas base
CREATE TABLE funcionarios (
    id INTEGER PRIMARY KEY,
    nome TEXT NOT NULL,
    cargo TEXT,
    departamento TEXT,
    salario REAL,
    data_admissao TEXT,
    ativo INTEGER DEFAULT 1
);

CREATE TABLE departamentos (
    id INTEGER PRIMARY KEY,
    nome TEXT UNIQUE NOT NULL,
    orcamento REAL
);

-- Inserir dados
INSERT INTO departamentos VALUES (1, 'TI', 500000);
INSERT INTO departamentos VALUES (2, 'RH', 200000);
INSERT INTO departamentos VALUES (3, 'Vendas', 300000);

INSERT INTO funcionarios VALUES (1, 'João Silva', 'Desenvolvedor', 'TI', 8000, '2020-01-15', 1);
INSERT INTO funcionarios VALUES (2, 'Maria Santos', 'Gerente', 'RH', 12000, '2018-06-01', 1);
INSERT INTO funcionarios VALUES (3, 'Pedro Costa', 'Vendedor', 'Vendas', 5000, '2021-03-10', 1);
INSERT INTO funcionarios VALUES (4, 'Ana Oliveira', 'Analista', 'TI', 6500, '2022-08-20', 1);
INSERT INTO funcionarios VALUES (5, 'Carlos Lima', 'Estagiário', 'TI', 2000, '2023-01-05', 0);

-- ============================================
-- CRIAR VIEWS
-- ============================================

-- View simples - funcionários ativos
CREATE VIEW vw_funcionarios_ativos AS
SELECT id, nome, cargo, departamento, salario
FROM funcionarios
WHERE ativo = 1;

-- Usar a view como tabela
SELECT * FROM vw_funcionarios_ativos;

-- View com cálculos
CREATE VIEW vw_folha_pagamento AS
SELECT 
    f.nome,
    f.cargo,
    f.departamento,
    f.salario,
    f.salario * 0.11 AS inss,
    f.salario * 0.275 AS irrf_estimado,
    f.salario - (f.salario * 0.11) AS salario_liquido_est
FROM funcionarios f
WHERE f.ativo = 1;

SELECT * FROM vw_folha_pagamento;

-- View com agregação - resumo por departamento
CREATE VIEW vw_resumo_departamento AS
SELECT 
    departamento,
    COUNT(*) AS total_funcionarios,
    printf('R$ %.2f', SUM(salario)) AS total_salarios,
    printf('R$ %.2f', AVG(salario)) AS media_salarios,
    printf('R$ %.2f', MIN(salario)) AS menor_salario,
    printf('R$ %.2f', MAX(salario)) AS maior_salario
FROM funcionarios
WHERE ativo = 1
GROUP BY departamento;

SELECT * FROM vw_resumo_departamento;

-- View com JOIN
CREATE VIEW vw_funcionarios_completo AS
SELECT 
    f.id,
    f.nome,
    f.cargo,
    f.departamento,
    d.orcamento AS orcamento_depto,
    printf('R$ %.2f', f.salario) AS salario,
    date(f.data_admissao) AS admissao,
    CAST((julianday('now') - julianday(f.data_admissao)) / 365 AS INTEGER) AS anos_empresa,
    CASE f.ativo WHEN 1 THEN 'Ativo' ELSE 'Inativo' END AS status
FROM funcionarios f
LEFT JOIN departamentos d ON f.departamento = d.nome;

SELECT * FROM vw_funcionarios_completo;

-- ============================================
-- GERENCIAR VIEWS
-- ============================================

-- Listar views
SELECT name, sql FROM sqlite_master WHERE type = 'view';

-- Ver definição de view específica
SELECT sql FROM sqlite_master WHERE type = 'view' AND name = 'vw_funcionarios_ativos';

-- Recriar view (alterar)
DROP VIEW IF EXISTS vw_funcionarios_ativos;
CREATE VIEW vw_funcionarios_ativos AS
SELECT id, nome, cargo, departamento, printf('R$ %.2f', salario) AS salario
FROM funcionarios
WHERE ativo = 1
ORDER BY nome;

-- Remover view
DROP VIEW IF EXISTS vw_folha_pagamento;

-- ============================================
-- VIEWS TEMPORÁRIAS
-- ============================================

-- View temporária (existe apenas na sessão)
CREATE TEMP VIEW vw_temp_ti AS
SELECT * FROM funcionarios WHERE departamento = 'TI';

SELECT * FROM vw_temp_ti;

-- Limpar
DROP VIEW IF EXISTS vw_funcionarios_ativos;
DROP VIEW IF EXISTS vw_resumo_departamento;
DROP VIEW IF EXISTS vw_funcionarios_completo;
DROP TABLE funcionarios;
DROP TABLE departamentos;

7 – Triggers

Triggers executam código automaticamente em resposta a eventos (INSERT, UPDATE, DELETE):

SQLITE3 Shell
.open /var/lib/sqlite/triggers.db
.headers on
.mode box

-- Criar tabelas
CREATE TABLE produtos (
    id INTEGER PRIMARY KEY,
    nome TEXT NOT NULL,
    preco REAL NOT NULL,
    estoque INTEGER DEFAULT 0,
    atualizado_em TEXT
);

CREATE TABLE produtos_log (
    id INTEGER PRIMARY KEY,
    produto_id INTEGER,
    acao TEXT,
    dados_antigos TEXT,
    dados_novos TEXT,
    usuario TEXT DEFAULT 'sistema',
    data_hora TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE estoque_alertas (
    id INTEGER PRIMARY KEY,
    produto_id INTEGER,
    produto_nome TEXT,
    estoque_atual INTEGER,
    mensagem TEXT,
    data_hora TEXT DEFAULT CURRENT_TIMESTAMP
);

-- ============================================
-- TRIGGERS DE INSERT
-- ============================================

-- Trigger AFTER INSERT - registrar log
CREATE TRIGGER trg_produtos_insert_log
AFTER INSERT ON produtos
BEGIN
    INSERT INTO produtos_log (produto_id, acao, dados_novos)
    VALUES (
        NEW.id, 
        'INSERT', 
        json_object('nome', NEW.nome, 'preco', NEW.preco, 'estoque', NEW.estoque)
    );
END;

-- Trigger AFTER INSERT - alerta de estoque baixo
CREATE TRIGGER trg_produtos_estoque_baixo
AFTER INSERT ON produtos
WHEN NEW.estoque < 10
BEGIN
    INSERT INTO estoque_alertas (produto_id, produto_nome, estoque_atual, mensagem)
    VALUES (NEW.id, NEW.nome, NEW.estoque, 'Estoque baixo ao cadastrar produto!');
END;

-- Testar INSERT
INSERT INTO produtos (nome, preco, estoque) VALUES ('Notebook', 4500.00, 5);
INSERT INTO produtos (nome, preco, estoque) VALUES ('Mouse', 89.90, 100);

SELECT * FROM produtos;
SELECT * FROM produtos_log;
SELECT * FROM estoque_alertas;

-- ============================================
-- TRIGGERS DE UPDATE
-- ============================================

-- Trigger BEFORE UPDATE - atualizar timestamp
CREATE TRIGGER trg_produtos_update_timestamp
BEFORE UPDATE ON produtos
BEGIN
    UPDATE produtos SET atualizado_em = datetime('now') WHERE id = NEW.id;
END;

-- Trigger AFTER UPDATE - registrar log
CREATE TRIGGER trg_produtos_update_log
AFTER UPDATE ON produtos
BEGIN
    INSERT INTO produtos_log (produto_id, acao, dados_antigos, dados_novos)
    VALUES (
        NEW.id,
        'UPDATE',
        json_object('nome', OLD.nome, 'preco', OLD.preco, 'estoque', OLD.estoque),
        json_object('nome', NEW.nome, 'preco', NEW.preco, 'estoque', NEW.estoque)
    );
END;

-- Trigger AFTER UPDATE - alerta quando estoque diminui
CREATE TRIGGER trg_produtos_estoque_update
AFTER UPDATE OF estoque ON produtos
WHEN NEW.estoque < 10 AND OLD.estoque >= 10
BEGIN
    INSERT INTO estoque_alertas (produto_id, produto_nome, estoque_atual, mensagem)
    VALUES (NEW.id, NEW.nome, NEW.estoque, 'Estoque ficou abaixo do mínimo!');
END;

-- Testar UPDATE
UPDATE produtos SET preco = 4299.00 WHERE id = 1;
UPDATE produtos SET estoque = 8 WHERE id = 2;

SELECT * FROM produtos;
SELECT * FROM produtos_log;
SELECT * FROM estoque_alertas;

-- ============================================
-- TRIGGERS DE DELETE
-- ============================================

-- Trigger BEFORE DELETE - registrar log
CREATE TRIGGER trg_produtos_delete_log
BEFORE DELETE ON produtos
BEGIN
    INSERT INTO produtos_log (produto_id, acao, dados_antigos)
    VALUES (
        OLD.id,
        'DELETE',
        json_object('nome', OLD.nome, 'preco', OLD.preco, 'estoque', OLD.estoque)
    );
END;

-- Testar DELETE
DELETE FROM produtos WHERE id = 1;

SELECT * FROM produtos;
SELECT * FROM produtos_log;

-- ============================================
-- TRIGGER INSTEAD OF (para VIEWS)
-- ============================================

-- Criar view
CREATE VIEW vw_produtos_ativos AS
SELECT id, nome, preco, estoque FROM produtos WHERE estoque > 0;

-- Trigger para permitir INSERT via VIEW
CREATE TRIGGER trg_vw_produtos_insert
INSTEAD OF INSERT ON vw_produtos_ativos
BEGIN
    INSERT INTO produtos (nome, preco, estoque)
    VALUES (NEW.nome, NEW.preco, NEW.estoque);
END;

-- Inserir via view
INSERT INTO vw_produtos_ativos (nome, preco, estoque) VALUES ('Teclado', 199.90, 50);

SELECT * FROM produtos;
SELECT * FROM vw_produtos_ativos;

-- ============================================
-- GERENCIAR TRIGGERS
-- ============================================

-- Listar triggers
SELECT name, sql FROM sqlite_master WHERE type = 'trigger';

-- Ver trigger específico
SELECT sql FROM sqlite_master WHERE type = 'trigger' AND name = 'trg_produtos_insert_log';

-- Remover trigger
DROP TRIGGER IF EXISTS trg_produtos_insert_log;
DROP TRIGGER IF EXISTS trg_produtos_estoque_baixo;

-- Limpar tudo
DROP VIEW IF EXISTS vw_produtos_ativos;
DROP TABLE IF EXISTS produtos;
DROP TABLE IF EXISTS produtos_log;
DROP TABLE IF EXISTS estoque_alertas;

8 – Transações

Transações garantem integridade dos dados (ACID):

SQLITE3 Shell
.open /var/lib/sqlite/triggers.db
.headers on
.mode box

-- Criar tabelas
CREATE TABLE contas (
    id INTEGER PRIMARY KEY,
    titular TEXT NOT NULL,
    saldo REAL DEFAULT 0 CHECK (saldo >= 0)
);

CREATE TABLE transferencias (
    id INTEGER PRIMARY KEY,
    conta_origem INTEGER,
    conta_destino INTEGER,
    valor REAL,
    data_hora TEXT DEFAULT CURRENT_TIMESTAMP,
    status TEXT
);

-- Dados iniciais
INSERT INTO contas (titular, saldo) VALUES ('João', 1000.00);
INSERT INTO contas (titular, saldo) VALUES ('Maria', 500.00);
INSERT INTO contas (titular, saldo) VALUES ('Pedro', 200.00);

SELECT * FROM contas;

-- ============================================
-- TRANSAÇÃO BÁSICA - COMMIT
-- ============================================

-- Iniciar transação
BEGIN TRANSACTION;

-- Transferir R$ 100 de João para Maria
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
UPDATE contas SET saldo = saldo + 100 WHERE id = 2;

-- Registrar transferência
INSERT INTO transferencias (conta_origem, conta_destino, valor, status)
VALUES (1, 2, 100.00, 'concluida');

-- Verificar antes de confirmar
SELECT * FROM contas;

-- Confirmar transação
COMMIT;

SELECT * FROM contas;
SELECT * FROM transferencias;

-- ============================================
-- TRANSAÇÃO COM ROLLBACK
-- ============================================

-- Verificar saldo atual
SELECT * FROM contas;

-- Iniciar transação
BEGIN TRANSACTION;

-- Tentar transferir R$ 1000 de Pedro (só tem R$ 200)
UPDATE contas SET saldo = saldo - 1000 WHERE id = 3;
-- ERRO: CHECK constraint failed (saldo >= 0)

-- Se houve erro, desfazer
ROLLBACK;

-- Saldos permanecem inalterados
SELECT * FROM contas;

-- ============================================
-- TRANSAÇÃO COM SAVEPOINT
-- ============================================

BEGIN TRANSACTION;

-- Primeira operação
UPDATE contas SET saldo = saldo + 50 WHERE id = 1;
SAVEPOINT ponto1;

-- Segunda operação
UPDATE contas SET saldo = saldo + 100 WHERE id = 2;
SAVEPOINT ponto2;

-- Terceira operação (queremos desfazer)
UPDATE contas SET saldo = saldo + 200 WHERE id = 3;

-- Ver estado atual
SELECT * FROM contas;

-- Voltar ao ponto2 (desfaz terceira operação)
ROLLBACK TO ponto2;

-- Ver estado após rollback parcial
SELECT * FROM contas;

-- Confirmar transação
COMMIT;

SELECT * FROM contas;

-- ============================================
-- MODOS DE TRANSAÇÃO
-- ============================================

-- BEGIN DEFERRED (padrão) - lock obtido quando necessário
BEGIN DEFERRED TRANSACTION;
SELECT * FROM contas;
COMMIT;

-- BEGIN IMMEDIATE - lock de escrita imediato
BEGIN IMMEDIATE TRANSACTION;
UPDATE contas SET saldo = saldo + 1 WHERE id = 1;
COMMIT;

-- BEGIN EXCLUSIVE - lock exclusivo (bloqueia outras conexões)
BEGIN EXCLUSIVE TRANSACTION;
UPDATE contas SET saldo = saldo + 1 WHERE id = 1;
COMMIT;

-- ============================================
-- TRANSAÇÃO AUTOMÁTICA (Autocommit)
-- ============================================

-- Por padrão, cada comando é uma transação automática
-- Estes comandos são equivalentes:

UPDATE contas SET saldo = 1000 WHERE id = 1;

-- É igual a:
BEGIN;
UPDATE contas SET saldo = 1000 WHERE id = 1;
COMMIT;

-- Limpar
DROP TABLE contas;
DROP TABLE transferencias;

9 – Funções SQL Úteis

SQLITE3 Shell
.open /var/lib/sqlite/samples01.db

.headers on
.mode box

-- ============================================
-- FUNÇÕES DE AGREGAÇÃO
-- ============================================

CREATE TEMP TABLE vendas (
    id INTEGER PRIMARY KEY,
    produto TEXT,
    quantidade INTEGER,
    valor REAL,
    data TEXT
);

INSERT INTO vendas VALUES 
    (1, 'A', 10, 100.00, '2024-01-15'),
    (2, 'B', 5, 50.00, '2024-01-16'),
    (3, 'A', 8, 80.00, '2024-01-17'),
    (4, 'C', 15, 150.00, '2024-01-18'),
    (5, 'B', 3, 30.00, '2024-01-19');

SELECT 
    COUNT(*) AS total_vendas,
    COUNT(DISTINCT produto) AS produtos_unicos,
    SUM(quantidade) AS qtd_total,
    SUM(valor) AS valor_total,
    AVG(valor) AS media_valor,
    MIN(valor) AS menor_venda,
    MAX(valor) AS maior_venda,
    GROUP_CONCAT(produto, ', ') AS produtos
FROM vendas;

-- Agregação por grupo
SELECT 
    produto,
    COUNT(*) AS vendas,
    SUM(quantidade) AS qtd,
    printf('R$ %.2f', SUM(valor)) AS total
FROM vendas
GROUP BY produto
HAVING SUM(valor) > 50;

-- ============================================
-- FUNÇÕES DE TEXTO
-- ============================================

SELECT 
    'texto' AS original,
    LENGTH('texto') AS tamanho,
    UPPER('texto') AS maiusculo,
    LOWER('TEXTO') AS minusculo,
    SUBSTR('texto', 2, 3) AS substring,
    REPLACE('texto', 'e', 'a') AS substituido,
    TRIM('  texto  ') AS sem_espacos,
    LTRIM('  texto') AS sem_espacos_esq,
    RTRIM('texto  ') AS sem_espacos_dir,
    INSTR('texto', 'x') AS posicao_x,
    'pre' || 'fixo' AS concatenado,
    PRINTF('%05d', 42) AS formatado,
    QUOTE('texto com ''aspas''') AS com_escape;

-- ============================================
-- FUNÇÕES NUMÉRICAS
-- ============================================

SELECT 
    ABS(-10) AS absoluto,
    ROUND(3.14159, 2) AS arredondado,
    ROUND(3.5) AS arred_inteiro,
    MAX(1, 2, 3) AS maximo,
    MIN(1, 2, 3) AS minimo,
    RANDOM() AS aleatorio,
    ABS(RANDOM() % 100) AS aleatorio_0_99,
    CAST(3.7 AS INTEGER) AS para_inteiro,
    CAST(42 AS REAL) AS para_real,
    TYPEOF(42) AS tipo_int,
    TYPEOF(3.14) AS tipo_real,
    TYPEOF('abc') AS tipo_text;

-- ============================================
-- FUNÇÕES DE DATA/HORA
-- ============================================

SELECT 
    DATE('now') AS hoje,
    TIME('now') AS hora,
    DATETIME('now') AS data_hora,
    DATETIME('now', 'localtime') AS data_hora_local,
    DATE('now', '+7 days') AS daqui_7_dias,
    DATE('now', '-1 month') AS mes_passado,
    DATE('now', 'start of month') AS inicio_mes,
    DATE('now', 'start of year') AS inicio_ano,
    DATE('now', '+1 month', 'start of month', '-1 day') AS fim_mes,
    STRFTIME('%d/%m/%Y', 'now') AS formato_br,
    STRFTIME('%H:%M:%S', 'now') AS formato_hora,
    STRFTIME('%Y', 'now') AS ano,
    STRFTIME('%m', 'now') AS mes,
    STRFTIME('%d', 'now') AS dia,
    STRFTIME('%W', 'now') AS semana_ano,
    STRFTIME('%w', 'now') AS dia_semana,
    STRFTIME('%s', 'now') AS unix_timestamp,
    JULIANDAY('now') AS dia_juliano;

-- Diferença entre datas
SELECT 
    CAST(JULIANDAY('2024-12-31') - JULIANDAY('2024-01-01') AS INTEGER) AS dias_no_ano,
    CAST((JULIANDAY('now') - JULIANDAY('2024-01-01')) AS INTEGER) AS dias_desde_jan;

-- ============================================
-- FUNÇÕES CONDICIONAIS
-- ============================================

SELECT 
    COALESCE(NULL, 'valor_padrao') AS coalesce_ex,
    IFNULL(NULL, 'se_null') AS ifnull_ex,
    NULLIF(1, 1) AS nullif_igual,
    NULLIF(1, 2) AS nullif_diferente,
    IIF(1 > 0, 'verdadeiro', 'falso') AS iif_ex,
    CASE 
        WHEN 10 > 5 THEN 'maior'
        WHEN 10 < 5 THEN 'menor'
        ELSE 'igual'
    END AS case_ex;

-- CASE com valores
SELECT 
    produto,
    valor,
    CASE 
        WHEN valor >= 100 THEN 'Alto'
        WHEN valor >= 50 THEN 'Médio'
        ELSE 'Baixo'
    END AS categoria
FROM vendas;

-- ============================================
-- FUNÇÕES JSON (SQLite 3.38+)
-- ============================================

SELECT 
    JSON('{"nome":"João","idade":30}') AS json_valido,
    JSON_EXTRACT('{"nome":"João","idade":30}', '$.nome') AS nome,
    JSON_EXTRACT('{"nome":"João","idade":30}', '$.idade') AS idade,
    JSON_TYPE('{"nome":"João"}', '$.nome') AS tipo_nome,
    JSON_ARRAY(1, 2, 3) AS array_json,
    JSON_OBJECT('a', 1, 'b', 2) AS objeto_json,
    JSON_INSERT('{"a":1}', '$.b', 2) AS json_inserido,
    JSON_REPLACE('{"a":1}', '$.a', 99) AS json_substituido,
    JSON_SET('{"a":1}', '$.b', 2) AS json_setado,
    JSON_REMOVE('{"a":1,"b":2}', '$.b') AS json_removido;

-- JSON com arrays
SELECT 
    JSON_ARRAY_LENGTH('[1,2,3,4,5]') AS tamanho,
    JSON_EXTRACT('[1,2,3,4,5]', '$[0]') AS primeiro,
    JSON_EXTRACT('[1,2,3,4,5]', '$[#-1]') AS ultimo;

DROP TABLE vendas;

10 – Consultas avançadas

Mais exemplos:

SQLITE3 Shell
.open /var/lib/sqlite/consultas.db

.headers on
.mode box

-- Criar tabelas de exemplo
CREATE TABLE clientes (
    id INTEGER PRIMARY KEY,
    nome TEXT,
    cidade TEXT
);

CREATE TABLE pedidos (
    id INTEGER PRIMARY KEY,
    cliente_id INTEGER,
    valor REAL,
    status TEXT,
    data TEXT
);

-- Inserir dados
INSERT INTO clientes VALUES 
    (1, 'João', 'São Paulo'),
    (2, 'Maria', 'Rio de Janeiro'),
    (3, 'Pedro', 'São Paulo'),
    (4, 'Ana', 'Curitiba');

INSERT INTO pedidos VALUES 
    (1, 1, 100.00, 'entregue', '2024-01-10'),
    (2, 1, 200.00, 'entregue', '2024-01-15'),
    (3, 2, 150.00, 'pendente', '2024-01-20'),
    (4, 2, 300.00, 'entregue', '2024-02-01'),
    (5, 3, 50.00, 'cancelado', '2024-02-10'),
    (6, 1, 175.00, 'pendente', '2024-02-15');

-- ============================================
-- JOINS
-- ============================================

-- INNER JOIN
SELECT c.nome, p.valor, p.status
FROM clientes c
INNER JOIN pedidos p ON c.id = p.cliente_id;

-- LEFT JOIN (todos os clientes, mesmo sem pedidos)
SELECT c.nome, COALESCE(SUM(p.valor), 0) AS total_compras
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
GROUP BY c.id;

-- ============================================
-- SUBQUERIES
-- ============================================

-- Subquery no WHERE
SELECT * FROM clientes
WHERE id IN (SELECT DISTINCT cliente_id FROM pedidos WHERE status = 'entregue');

-- Subquery escalar
SELECT 
    nome,
    (SELECT COUNT(*) FROM pedidos WHERE cliente_id = clientes.id) AS qtd_pedidos
FROM clientes;

-- Subquery no FROM
SELECT cidade, total_clientes
FROM (
    SELECT cidade, COUNT(*) AS total_clientes
    FROM clientes
    GROUP BY cidade
) AS resumo
WHERE total_clientes >= 1;

-- ============================================
-- CTE (Common Table Expressions)
-- ============================================

-- CTE simples
WITH pedidos_entregues AS (
    SELECT * FROM pedidos WHERE status = 'entregue'
)
SELECT c.nome, SUM(pe.valor) AS total_entregue
FROM clientes c
JOIN pedidos_entregues pe ON c.id = pe.cliente_id
GROUP BY c.id;

-- CTE múltiplas
WITH 
resumo_cliente AS (
    SELECT 
        cliente_id,
        COUNT(*) AS qtd_pedidos,
        SUM(valor) AS total
    FROM pedidos
    GROUP BY cliente_id
),
media_geral AS (
    SELECT AVG(total) AS media FROM resumo_cliente
)
SELECT 
    c.nome,
    rc.qtd_pedidos,
    printf('R$ %.2f', rc.total) AS total,
    CASE WHEN rc.total > mg.media THEN 'Acima' ELSE 'Abaixo' END AS comparativo
FROM clientes c
JOIN resumo_cliente rc ON c.id = rc.cliente_id
CROSS JOIN media_geral mg;

-- CTE recursiva (hierarquia)
CREATE TABLE categorias (
    id INTEGER PRIMARY KEY,
    nome TEXT,
    pai_id INTEGER
);

INSERT INTO categorias VALUES 
    (1, 'Eletrônicos', NULL),
    (2, 'Computadores', 1),
    (3, 'Notebooks', 2),
    (4, 'Desktops', 2),
    (5, 'Celulares', 1),
    (6, 'Smartphones', 5);

WITH RECURSIVE arvore AS (
    -- Base: categorias raiz
    SELECT id, nome, pai_id, 0 AS nivel, nome AS caminho
    FROM categorias
    WHERE pai_id IS NULL
    
    UNION ALL
    
    -- Recursivo: filhos
    SELECT c.id, c.nome, c.pai_id, a.nivel + 1, a.caminho || ' > ' || c.nome
    FROM categorias c
    JOIN arvore a ON c.pai_id = a.id
)
SELECT 
    SUBSTR('        ', 1, nivel * 2) || nome AS categoria,
    caminho
FROM arvore
ORDER BY caminho;

-- ============================================
-- WINDOW FUNCTIONS (SQLite 3.25+)
-- ============================================

-- ROW_NUMBER, RANK, DENSE_RANK
SELECT 
    cliente_id,
    valor,
    ROW_NUMBER() OVER (ORDER BY valor DESC) AS posicao,
    RANK() OVER (ORDER BY valor DESC) AS ranking,
    DENSE_RANK() OVER (ORDER BY valor DESC) AS ranking_denso
FROM pedidos;

-- Particionado por cliente
SELECT 
    cliente_id,
    valor,
    ROW_NUMBER() OVER (PARTITION BY cliente_id ORDER BY valor DESC) AS pos_cliente,
    SUM(valor) OVER (PARTITION BY cliente_id) AS total_cliente
FROM pedidos;

-- Running total (soma acumulada)
SELECT 
    data,
    valor,
    SUM(valor) OVER (ORDER BY data) AS acumulado,
    AVG(valor) OVER (ORDER BY data ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS media_movel_3
FROM pedidos
ORDER BY data;

-- LEAD e LAG (próximo e anterior)
SELECT 
    data,
    valor,
    LAG(valor, 1) OVER (ORDER BY data) AS valor_anterior,
    LEAD(valor, 1) OVER (ORDER BY data) AS valor_proximo,
    valor - LAG(valor, 1) OVER (ORDER BY data) AS diferenca
FROM pedidos
ORDER BY data;

-- FIRST_VALUE e LAST_VALUE
SELECT 
    cliente_id,
    data,
    valor,
    FIRST_VALUE(valor) OVER (
        PARTITION BY cliente_id ORDER BY data
    ) AS primeiro_valor,
    LAST_VALUE(valor) OVER (
        PARTITION BY cliente_id ORDER BY data
        RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS ultimo_valor
FROM pedidos;

-- ============================================
-- UPSERT (INSERT OR REPLACE)
-- ============================================

CREATE TABLE config (
    chave TEXT PRIMARY KEY,
    valor TEXT,
    atualizado TEXT
);

-- Insert or Replace (substitui se existir)
INSERT OR REPLACE INTO config VALUES ('tema', 'escuro', datetime('now'));
INSERT OR REPLACE INTO config VALUES ('idioma', 'pt-BR', datetime('now'));
INSERT OR REPLACE INTO config VALUES ('tema', 'claro', datetime('now'));

SELECT * FROM config;

-- Insert or Ignore (ignora se existir)
INSERT OR IGNORE INTO config VALUES ('tema', 'azul', datetime('now'));

SELECT * FROM config;

-- UPSERT com ON CONFLICT (SQLite 3.24+)
INSERT INTO config (chave, valor, atualizado) VALUES ('versao', '1.0', datetime('now'))
ON CONFLICT(chave) DO UPDATE SET 
    valor = excluded.valor,
    atualizado = excluded.atualizado;

SELECT * FROM config;

-- Limpar
DROP TABLE clientes;
DROP TABLE pedidos;
DROP TABLE categorias;
DROP TABLE config;

11 – Importação e Exportação de Dados

Manipulando dados para dentro e para fora do SQLITE:

SQLITE3 Shell
.open /var/lib/sqlite/importacao.db

-- ============================================
-- EXPORTAR DADOS
-- ============================================

-- Criar tabela de exemplo
CREATE TABLE usuarios (
    id INTEGER PRIMARY KEY,
    nome TEXT,
    email TEXT,
    cidade TEXT
);

INSERT INTO usuarios VALUES 
    (1, 'João Silva', 'joao@email.com', 'São Paulo'),
    (2, 'Maria Santos', 'maria@email.com', 'Rio de Janeiro'),
    (3, 'Pedro Costa', 'pedro@email.com', 'Belo Horizonte');

-- Exportar para CSV
.headers on
.mode csv
.output usuarios.csv
SELECT * FROM usuarios;
.output stdout

-- Exportar para JSON (linha por linha)
.mode json
.output usuarios.json
SELECT * FROM usuarios;
.output stdout

-- Exportar como SQL (dump)
.output usuarios.sql
.dump usuarios
.output stdout

-- Exportar apenas dados (INSERT)
.mode insert usuarios
.output usuarios_insert.sql
SELECT * FROM usuarios;
.output stdout

-- Ver arquivos criados
.shell ls -la *.csv *.json *.sql

-- ============================================
-- IMPORTAR DADOS
-- ============================================

-- Criar tabela para importação
CREATE TABLE usuarios_import (
    id INTEGER PRIMARY KEY,
    nome TEXT,
    email TEXT,
    cidade TEXT
);

-- Importar CSV
.mode csv
.import usuarios.csv usuarios_import

-- Verificar importação
.mode box
.headers on
SELECT * FROM usuarios_import;

-- Se CSV tiver header, pular primeira linha
DELETE FROM usuarios_import;
.import --skip 1 usuarios.csv usuarios_import
SELECT * FROM usuarios_import;

-- ============================================
-- BACKUP E RESTORE
-- ============================================

-- Backup do banco inteiro
.backup backup_completo.db

-- Dump SQL completo
.output dump_completo.sql
.dump
.output stdout

-- Restore de dump
.read dump_completo.sql

-- ============================================
-- ATTACH E COPIAR ENTRE BANCOS
-- ============================================

-- Criar segundo banco
ATTACH DATABASE 'outro_banco.db' AS outro;

-- Copiar tabela para outro banco
CREATE TABLE outro.usuarios AS SELECT * FROM usuarios;

-- Verificar
SELECT * FROM outro.usuarios;

-- Desanexar
DETACH DATABASE outro;

-- Limpar arquivos
.shell rm -f usuarios.csv usuarios.json usuarios.sql usuarios_insert.sql
.shell rm -f backup_completo.db dump_completo.sql outro_banco.db

DROP TABLE usuarios;
DROP TABLE usuarios_import;

12 – Performance e Otimização

SQLITE3 Shell
.open /var/lib/sqlite/performance.db
.headers on
.mode box

-- ============================================
-- CONFIGURAÇÕES DE PERFORMANCE
-- ============================================

-- Modo WAL (Write-Ahead Logging) - recomendado para concorrência
PRAGMA journal_mode = WAL;

-- Cache em memória (páginas, negativo = KB)
PRAGMA cache_size = -64000;  -- 64MB

-- Armazenar temporários em memória
PRAGMA temp_store = MEMORY;

-- Memory-mapped I/O
PRAGMA mmap_size = 268435456;  -- 256MB

-- Modo de sincronização
PRAGMA synchronous = NORMAL;  -- Equilíbrio entre segurança e velocidade

-- Verificar configurações
PRAGMA journal_mode;
PRAGMA cache_size;
PRAGMA temp_store;
PRAGMA mmap_size;
PRAGMA synchronous;

-- ============================================
-- ANALISAR QUERIES
-- ============================================

-- Criar tabela grande para testes
CREATE TABLE IF NOT EXISTS logs (
    id INTEGER PRIMARY KEY,
    nivel TEXT,
    mensagem TEXT,
    data_hora TEXT,
    origem TEXT
);

-- Inserir 100.000 registros
WITH RECURSIVE cnt(x) AS (
    SELECT 1
    UNION ALL
    SELECT x + 1 FROM cnt WHERE x < 100000
)
INSERT INTO logs (nivel, mensagem, data_hora, origem)
SELECT 
    CASE (x % 4) WHEN 0 THEN 'INFO' WHEN 1 THEN 'WARN' WHEN 2 THEN 'ERROR' ELSE 'DEBUG' END,
    'Mensagem de log número ' || x,
    datetime('2024-01-01', '+' || (x % 365) || ' days', '+' || (x % 24) || ' hours'),
    CASE (x % 3) WHEN 0 THEN 'api' WHEN 1 THEN 'web' ELSE 'batch' END
FROM cnt;

SELECT COUNT(*) AS total FROM logs;

-- Ver plano de execução
.timer on
EXPLAIN QUERY PLAN SELECT * FROM logs WHERE nivel = 'ERROR';
-- Resultado: SCAN logs (varredura completa)

-- Criar índice
CREATE INDEX idx_logs_nivel ON logs(nivel);

-- Ver plano novamente
EXPLAIN QUERY PLAN SELECT * FROM logs WHERE nivel = 'ERROR';
-- Resultado: SEARCH logs USING INDEX (usa índice)

-- Comparar tempo de execução
SELECT COUNT(*) FROM logs WHERE nivel = 'ERROR';

-- Analisar estatísticas
ANALYZE logs;

-- Ver estatísticas
SELECT * FROM sqlite_stat1 WHERE tbl = 'logs';

-- ============================================
-- EXPLAIN DETALHADO
-- ============================================

-- EXPLAIN mostra bytecode VM
EXPLAIN SELECT * FROM logs WHERE nivel = 'ERROR' LIMIT 5;

-- EXPLAIN QUERY PLAN é mais legível
EXPLAIN QUERY PLAN 
SELECT l1.nivel, COUNT(*) 
FROM logs l1
JOIN logs l2 ON l1.data_hora = l2.data_hora
GROUP BY l1.nivel;

-- ============================================
-- VACUUM - OTIMIZAÇÃO DE ESPAÇO
-- ============================================

-- Ver tamanho do banco
SELECT page_count * page_size AS tamanho_bytes FROM pragma_page_count(), pragma_page_size();

-- Deletar metade dos registros
DELETE FROM logs WHERE id % 2 = 0;

-- Ver páginas livres
PRAGMA freelist_count;

-- Compactar banco (recupera espaço)
VACUUM;

-- Verificar novamente
PRAGMA freelist_count;
SELECT page_count * page_size AS tamanho_bytes FROM pragma_page_count(), pragma_page_size();

-- ============================================
-- OTIMIZAÇÃO AUTOMÁTICA
-- ============================================

-- Rodar otimização (analisa e reorganiza)
PRAGMA optimize;

-- Verificar integridade
PRAGMA integrity_check;

-- Limpar
DROP TABLE logs;
.timer off

13 – Extensões e Funcionalidades Especiais

SQLITE3 Shell
.open /var/lib/sqlite/extensoes.db

.headers on
.mode box

-- ============================================
-- FULL-TEXT SEARCH (FTS5)
-- ============================================

-- Criar tabela FTS5
CREATE VIRTUAL TABLE artigos USING fts5(
    titulo,
    conteudo,
    autor,
    content='artigos_base',
    content_rowid='id'
);

-- Tabela base
CREATE TABLE artigos_base (
    id INTEGER PRIMARY KEY,
    titulo TEXT,
    conteudo TEXT,
    autor TEXT
);

-- Triggers para sincronizar
CREATE TRIGGER artigos_ai AFTER INSERT ON artigos_base BEGIN
    INSERT INTO artigos(rowid, titulo, conteudo, autor) 
    VALUES (new.id, new.titulo, new.conteudo, new.autor);
END;

CREATE TRIGGER artigos_ad AFTER DELETE ON artigos_base BEGIN
    INSERT INTO artigos(artigos, rowid, titulo, conteudo, autor) 
    VALUES ('delete', old.id, old.titulo, old.conteudo, old.autor);
END;

CREATE TRIGGER artigos_au AFTER UPDATE ON artigos_base BEGIN
    INSERT INTO artigos(artigos, rowid, titulo, conteudo, autor) 
    VALUES ('delete', old.id, old.titulo, old.conteudo, old.autor);
    INSERT INTO artigos(rowid, titulo, conteudo, autor) 
    VALUES (new.id, new.titulo, new.conteudo, new.autor);
END;

-- Inserir dados
INSERT INTO artigos_base (titulo, conteudo, autor) VALUES 
    ('Introdução ao SQLite', 'SQLite é um banco de dados leve e eficiente para aplicações embarcadas.', 'João'),
    ('Python e Banco de Dados', 'Python possui excelente suporte para SQLite através do módulo sqlite3.', 'Maria'),
    ('Performance em SQLite', 'Dicas para otimizar consultas e índices no SQLite.', 'Pedro'),
    ('SQLite vs PostgreSQL', 'Comparação entre SQLite e PostgreSQL para diferentes casos de uso.', 'Ana');

-- Busca full-text
SELECT * FROM artigos WHERE artigos MATCH 'SQLite';

-- Busca com ranking
SELECT *, rank FROM artigos WHERE artigos MATCH 'SQLite' ORDER BY rank;

-- Busca em coluna específica
SELECT * FROM artigos WHERE artigos MATCH 'titulo:SQLite';

-- Busca com operadores
SELECT * FROM artigos WHERE artigos MATCH 'SQLite AND Python';
SELECT * FROM artigos WHERE artigos MATCH 'SQLite OR PostgreSQL';
SELECT * FROM artigos WHERE artigos MATCH 'SQLite NOT PostgreSQL';

-- Busca por prefixo
SELECT * FROM artigos WHERE artigos MATCH 'SQL*';

-- Highlight dos termos encontrados
SELECT highlight(artigos, 0, '<b>', '</b>') AS titulo_destacado
FROM artigos 
WHERE artigos MATCH 'SQLite';

-- Snippet do conteúdo
SELECT snippet(artigos, 1, '<b>', '</b>', '...', 10) AS trecho
FROM artigos 
WHERE artigos MATCH 'banco dados';

-- ============================================
-- R-TREE (Índice Espacial)
-- ============================================

-- Criar índice R-Tree para dados geoespaciais
CREATE VIRTUAL TABLE locais USING rtree(
    id,
    min_lat, max_lat,
    min_lng, max_lng
);

-- Inserir pontos (como retângulos de tamanho zero)
INSERT INTO locais VALUES (1, -23.5505, -23.5505, -46.6333, -46.6333);  -- São Paulo
INSERT INTO locais VALUES (2, -22.9068, -22.9068, -43.1729, -43.1729);  -- Rio de Janeiro
INSERT INTO locais VALUES (3, -19.9191, -19.9191, -43.9386, -43.9386);  -- Belo Horizonte
INSERT INTO locais VALUES (4, -25.4284, -25.4284, -49.2733, -49.2733);  -- Curitiba

-- Buscar pontos em uma região (bounding box)
SELECT id FROM locais
WHERE min_lat >= -25 AND max_lat <= -20
  AND min_lng >= -50 AND max_lng <= -40;

-- ============================================
-- GENERATED COLUMNS (SQLite 3.31+)
-- ============================================

CREATE TABLE produtos (
    id INTEGER PRIMARY KEY,
    nome TEXT NOT NULL,
    preco REAL NOT NULL,
    quantidade INTEGER NOT NULL,
    -- Coluna gerada (calculada automaticamente)
    valor_total REAL GENERATED ALWAYS AS (preco * quantidade) STORED,
    -- Coluna virtual (calculada sob demanda)
    em_estoque TEXT GENERATED ALWAYS AS (
        CASE WHEN quantidade > 0 THEN 'Sim' ELSE 'Não' END
    ) VIRTUAL
);

INSERT INTO produtos (nome, preco, quantidade) VALUES ('Notebook', 3500.00, 10);
INSERT INTO produtos (nome, preco, quantidade) VALUES ('Mouse', 89.90, 50);
INSERT INTO produtos (nome, preco, quantidade) VALUES ('Monitor', 1200.00, 0);

SELECT * FROM produtos;

-- ============================================
-- STRICT TABLES (SQLite 3.37+)
-- ============================================

-- Tabela com tipos estritos (como outros bancos)
CREATE TABLE usuarios_strict (
    id INTEGER PRIMARY KEY,
    nome TEXT NOT NULL,
    idade INTEGER,
    saldo REAL
) STRICT;

-- Isso funciona
INSERT INTO usuarios_strict VALUES (1, 'João', 30, 1000.50);

-- Isso FALHA (texto em coluna INTEGER)
-- INSERT INTO usuarios_strict VALUES (2, 'Maria', 'trinta', 500.00);
-- Error: cannot store TEXT value in INTEGER column usuarios_strict.idade

-- ============================================
-- JSON (Funções nativas)
-- ============================================

CREATE TABLE configuracoes (
    id INTEGER PRIMARY KEY,
    nome TEXT,
    dados TEXT  -- JSON armazenado como TEXT
);

INSERT INTO configuracoes VALUES 
    (1, 'app', '{"tema": "escuro", "idioma": "pt-BR", "notificacoes": true}'),
    (2, 'usuario', '{"nome": "João", "preferencias": {"fonte": 14, "cores": ["azul", "verde"]}}');

-- Extrair valores JSON
SELECT 
    nome,
    json_extract(dados, '$.tema') AS tema,
    json_extract(dados, '$.idioma') AS idioma
FROM configuracoes
WHERE nome = 'app';

-- Extrair de estruturas aninhadas
SELECT 
    json_extract(dados, '$.nome') AS nome_usuario,
    json_extract(dados, '$.preferencias.fonte') AS tamanho_fonte,
    json_extract(dados, '$.preferencias.cores[0]') AS cor_primaria
FROM configuracoes
WHERE nome = 'usuario';

-- Modificar JSON
UPDATE configuracoes 
SET dados = json_set(dados, '$.tema', 'claro')
WHERE nome = 'app';

SELECT dados FROM configuracoes WHERE nome = 'app';

-- Limpar
DROP TABLE artigos;
DROP TABLE artigos_base;
DROP TABLE locais;
DROP TABLE produtos;
DROP TABLE usuarios_strict;
DROP TABLE configuracoes;

14 – Dicas e Boas Práticas

SQLITE3 Shell
-- ============================================
-- BOAS PRÁTICAS
-- ============================================

-- 1. SEMPRE usar transações para múltiplas operações
BEGIN;
INSERT INTO tabela VALUES (1, 'a');
INSERT INTO tabela VALUES (2, 'b');
INSERT INTO tabela VALUES (3, 'c');
COMMIT;

-- 2. Habilitar chaves estrangeiras (desabilitadas por padrão!)
PRAGMA foreign_keys = ON;

-- 3. Usar WAL mode para melhor concorrência
PRAGMA journal_mode = WAL;

-- 4. Criar índices para colunas usadas em WHERE, JOIN, ORDER BY
CREATE INDEX idx_tabela_coluna ON tabela(coluna);

-- 5. Usar EXPLAIN QUERY PLAN para otimizar queries
EXPLAIN QUERY PLAN SELECT * FROM tabela WHERE coluna = 'valor';

-- 6. Executar ANALYZE após grandes alterações
ANALYZE;

-- 7. Usar VACUUM periodicamente para compactar
VACUUM;

-- 8. Usar prepared statements (via código) para evitar SQL injection

-- 9. Fazer backup regular
-- .backup arquivo_backup.db

-- 10. Usar tipos apropriados (mesmo com type affinity flexível)
CREATE TABLE exemplo (
    id INTEGER PRIMARY KEY,      -- Sempre INTEGER para PK
    nome TEXT NOT NULL,          -- TEXT para strings
    valor REAL,                  -- REAL para decimais
    ativo INTEGER DEFAULT 1,     -- INTEGER para boolean (0/1)
    dados BLOB,                  -- BLOB para binários
    criado TEXT DEFAULT CURRENT_TIMESTAMP  -- TEXT para datas
);

-- ============================================
-- ERROS COMUNS A EVITAR
-- ============================================

-- ❌ NÃO: Esquecer de habilitar FK
-- ✅ SIM: PRAGMA foreign_keys = ON;

-- ❌ NÃO: Múltiplos INSERTs sem transação (muito lento)
-- ✅ SIM: BEGIN; ... múltiplos INSERTs ... COMMIT;

-- ❌ NÃO: SELECT * em produção
-- ✅ SIM: SELECT coluna1, coluna2 FROM tabela;

-- ❌ NÃO: Confiar no autocommit para operações críticas
-- ✅ SIM: Usar transações explícitas

-- ❌ NÃO: Guardar arquivos grandes no banco
-- ✅ SIM: Guardar path do arquivo e manter arquivo no filesystem

-- ❌ NÃO: Usar SQLite para alta concorrência de escrita
-- ✅ SIM: Usar PostgreSQL/MySQL para esses casos

-- ============================================
-- QUANDO USAR SQLITE
-- ============================================

-- ✅ Aplicações desktop/mobile
-- ✅ Sites com baixo/médio tráfego
-- ✅ Dispositivos embarcados/IoT
-- ✅ Testes e desenvolvimento
-- ✅ Cache local
-- ✅ Armazenamento de configurações
-- ✅ Prototipagem rápida
-- ✅ Análise de dados (datasets pequenos/médios)

-- ❌ Alta concorrência de escrita
-- ❌ Grandes volumes de dados (>100GB)
-- ❌ Replicação necessária
-- ❌ Múltiplos servidores acessando mesmo banco

A simplicidade é o mais
alto grau de sofisticação

Autor: Leonardo da Vinci

Terminamos por hoje!

Patrick Brandão, patrickbrandao@gmail.com