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".

Você vai aprender
  • A começar com uma tabela de gastos e a somar por categoria.
  • Por que REAL engana com dinheiro (0.1 + 0.2 nã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 CASE dentro do SUM.
  • O saldo acumulado, dia a dia, com SUM() OVER.
Como as caixas deste projeto funcionam

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 é simplesmente SUM(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 / 100 dá 385).
printf('%.2f', …)
Formata com exatamente 2 casas: 385.90, -60.00… O %.2f quer 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 dinheiroExemploPrós e contras
REAL em reais385.90Fácil de ler, mas soma com "restos" e comparação de igualdade falha. Evite.
INTEGER em centavos38590Exato. 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 bancos385.90Exato, 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 o printf com NULL devolve 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, o SUM nã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 id desempata 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

Comparou um REAL com =
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.

Dividiu por 100 e perdeu os centavos
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.

Esqueceu o sinal do gasto
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).

Dividiu por um limite que não existe
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).

Nos outros bancos

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 gastosSQL
Dinheiro exato no SQLitevalor INTEGER NOT NULL (em centavos)
Mostrar em reaisvalor / 100.0 ou printf('%.2f', valor / 100.0)
Saldo (convenção do sinal)SUM(valor)
Só as entradasSUM(CASE WHEN valor > 0 THEN valor ELSE 0 END)
Gasto positivo-SUM(valor) … WHERE valor < 0
Por mêsstrftime('%Y-%m', data) AS mes … GROUP BY mes
Porcentagem sem divisão inteiraROUND(100.0 * parte / todo, 1)
Saldo acumuladoSUM(valor) OVER (ORDER BY data, id)
Acumulado que recomeça todo mêsSUM(…) OVER (PARTITION BY strftime('%Y-%m', data) ORDER BY data, id)
Estourou o orçamentoGROUP 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?

INSERT INTO lancamentos (data, descricao, categoria_id, valor) VALUES ('2026-09-20', 'Farmácia', 6, );

2. De volta a reais

Complete para mostrar o valor em reais sem perder os centavos:

SELECT valor / FROM lancamentos;

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. REAL guarda 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); CASE dentro do SUM separa entradas e saídas.
  • Categorias com limite numa tabela própria permitem comparar gasto e orçamento com GROUP BY mes, c.id e HAVING.
  • SUM() OVER (ORDER BY …) dá o saldo acumulado; com PARTITION 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 por 100.0 *.