Programação Por Equipe DevBras

Capa do artigo: Funções matemáticas no MySQL: ROUND, CEIL e rateio sem centavo perdido

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 para d casas. ROUND(10.555, 2) dá 10.56.
  • TRUNCATE(x, d): corta sem arredondar. TRUNCATE(10.559, 2) dá 10.55.
  • CEIL(x) e FLOOR(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) e a DIV b: resto e divisão inteira. MOD(17, 5) é 2; 17 DIV 5 é 3.
  • GREATEST e LEAST: limitam um valor a um teto ou piso. Se qualquer argumento for NULL, o resultado é NULL.
  • SUM e AVG: agregam no conjunto. Ambas ignoram NULL, 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

  1. 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.
  2. Escreva a expressão uma vez no SQL, numa view ou numa coluna gerada.
  3. Faça relatório, tela e exportação lerem dessa view ou coluna.
  4. Na aplicação, só formate para exibir. Nunca grave o texto formatado de volta.
  5. 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 FLOAT ou DOUBLE. 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 AVG sem pensar nos nulos. Avaliações sem nota ficam fora da média; trocar por COALESCE(nota, 0) muda o significado, não só o número.
  • Ratear com ROUND e não distribuir a diferença. A soma das parcelas não fecha com o total da nota.
  • Usar FLOOR esperando "cortar as casas" em valor negativo. FLOOR(-2.1) é -3; para cortar, use TRUNCATE.
  • Gravar o valor formatado como texto. Nenhuma conta futura consegue somar "R$ 11,84".

Checklist

  • Colunas de dinheiro são DECIMAL com 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?

Avalie este artigo

Referência: http://www.linhadecodigo.com.br/artigo/3583/mysql-basico-funcoes-matematicas.aspx