Funções matemáticas no MySQL: ROUND, CEIL e rateio sem centavo perdido
Use ROUND, TRUNCATE, CEIL, FLOOR e agregações no MySQL para que relatório, tela e exportação mostrem o mesmo número, inclusive no rateio de frete.
O relatório do financeiro diz R$ 11,84 de desconto. A tela do pedido diz R$ 11,83. A planilha exportada diz 11,8405. Os três leram o mesmo banco, e cada um arredondou num lugar diferente. A resposta curta: faça a conta uma vez, no SQL, com a regra de arredondamento escrita, e deixe a aplicação apenas formatar o número para exibir. Para isso servem ROUND, TRUNCATE, CEIL, FLOOR e as agregações do MySQL.
O exemplo deste guia é a loja fictícia Casa do Mate, com pedidos, descontos e rateio de frete. Os números são pequenos de propósito: o centavo que some aqui é o mesmo que some em milhares de pedidos.
Por que três telas mostram três números
Cada camada tem o seu jeito de arredondar. O MySQL arredonda de um jeito, o PHP de outro, o JavaScript com toFixed de um terceiro, e a planilha mostra o valor bruto. Quando a conta é refeita em cada lugar, basta uma etapa arredondar antes da outra para o resultado mudar.
Há ainda o tipo da coluna. Em DECIMAL, o MySQL faz conta exata e o ROUND arredonda a metade para longe do zero. Em FLOAT e DOUBLE, o valor é aproximado e o arredondamento da metade segue outra regra. A documentação mostra o efeito com SELECT ROUND(2.5), ROUND(25E-1), que devolve 3 e 2. Dinheiro mora em DECIMAL; o post tipo da coluna no MySQL explica por quê.
As funções que você vai usar de verdade
ROUND(x, d): arredonda paradcasas.ROUND(10.555, 2)dá 10.56.TRUNCATE(x, d): corta sem arredondar.TRUNCATE(10.559, 2)dá 10.55.CEIL(x)eFLOOR(x): inteiro de cima e de baixo. Atenção ao negativo:FLOOR(-2.1)é -3.ABS(x): tira o sinal, útil para diferença entre dois valores.MOD(a, b)ea DIV b: resto e divisão inteira.MOD(17, 5)é 2;17 DIV 5é 3.GREATESTeLEAST: limitam um valor a um teto ou piso. Se qualquer argumento forNULL, o resultado éNULL.SUMeAVG: agregam no conjunto. Ambas ignoramNULL, o que muda o significado de uma média.
Divisão por zero no MySQL devolve NULL com um aviso em consultas, e não um erro. Proteja o divisor com NULLIF(divisor, 0) para deixar a intenção explícita.
Como levar a regra para o SQL passo a passo
- Pergunte ao financeiro (ou a quem responde pelo número) qual é a regra: arredondar por item ou no total, quantas casas, metade para cima ou corte.
- Escreva a expressão uma vez no SQL, numa view ou numa coluna gerada.
- Faça relatório, tela e exportação lerem dessa view ou coluna.
- Na aplicação, só formate para exibir. Nunca grave o texto formatado de volta.
- Crie um teste com os casos que já deram briga, com o valor esperado anotado.
Desconto por item ou no total
Na Casa do Mate, o cliente comprou 7 sachês a R$ 1,99 com 15% de desconto. Duas regras plausíveis, dois resultados:
-- Arredondando por unidade e multiplicando
SELECT ROUND(1.99 * 0.85, 2) * 7 AS por_unidade; -- 1.69 * 7 = 11.83
-- Calculando no total da linha e arredondando uma vez
SELECT ROUND(1.99 * 7 * 0.85, 2) AS no_total; -- 11.8405 -> 11.84
Nenhuma das duas está errada. O erro é a tela usar uma e o relatório usar a outra. Escolhida a regra, grave-a numa coluna gerada, que o banco recalcula sozinho:
CREATE TABLE itens (
id INT PRIMARY KEY AUTO_INCREMENT,
pedido_id INT NOT NULL,
produto VARCHAR(80) NOT NULL,
preco_unit DECIMAL(10,2) NOT NULL,
qtd INT NOT NULL,
desconto_pct DECIMAL(5,2) NOT NULL DEFAULT 0,
total_linha DECIMAL(10,2)
AS (ROUND(preco_unit * qtd * (1 - desconto_pct / 100), 2)) STORED
);
Agora qualquer consulta que precisar do total do item lê total_linha. Não existe outra versão da conta para divergir.
Rateio de frete sem perder o centavo
O pedido 1001 tem três itens de R$ 30,00 e frete de R$ 10,00, que precisa ser distribuído proporcionalmente para a nota. ROUND(10 / 3, 2) dá 3,33 para cada um, e a soma fica R$ 9,99. O centavo restante precisa ir para algum item, por regra. Com funções de janela (MySQL 8 ou superior):
INSERT INTO itens (pedido_id, produto, preco_unit, qtd) VALUES
(1001, 'Erva-mate 1 kg', 30.00, 1),
(1001, 'Cuia de porongo', 30.00, 1),
(1001, 'Bomba inox', 30.00, 1);
WITH base AS (
SELECT id, produto, total_linha,
ROUND(10.00 * total_linha / SUM(total_linha) OVER (), 2) AS frete_arred,
ROW_NUMBER() OVER (ORDER BY total_linha DESC, id) AS posicao
FROM itens
WHERE pedido_id = 1001
)
SELECT id, produto, total_linha,
frete_arred
+ CASE WHEN posicao = 1
THEN 10.00 - SUM(frete_arred) OVER ()
ELSE 0 END AS frete
FROM base;
O item de maior valor (no empate, o de menor id) recebe 3,34 e os outros 3,33. A soma volta a ser exatamente 10,00. A regra "a diferença vai para o maior item" fica escrita na consulta, e não escondida num loop da aplicação. O mesmo raciocínio vale para parcelamento: guardar valores em centavos inteiros, como em dinheiro em centavos, simplifica ainda mais.
Quando arredondar para cima é a regra
-- Caixas de 12 unidades: 30 sachês ocupam 3 caixas
SELECT CEIL(30 / 12) AS caixas;
-- Hora técnica cobrada por hora iniciada
SELECT CEIL(TIMESTAMPDIFF(MINUTE, '2026-10-17 09:00', '2026-10-17 11:10') / 60) AS horas; -- 3
-- Taxa de cancelamento com uma casa, sem dividir por zero
SELECT ROUND(100 * SUM(status = 'cancelado') / NULLIF(COUNT(*), 0), 1) AS pct_cancelado
FROM pedidos
WHERE criado_em >= '2026-10-01';
O que a aplicação ainda faz
A aplicação formata. Em PHP, por exemplo, o valor que veio do banco vira texto só na hora de exibir:
echo 'R$ ' . number_format((float) $item['total_linha'], 2, ',', '.');
Ela não chama round de novo "para ficar bonito", não soma os itens num loop para mostrar o total e não salva "11,84" como texto. Se a tela precisa do total do pedido, peça ao banco um SUM(total_linha).
Erros comuns
- Guardar dinheiro em
FLOATouDOUBLE. O arredondamento da metade muda e aparecem diferenças de centavo difíceis de explicar. - Arredondar no SQL e de novo na aplicação. Nasce um terceiro número que não bate com nenhum relatório.
- Usar
AVGsem pensar nos nulos. Avaliações sem nota ficam fora da média; trocar porCOALESCE(nota, 0)muda o significado, não só o número. - Ratear com
ROUNDe não distribuir a diferença. A soma das parcelas não fecha com o total da nota. - Usar
FLOOResperando "cortar as casas" em valor negativo.FLOOR(-2.1)é -3; para cortar, useTRUNCATE. - Gravar o valor formatado como texto. Nenhuma conta futura consegue somar "R$ 11,84".
Checklist
- Colunas de dinheiro são
DECIMALcom casas definidas. - A regra de arredondamento está escrita e foi confirmada por quem responde pelo número.
- A conta existe em um lugar só: view, coluna gerada ou consulta compartilhada.
- Rateios distribuem a diferença de centavos por uma regra explícita.
- Toda divisão tem o divisor protegido com
NULLIF. - A aplicação apenas formata, sem recalcular nem arredondar de novo.
- Há um teste com os casos que já foram contestados.
Pegue o último relatório que alguém contestou e siga o número até a origem: em quantos lugares ele é arredondado antes de chegar à tela, e qual dessas regras o financeiro realmente aprovou?
Referência: http://www.linhadecodigo.com.br/artigo/3583/mysql-basico-funcoes-matematicas.aspx