Aritmética decimal no SQL Server: precisão e arredondamento
Evite divisão inteira, estouros intermediários e diferenças em faturas escolhendo tipos numéricos e regras explícitas de arredondamento no SQL Server.
Uma coluna decimal não torna automaticamente exatos todos os cálculos usados para preenchê-la. O SQL Server avalia expressões intermediárias conforme os tipos dos operandos. Informações podem desaparecer antes da atribuição final, e uma agregação pode estourar mesmo quando a coluna de destino comportaria o resultado. Em faturamento, esses erros são traiçoeiros porque os números frequentemente parecem razoáveis.
Comece pela grandeza de negócio. Valores monetários, taxas de câmbio, percentuais e quantidades têm necessidades diferentes. Em decimal(p,s), p representa todos os dígitos e s os dígitos depois da vírgula. decimal(12,2) deixa dez posições para a parte inteira. Definir apenas as casas decimais não resolve o dimensionamento.
Controlar os cálculos intermediários
Estas expressões demonstram por que converter apenas o resultado pode ser tarde demais.
SELECT 5 / 2 AS IntegerResult,
CAST(5 AS decimal(12,4)) / 2 AS DecimalResult,
CAST(5 / 2 AS decimal(12,4)) AS CastTooLate;
A divisão inteira produz 2. Converter um operando antes da divisão preserva 2,5. Converter o resultado inteiro produz 2,0000; a fração perdida não pode ser reconstruída.
O mesmo princípio vale para multiplicação. Dois valores int podem estourar antes de uma conversão externa para bigint. Amplie um operando antes da operação. O tipo da coluna de destino não altera retroativamente a avaliação da expressão de origem.
Expressões decimal também recebem precisão e escala derivadas. Na multiplicação, a regra inicial usa p1 + p2 + 1 para precisão e soma as escalas. O limite de precisão é 38; regras adicionais podem reduzir a escala ou ainda permitir um estouro. Divisões podem exigir muitas casas decimais. Portanto, converter tudo para decimal(38,...) não oferece precisão ilimitada.
Calcule o maior produto esperado entre quantidade, preço e taxa e determine o erro final aceitável. Escolha tipos intermediários compatíveis e deixe a conversão final explícita. Quando o driver permite configurar precisão e escala de parâmetros, configure essas propriedades para evitar definições diferentes conforme o valor recebido.
Definir onde arredondar
Duas políticas de faturamento coerentes podem chegar a totais diferentes.
DECLARE @Lines table (Amount decimal(10,3));
INSERT @Lines VALUES (19.995), (19.995);
SELECT SUM(ROUND(Amount, 2)) AS RoundedPerLine,
ROUND(SUM(Amount), 2) AS RoundedInvoice
FROM @Lines;
Arredondar cada linha de 19,995 para duas casas antes de somar resulta em 40,000. Somar primeiro e arredondar a fatura resulta em 39,990. O zero extra exibido vem da escala preservada; a diferença relevante é de um centavo.
A posição de ROUND não deveria escolher essa política por acidente. Defina se impostos e descontos são calculados por item ou por fatura, em qual ordem e como distribuir eventuais diferenças. O ROUND do SQL Server resolve empates exatos afastando o valor de zero. Um sistema com outra regra pode produzir valores diferentes usando os mesmos dados.
Não substitua decimal por float apenas para escapar de um estouro. float usa representação binária aproximada e não atende bem a contratos de igualdade decimal exata. Medições científicas podem justificar tipos aproximados. A escolha deve seguir a necessidade, e não uma preferência absoluta por um tipo.
Ampliar antes da agregação
SUM de uma expressão int retorna int, mesmo que o resultado seja posteriormente atribuído a bigint.
DECLARE @Counts table (Quantity int);
INSERT @Counts VALUES (2000000000), (2000000000);
SELECT SUM(CONVERT(bigint, Quantity)) AS TotalQuantity
FROM @Counts;
Neste caso, a entrada é ampliada antes da soma e o resultado é 4.000.000.000. Converter SUM(Quantity) depois não evitaria o estouro interno. SUM de decimal retorna decimal(38,s), mas ainda existe um limite para a quantidade de dígitos inteiros.
Defina também o significado de dados ausentes. SUM ignora NULL e retorna NULL quando não recebe nenhum valor não nulo. Trocar esse resultado por zero pode ser adequado em um relatório de quantidades, mas esconder valores financeiros desconhecidos. Preço desconhecido não significa produto gratuito.
Uma boa coleção de testes inclui divisões fracionárias, maiores valores possíveis, reembolsos negativos, casos exatamente no meio do arredondamento e conjuntos vazios. Compare também com os cálculos da aplicação. Observe cada etapa intermediária, e não apenas o texto formatado no final. A formatação consegue esconder perda de precisão, mas não consegue recuperá-la.
Referências técnicas: Microsoft Learn: Precision, scale, and length · Microsoft Learn: SUM.