Projeto: Controle de Gastos
Todo mês acontece a mesma coisa com a Ana: o salário cai no dia 1º e, lá pelo dia 25, ela não sabe para onde foi o dinheiro. Ela já tentou caderno, planilha e aplicativo. Agora vai tentar o que aprendeu neste guia: um banco de dados só dela, com cada gasto anotado e o SQL respondendo às perguntas. Quanto gastei no mercado? Estourei o orçamento do lazer? Quanto sobrou em setembro? Neste projeto você constrói esse banco em versões e, no caminho, aprende a regra de ouro de quem guarda dinheiro em banco de dados: nada de casas decimais "soltas".
- A começar com uma tabela de gastos e a somar por categoria.
- Por que
REALengana com dinheiro (0.1 + 0.2não é0.3) e como guardar tudo em centavos. - A registrar entradas e saídas na mesma tabela, pelo sinal do valor, e a mostrar reais com
printf. - A criar categorias com um orçamento mensal e comparar com o que foi gasto.
- Entradas, saídas e saldo por mês com
CASEdentro doSUM. - O saldo acumulado, dia a dia, com
SUM() OVER.
O controle de gastos é um banco novo. Como o editor começa do zero a cada Rodar (com a Loja Aurora carregada), toda caixa traz a montagem (as tabelas e os lançamentos) e, depois do comentário -- a consulta, a pergunta daquela versão. A montagem só muda quando a versão muda o modelo.
Versão 1: uma tabela de gastos
A primeira ideia da Ana é copiar o caderno: cada gasto numa linha, com a data, o que foi, a categoria e o valor em reais.
CREATE TABLE gastos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria TEXT NOT NULL,
valor REAL NOT NULL
);
INSERT INTO gastos (data, descricao, categoria, valor) VALUES
('2026-09-06', 'Supermercado', 'Mercado', 385.90),
('2026-09-10', 'Recarga do ônibus', 'Transporte', 150.00),
('2026-09-13', 'Cinema', 'Lazer', 64.00),
('2026-09-18', 'Feira', 'Mercado', 92.35),
('2026-09-20', 'Farmácia', 'Saúde', 47.89),
('2026-09-22', 'Supermercado', 'Mercado', 412.70),
('2026-09-27', 'Show', 'Lazer', 180.00);
-- a consulta
SELECT categoria, SUM(valor) AS total
FROM gastos
GROUP BY categoria
ORDER BY total DESC;
- data TEXT NOT NULL
- A data no formato
AAAA-MM-DD, que se ordena certinho e que as funções de data entendem (aula Datas e Horas). - valor REAL NOT NULL
- O valor em reais, com centavos depois do ponto. Guarde essa escolha: a versão 2 vai mudá-la.
- SUM(valor) … GROUP BY categoria
- Quanto foi para cada categoria (aulas SUM() e GROUP BY).
Em setembro, o mercado levou 890.95, e o lazer, 244.0. Repare que o SQLite escreve 244.0, não 244.00: ele mostra o número, não "dinheiro". Isso também se resolve na versão 2.
Missão: a Ana quer ver, um por um, os gastos da categoria Mercado: a data, a descrição e o valor, do maior para o menor (aulas WHERE e ORDER BY).
CREATE TABLE gastos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria TEXT NOT NULL,
valor REAL NOT NULL
);
INSERT INTO gastos (data, descricao, categoria, valor) VALUES
('2026-09-06', 'Supermercado', 'Mercado', 385.90),
('2026-09-10', 'Recarga do ônibus', 'Transporte', 150.00),
('2026-09-13', 'Cinema', 'Lazer', 64.00),
('2026-09-18', 'Feira', 'Mercado', 92.35),
('2026-09-20', 'Farmácia', 'Saúde', 47.89),
('2026-09-22', 'Supermercado', 'Mercado', 412.70),
('2026-09-27', 'Show', 'Lazer', 180.00);
-- a consulta
SELECT categoria, SUM(valor) AS total
FROM gastos
GROUP BY categoria;
CREATE TABLE gastos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria TEXT NOT NULL,
valor REAL NOT NULL
);
INSERT INTO gastos (data, descricao, categoria, valor) VALUES
('2026-09-06', 'Supermercado', 'Mercado', 385.90),
('2026-09-10', 'Recarga do ônibus', 'Transporte', 150.00),
('2026-09-13', 'Cinema', 'Lazer', 64.00),
('2026-09-18', 'Feira', 'Mercado', 92.35),
('2026-09-20', 'Farmácia', 'Saúde', 47.89),
('2026-09-22', 'Supermercado', 'Mercado', 412.70),
('2026-09-27', 'Show', 'Lazer', 180.00);
-- a consulta
SELECT data, descricao, valor
FROM gastos
WHERE categoria = 'Mercado'
ORDER BY valor DESC;
Versão 2: dinheiro em centavos
Antes de crescer o banco, um teste que todo programador faz uma vez na vida e nunca esquece:
SELECT 0.1 + 0.2 AS soma,
0.1 + 0.2 = 0.3 AS igual,
0.1 + 0.2 - 0.3 AS diferenca,
10 + 20 = 30 AS igual_em_centavos;
A soma aparece como 0.3, mas a comparação com 0.3 dá 0, falso. A diferença mostra o porquê: sobra um 5.55111512312578e-17 (0,0000000000000000555…). O tipo REAL guarda números em binário, e 0.1 em binário é uma dízima, como 1/3 em decimal: o computador guarda uma aproximação. Na tela, o arredondamento esconde o erro; na comparação, ele aparece (aula Os Tipos no SQLite).
E isso acontece com os dados da Ana. O total do mercado em setembro "é" 890.95, mas:
SELECT 385.90 + 92.35 + 412.70 AS total_mercado,
385.90 + 92.35 + 412.70 = 890.95 AS bate_com_890_95;
Não bate. Num relatório que só mostra o número, ninguém percebe; num WHERE total = 890.95, a linha some. A solução clássica é guardar o dinheiro em centavos, como número inteiro: 385.90 vira 38590. Inteiros são exatos: somam, subtraem e comparam sem resto nenhum. E já que vamos refazer a tabela, ela passa a guardar também as entradas, como o extrato do banco: dinheiro que entra é positivo, dinheiro que sai é negativo.
-- montagem da versão 2
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria TEXT NOT NULL,
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos (data, descricao, categoria, valor) VALUES
('2026-09-01', 'Salário', 'Salário', 350000),
('2026-09-05', 'Aluguel', 'Moradia', -110000),
('2026-09-06', 'Supermercado', 'Mercado', -38590),
('2026-09-10', 'Recarga do ônibus', 'Transporte', -15000),
('2026-09-13', 'Cinema', 'Lazer', -6400),
('2026-09-18', 'Feira', 'Mercado', -9235),
('2026-09-20', 'Farmácia', 'Saúde', -4789),
('2026-09-22', 'Supermercado', 'Mercado', -41270),
('2026-09-27', 'Show', 'Lazer', -18000);
-- a consulta
SELECT data, descricao, valor,
printf('%.2f', valor / 100.0) AS reais
FROM lancamentos;
- valor INTEGER NOT NULL
- Centavos, inteiros. O salário de R$ 3.500,00 é
350000; o supermercado de R$ 385,90 é-38590. - o sinal
+entra,-sai. Com isso, o saldo é simplesmenteSUM(valor).- valor / 100.0
- Volta para reais só na hora de mostrar. O
.0é obrigatório:valor / 100, com dois inteiros, faria divisão inteira e jogaria os centavos fora (38590 / 100dá385). - printf('%.2f', …)
- Formata com exatamente 2 casas:
385.90,-60.00… O%.2fquer dizer "número com 2 casas decimais" (aula Funções de Texto).
Missão: a consulta abaixo tenta mostrar o saldo de setembro em reais, mas mostra 1067: sumiram os centavos. Conserte para aparecer o saldo com 2 casas decimais, numa coluna saldo.
-- montagem da versão 2
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria TEXT NOT NULL,
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos (data, descricao, categoria, valor) VALUES
('2026-09-01', 'Salário', 'Salário', 350000),
('2026-09-05', 'Aluguel', 'Moradia', -110000),
('2026-09-06', 'Supermercado', 'Mercado', -38590),
('2026-09-10', 'Recarga do ônibus', 'Transporte', -15000),
('2026-09-13', 'Cinema', 'Lazer', -6400),
('2026-09-18', 'Feira', 'Mercado', -9235),
('2026-09-20', 'Farmácia', 'Saúde', -4789),
('2026-09-22', 'Supermercado', 'Mercado', -41270),
('2026-09-27', 'Show', 'Lazer', -18000);
-- a consulta
SELECT SUM(valor) / 100 AS saldo
FROM lancamentos;
-- montagem da versão 2
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria TEXT NOT NULL,
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos (data, descricao, categoria, valor) VALUES
('2026-09-01', 'Salário', 'Salário', 350000),
('2026-09-05', 'Aluguel', 'Moradia', -110000),
('2026-09-06', 'Supermercado', 'Mercado', -38590),
('2026-09-10', 'Recarga do ônibus', 'Transporte', -15000),
('2026-09-13', 'Cinema', 'Lazer', -6400),
('2026-09-18', 'Feira', 'Mercado', -9235),
('2026-09-20', 'Farmácia', 'Saúde', -4789),
('2026-09-22', 'Supermercado', 'Mercado', -41270),
('2026-09-27', 'Show', 'Lazer', -18000);
-- a consulta
SELECT printf('%.2f', SUM(valor) / 100.0) AS saldo
FROM lancamentos;
R$ 1067,16 sobraram em setembro. Repare que SUM(valor) / 100.0, sem o printf, também cumpre a missão: dá 1067.16. O printf só garante as duas casas sempre, inclusive em valores redondos como 1100.00.
| Como guardar dinheiro | Exemplo | Prós e contras |
|---|---|---|
REAL em reais | 385.90 | Fácil de ler, mas soma com "restos" e comparação de igualdade falha. Evite. |
INTEGER em centavos | 38590 | Exato. Exige dividir por 100.0 para mostrar. O jeito recomendado no SQLite. |
| Texto | '385,90' | Não faz conta: o SQLite trata como texto. Nunca. |
DECIMAL(10,2) em outros bancos | 385.90 | Exato, nos bancos que têm esse tipo de verdade (veja "Nos outros bancos"). |
Versão 3: categorias com orçamento
A categoria escrita como texto em cada lançamento tem o mesmo problema dos autores no projeto Biblioteca: um "mercado" minúsculo vira uma categoria nova. E a Ana quer mais: um limite de gasto por mês para cada categoria. Isso pede uma tabela própria. A primeira tentativa de nome dá erro no editor:
CREATE TABLE categorias (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER
);
"A tabela categorias já existe": é a das categorias de produto da Loja Aurora, que o editor carrega. Num banco só da Ana não haveria conflito, mas o nome categorias_gasto é até melhor: diz de quê são as categorias.
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT c.nome AS categoria,
printf('%.2f', c.limite / 100.0) AS orcamento
FROM categorias_gasto c;
- nome TEXT NOT NULL UNIQUE
- Não existem duas categorias com o mesmo nome (aula UNIQUE).
- limite INTEGER
- O orçamento do mês, em centavos. O Salário não é gasto, então não tem limite:
NULL. E oprintfcomNULLdevolve um texto vazio, não um erro. - categoria_id … REFERENCES categorias_gasto(id)
- Cada lançamento aponta para a sua categoria (aula FOREIGN KEY).
- outubro
- A montagem agora tem também os lançamentos de outubro, até o dia 16: 16 lançamentos ao todo.
Missão: quanto a Ana gastou em cada categoria em setembro? Mostre o nome da categoria e o gasto em reais (positivo), só das saídas (valor < 0) com data de setembro. Dica: gasto positivo é -SUM(l.valor), ou SUM(-l.valor).
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT c.nome AS categoria,
printf('%.2f', c.limite / 100.0) AS orcamento
FROM categorias_gasto c;
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT c.nome AS categoria,
-SUM(l.valor) / 100.0 AS gasto
FROM lancamentos l
JOIN categorias_gasto c ON c.id = l.categoria_id
WHERE l.valor < 0
AND l.data BETWEEN '2026-09-01' AND '2026-09-30'
GROUP BY c.id
ORDER BY gasto DESC;
Aqui, o 890.95 do mercado vem de 89095 / 100.0: uma divisão só, de um inteiro exato, no fim da conta. É bem diferente de somar três REAL aproximados. A regra é calcular em centavos, dividir só para mostrar.
Versão 4: entradas, saídas e saldo de cada mês
O resumo que a Ana quer ver no dia 1º: quanto entrou, quanto saiu e quanto sobrou em cada mês. Com o sinal, o saldo é SUM(valor). Para separar entradas de saídas, um CASE dentro do SUM escolhe o que entra em cada coluna (aula CASE):
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT strftime('%Y-%m', data) AS mes,
SUM(CASE WHEN valor > 0 THEN valor ELSE 0 END) / 100.0 AS entradas,
SUM(CASE WHEN valor < 0 THEN -valor ELSE 0 END) / 100.0 AS saidas,
SUM(valor) / 100.0 AS saldo
FROM lancamentos
GROUP BY mes;
- strftime('%Y-%m', data)
- De
'2026-09-22', o mês:'2026-09'(aula Funções de Data). - SUM(CASE WHEN valor > 0 THEN valor ELSE 0 END)
- Soma só os positivos: os negativos viram 0 e não mudam a soma.
- SUM(CASE WHEN valor < 0 THEN -valor ELSE 0 END)
- Soma só os negativos, com o sinal trocado, para a coluna "saídas" sair positiva.
- SUM(valor)
- Tudo junto: o saldo do mês.
Em setembro saíram 2432.84 e sobraram 1067.16; em outubro, até o dia 16, saíram 2069.85 e sobram 1430.15. Outubro ainda não acabou, então essa sobra vai diminuir.
Missão: a Ana quer saber que porcentagem do que entrou ela conseguiu guardar. Mostre as colunas mes, saldo (em reais) e poupado: o saldo dividido pelas entradas, vezes 100, arredondado em 1 casa. Cuidado com a divisão de inteiros: comece a conta por 100.0 *.
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT strftime('%Y-%m', data) AS mes,
SUM(CASE WHEN valor > 0 THEN valor ELSE 0 END) / 100.0 AS entradas,
SUM(valor) / 100.0 AS saldo
FROM lancamentos
GROUP BY mes;
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT strftime('%Y-%m', data) AS mes,
SUM(valor) / 100.0 AS saldo,
ROUND(100.0 * SUM(valor) / SUM(CASE WHEN valor > 0 THEN valor ELSE 0 END), 1) AS poupado
FROM lancamentos
GROUP BY mes;
30.5% em setembro e, por enquanto, 40.9% em outubro. Sem o 100.0, a conta SUM(valor) / SUM(…) seria uma divisão de inteiros, e daria 0 nos dois meses.
Versão 5: o saldo dia a dia
O extrato do banco mostra, depois de cada lançamento, o saldo até ali. É uma soma que vai acumulando, linha a linha: o SUM como função de janela (aula Funções de Janela (OVER)).
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT data, descricao,
valor / 100.0 AS valor,
SUM(valor) OVER (ORDER BY data, id) / 100.0 AS saldo
FROM lancamentos;
- SUM(valor) OVER ( … )
- Com
OVER, oSUMnão junta as linhas num grupo: cada linha continua lá e ganha a soma da sua "janela". - ORDER BY data, id
- A janela de cada linha vai do primeiro lançamento até ela, nessa ordem. O
iddesempata dois lançamentos no mesmo dia. - / 100.0
- Só para mostrar em reais; a soma continua em centavos.
O saldo nunca ficou negativo: o ponto mais baixo foi 1067.16, no fim de setembro, logo antes do salário de outubro. No fim da lista, 2497.31: a soma de tudo, igual a um SUM(valor) comum.
Missão: agora a Ana quer só as saídas, com uma coluna gasto_no_mes: quanto ela já gastou no mês até aquele lançamento, recomeçando do zero a cada mês. Mostre data, descricao, gasto (positivo, em reais) e gasto_no_mes. Para a soma recomeçar, ponha PARTITION BY strftime('%Y-%m', data) dentro do OVER (aula PARTITION BY).
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT data, descricao,
valor / 100.0 AS valor,
SUM(valor) OVER (ORDER BY data, id) / 100.0 AS saldo
FROM lancamentos;
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT data, descricao,
-valor / 100.0 AS gasto,
SUM(-valor) OVER (PARTITION BY strftime('%Y-%m', data)
ORDER BY data, id) / 100.0 AS gasto_no_mes
FROM lancamentos
WHERE valor < 0;
Em outubro, o aluguel do dia 5 abre a conta do mês com 1100.0, e a feira do dia 16 fecha com 2069.85, o mesmo número das saídas de outubro na versão 4. O WHERE valor < 0 roda antes da janela: as entradas nem chegam a ela.
Versão 6: o fechamento do orçamento
A pergunta que motivou tudo: estourei o orçamento? Para cada mês e categoria, o que foi gasto, o limite e quanto do limite foi usado.
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT strftime('%Y-%m', l.data) AS mes,
c.nome AS categoria,
-SUM(l.valor) / 100.0 AS gasto,
c.limite / 100.0 AS limite,
ROUND(100.0 * -SUM(l.valor) / c.limite, 1) AS usado
FROM lancamentos l
JOIN categorias_gasto c ON c.id = l.categoria_id
WHERE l.valor < 0
GROUP BY mes, c.id;
- GROUP BY mes, c.id
- Um grupo por mês e categoria (aula Agrupando por Mais de uma Coluna).
- c.limite
- Fora de uma agregação, mas seguro: dentro do grupo, a categoria é uma só, e o limite também.
- ROUND(100.0 * -SUM(l.valor) / c.limite, 1)
- A porcentagem do orçamento usada. Mais de 100 é orçamento estourado.
Três estouros: em setembro, o Mercado (111.4%) e o Lazer (122.0%, culpa do show); em outubro, a Saúde (166.7%, a consulta médica). E repare que o Lazer nem aparece em outubro: a Ana não gastou nada com lazer, e um grupo sem linhas não existe. Se ela quisesse ver "Lazer: 0", a consulta precisaria partir das categorias com LEFT JOIN, como na estante do projeto Biblioteca.
Missão final: monte o alerta do orçamento: só as linhas em que o gasto passou do limite, com as colunas mes, categoria, gasto, limite e uma coluna situacao com o texto 'estourou'. O filtro é sobre uma soma, então vai no HAVING (aula HAVING).
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT strftime('%Y-%m', l.data) AS mes,
c.nome AS categoria,
-SUM(l.valor) / 100.0 AS gasto,
c.limite / 100.0 AS limite
FROM lancamentos l
JOIN categorias_gasto c ON c.id = l.categoria_id
WHERE l.valor < 0
GROUP BY mes, c.id;
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT strftime('%Y-%m', l.data) AS mes,
c.nome AS categoria,
-SUM(l.valor) / 100.0 AS gasto,
c.limite / 100.0 AS limite,
'estourou' AS situacao
FROM lancamentos l
JOIN categorias_gasto c ON c.id = l.categoria_id
WHERE l.valor < 0
GROUP BY mes, c.id
HAVING -SUM(l.valor) > c.limite;
Uma variação mais completa, que também cumpre a missão se você filtrar depois: uma coluna CASE WHEN -SUM(l.valor) > c.limite THEN 'estourou' WHEN -SUM(l.valor) > 0.8 * c.limite THEN 'atenção' ELSE 'ok' END, que avisa antes de estourar. A Moradia, com 91.7% nos dois meses, ficaria em "atenção".
Quando dá errado
=
CREATE TABLE gastos (descricao TEXT, categoria TEXT, valor REAL);
INSERT INTO gastos VALUES ('Supermercado', 'Mercado', 385.90),
('Feira', 'Mercado', 92.35),
('Supermercado', 'Mercado', 412.70);
SELECT categoria, SUM(valor) AS total
FROM gastos
GROUP BY categoria
HAVING SUM(valor) = 890.95;
O que aconteceu: nenhuma linha. O total "é" 890.95 na tela, mas não é exatamente 890.95 na memória, e o = compara bit a bit. Sem erro, sem aviso.
Como consertar: guarde centavos em INTEGER (versão 2). Se precisar lidar com um REAL que já existe, compare arredondado: HAVING ROUND(SUM(valor), 2) = 890.95.
SELECT 38590 / 100 AS inteira,
38590 / 100.0 AS com_ponto,
printf('%.2f', 38590 / 100.0) AS formatada;
O que aconteceu: 38590 / 100 deu 385. Inteiro dividido por inteiro, no SQLite, é divisão inteira: os 90 centavos foram embora, sem aviso.
Como consertar: divida por 100.0. Basta um dos lados ter ponto para a conta ter casas decimais.
CREATE TABLE lancamentos (descricao TEXT, valor INTEGER NOT NULL);
INSERT INTO lancamentos VALUES ('Salário', 350000),
('Aluguel', 110000); -- faltou o sinal de menos
SELECT SUM(valor) / 100.0 AS saldo FROM lancamentos;
O que aconteceu: saldo de 4600.0, em vez de 2400.0. O aluguel entrou como se fosse dinheiro recebido. Na convenção do sinal, esquecer o - é o erro mais comum, e o banco não tem como saber.
Como consertar: corrija com UPDATE lancamentos SET valor = -110000 WHERE descricao = 'Aluguel';. Para não depender do sinal, existe outra forma de modelar: valor sempre positivo, com CHECK (valor > 0), e uma coluna tipo com CHECK (tipo IN ('entrada', 'saida')). O saldo vira SUM(CASE WHEN tipo = 'entrada' THEN valor ELSE -valor END): mais longo, mas o banco recusa um tipo errado (aula CHECK).
SELECT 100 / 0 AS por_zero,
100 / NULL AS por_null,
ROUND(100.0 * 350000 / NULL, 1) AS salario_usado;
O que aconteceu: as três dão NULL, e nenhuma dá erro. No SQLite, dividir por zero devolve NULL. Por isso a categoria Salário, com limite NULL, não quebraria a versão 6; ela ficaria com usado vazio (e lá ela nem aparece, porque o WHERE l.valor < 0 tira as entradas).
Como consertar: decida o que mostrar: COALESCE(…, 'sem limite') troca o NULL por um texto (aula IFNULL e COALESCE). E não conte com esse comportamento nos outros bancos (veja abaixo).
Os bancos com servidor têm um tipo decimal exato para dinheiro: DECIMAL(10,2) ou NUMERIC(10,2) (PostgreSQL, MySQL, SQL Server). Lá, 385.90 + 92.35 + 412.70 = 890.95 é verdadeiro, e guardar em centavos é opcional. O SQLite aceita escrever DECIMAL(10,2) no CREATE TABLE, mas guarda o valor como REAL do mesmo jeito: o nome do tipo não muda a matemática (aula Os Tipos no SQLite).
Divisão por zero também muda: o PostgreSQL e o SQL Server param com erro (division by zero); o MySQL, no modo padrão, devolve NULL com um aviso, como o SQLite. E o printf é do SQLite; para formatar números, o PostgreSQL usa to_char, o MySQL FORMAT e o SQL Server FORMAT. Mais em As Diferenças entre os Bancos.
Referência rápida
| Tarefa do controle de gastos | SQL |
|---|---|
| Dinheiro exato no SQLite | valor INTEGER NOT NULL (em centavos) |
| Mostrar em reais | valor / 100.0 ou printf('%.2f', valor / 100.0) |
| Saldo (convenção do sinal) | SUM(valor) |
| Só as entradas | SUM(CASE WHEN valor > 0 THEN valor ELSE 0 END) |
| Gasto positivo | -SUM(valor) … WHERE valor < 0 |
| Por mês | strftime('%Y-%m', data) AS mes … GROUP BY mes |
| Porcentagem sem divisão inteira | ROUND(100.0 * parte / todo, 1) |
| Saldo acumulado | SUM(valor) OVER (ORDER BY data, id) |
| Acumulado que recomeça todo mês | SUM(…) OVER (PARTITION BY strftime('%Y-%m', data) ORDER BY data, id) |
| Estourou o orçamento | GROUP BY mes, c.id HAVING -SUM(l.valor) > c.limite |
Exercícios
1. Em centavos
Como fica R$ 47,89 guardado em centavos, numa saída?
2. De volta a reais
Complete para mostrar o valor em reais sem perder os centavos:
3. SELECT 0.1 + 0.2 = 0.3; devolve 0. Por quê?
4. Na convenção do sinal, qual consulta dá o saldo?
5. O que muda quando o SUM(valor) ganha OVER (ORDER BY data, id)?
6. Em outubro a Ana não gastou nada com Lazer. O que a consulta do fechamento (versão 6) mostra para o Lazer em outubro?
Desafio: o maior gasto de cada mês
Missão: qual foi o maior gasto de cada mês? Uma linha por mês, com mes, descricao e gasto (em reais). Dica: RANK() OVER (PARTITION BY … ORDER BY valor) dentro de um WITH, e filtre a posição 1 por fora (aula ROW_NUMBER, RANK e DENSE_RANK). Lembre-se: o maior gasto é o valor mais negativo.
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
SELECT strftime('%Y-%m', data) AS mes, descricao, -valor / 100.0 AS gasto
FROM lancamentos
WHERE valor < 0;
-- montagem (igual em toda caixa)
CREATE TABLE categorias_gasto (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
limite INTEGER -- orçamento do mês, em centavos
);
INSERT INTO categorias_gasto VALUES
(1, 'Salário', NULL), (2, 'Moradia', 120000), (3, 'Mercado', 80000),
(4, 'Transporte', 30000), (5, 'Lazer', 20000), (6, 'Saúde', 15000);
CREATE TABLE lancamentos (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL,
descricao TEXT NOT NULL,
categoria_id INTEGER NOT NULL REFERENCES categorias_gasto(id),
valor INTEGER NOT NULL -- centavos: + entra, - sai
);
INSERT INTO lancamentos VALUES
(1, '2026-09-01', 'Salário', 1, 350000),
(2, '2026-09-05', 'Aluguel', 2, -110000),
(3, '2026-09-06', 'Supermercado', 3, -38590),
(4, '2026-09-10', 'Recarga do ônibus', 4, -15000),
(5, '2026-09-13', 'Cinema', 5, -6400),
(6, '2026-09-18', 'Feira', 3, -9235),
(7, '2026-09-20', 'Farmácia', 6, -4789),
(8, '2026-09-22', 'Supermercado', 3, -41270),
(9, '2026-09-27', 'Show', 5, -18000),
(10, '2026-10-01', 'Salário', 1, 350000),
(11, '2026-10-05', 'Aluguel', 2, -110000),
(12, '2026-10-07', 'Supermercado', 3, -45320),
(13, '2026-10-09', 'Recarga do ônibus', 4, -15000),
(14, '2026-10-11', 'Corrida por aplicativo', 4, -2875),
(15, '2026-10-14', 'Consulta médica', 6, -25000),
(16, '2026-10-16', 'Feira', 3, -8790);
-- a consulta
WITH ranking AS (
SELECT strftime('%Y-%m', data) AS mes, descricao, -valor / 100.0 AS gasto,
RANK() OVER (PARTITION BY strftime('%Y-%m', data) ORDER BY valor) AS posicao
FROM lancamentos
WHERE valor < 0
)
SELECT mes, descricao, gasto
FROM ranking
WHERE posicao = 1;
O aluguel, nos dois meses. Sem surpresa, mas agora a Ana sabe o que vem depois: tire o aluguel (AND categoria_id <> 2) e o maior gasto de setembro passa a ser o supermercado do dia 22.
Resumo
- Dinheiro no SQLite vai em
INTEGER, em centavos.REALguarda aproximações:0.1 + 0.2 = 0.3é falso, e um total "890.95" pode não ser 890.95. - Calcule em centavos e divida por
100.0(com ponto) só para mostrar;printf('%.2f', …)garante as duas casas. - Com entradas positivas e saídas negativas, o saldo é
SUM(valor);CASEdentro doSUMsepara entradas e saídas. - Categorias com
limitenuma tabela própria permitem comparar gasto e orçamento comGROUP BY mes, c.ideHAVING. SUM() OVER (ORDER BY …)dá o saldo acumulado; comPARTITION BY, ele recomeça a cada mês.- No SQLite, dividir inteiros corta a parte decimal e dividir por zero dá
NULL, sem erro. Comece porcentagens por100.0 *.