Tipos de dados para armazenar valores monetários

Karolina Kaboompics

“Qual o melhor tipo de dados para trabalhar com valores monetários?” é uma dúvida recorrente, tanto na programação em sql quanto em outras linguagens de programação.
E surgem termos como precisão, exatidão, arredondamento, truncamento, etc. Mas o que significam?

Introdução

A característica de valores monetários é ser um valor numérico e com parte fracionária. No sql server, os tipos de dados que armazenam valores numéricos são: tinyint, smallint, int, bigint, real, double precision, decimal, smallmoney e money.

Para os testes deste artigo foi utilizado conjunto de dados semelhante ao do artigo Rateio proporcional de valores. Nesse conjunto de dados, para a nota fiscal de número 043659 há os seguintes itens:

Os valores monetários, colunas Valor_unitario e Valor_total_item, estão na moeda Real, com dois algarismos na parte fracionária (duas casas decimais).

Este artigo tem como objetivo analisar os tipos de dados numéricos, disponíveis no sql server, e suas aplicações para manipular valores monetários.

tinyint, smallint, int e bigint

Estes tipos de dados são para armazenar valores numéricos inteiros, sendo que o que varia entre eles é a quantidade de bytes utilizada para armazenamento:

Entretanto, para valor monetário é necessária a parte fracionária, o que elimina os tipos de dados que armazenam somente valores inteiros: tinyint, smallint, int e bigint. Entretanto, já li relatos de casos em que valores monetários são armazenados nesses tipos de dados, aplicando conversão prévia do valor monetário de real para inteiro. Por exemplo, considerando-se os itens da nota fiscal de número 043659, os valores monetários devem ser previamente multiplicados por 100 antes de serem armazenados, ficando assim:

Observe como os valores das colunas Valor_unitario e Valor_total_item estão como valores inteiros. Por exemplo, o valor R$34,20 foi armazenado como 3420; ou seja, a unidade de medida passa de reais para centavos de real.

Nesta abordagem, antes de realizar determinados cálculos uma opção é dividir os valores por 100.0 e, após os cálculos, multiplicar por 100 antes de armazenar novamente. Ou então efetuar todos os cálculos normalmente, mas tendo o cuidado de que todos os valores monetários envolvidos estejam na unidade centavos de real.

Na imagem abaixo está um exemplo de apresentação dos valores monetários e o cálculo do percentual do valor do frete sobre o valor total da venda.

Os valores monetários foram divididos por 100.0 antes de serem exibidos e o valor da porcentagem foi arredondado para centésimos.

smallmoney e money

Opa! Temos 2 tipos de dados específicos para moeda, smallmoney e money, com as seguintes características:

Após definição e carga das tabelas, temos o seguinte trecho da apresentação dos valores monetários e o cálculo do percentual do valor do frete sobre o valor total da venda:

Ao comparar os valores da coluna % do relatório acima com a mesma coluna do relatório anterior (tipo de dados integer), percebemos que os valores são diferentes para algumas linhas. Qual dos 2 relatórios está correto?

Ao consultar a documentação para os tipos de dados encontrei o seguinte aviso:

You can experience rounding errors through truncation, when storing monetary values as money and smallmoney. Avoid using this data type if your money or currency values are used in calculations.

Considerando-se que a parte fracionária da coluna de percentual está com dois algarismos, se fizermos o seguinte cálculo:

616,10 / 20.565,60 * 100

teremos 2,995.779.359.70 como resultado. Ao reduzirmos a apresentação para centésimos (dois algarismos na parte fracionária), se truncarmos teremos 2,99 como valor final mas se arredondarmos teremos 3,00. Parece que a diferença nos dois relatórios foi causada pelo fato de no primeiro relatório ter sido utilizado o arredondamento enquanto que para tipo de dados money ocorre o truncamento.

Em resumo, a diferença está entre arredondar ou truncar.

Curiosidade: segundo o artigo How SQL Server stores data types: money, internamente money e smallmoney são armazenados como valores inteiros.

real

O tipo de dados real possibilita o armazenamento de parte fracionária, inclusive com número variável de algarismos na parte fracionária. Isso, a princípio, significa maior precisão nos cálculos.

Por exemplo, ao dividir 10,50 por 4 temos como resultado o valor 2,625. Se dividirmos os mesmos 10,50 por 5 temos então 2,1 ou ainda, dividindo 10,50 por 8 temos 1,3125.

O número de algarismos da parte fracionária se ajusta automaticamente, sem necessidade de arredondamentos ou truncamentos. Essa característica parece muito útil, indicando ser o tipo de dados real o indicado para armazenar valores monetários, certo?

Repetindo o relatório anterior, mas agora com as colunas Valor_total_venda e Valor_frete declaradas como real, temos

O valor da porcentagem foi arredondamento para centésimos.

Algo curioso neste último relatório é que zeros à direita na parte fracionária não são exibidos. Procurando analisar o conteúdo dos dados da nota fiscal de número 043659, obtém-se o seguinte relatório:

O valor armazenado na coluna Valor_total_item não está correto; por exemplo, o resultado de 6.00 multiplicado por 5,70 é 34,20 mas no relatório acima consta 34,199997 como valor armazenado na tabela. E o erro se repete em alguns dos demais itens. Na tabela em que estão armazenados esses valores, a coluna Valor_unitario está declarada como real e a coluna Qtd_unidades como decimal (5,2).

O que aconteceu? 🤔

Aprofundando na forma como valores são armazenados para o tipo de dados real, temos a informação de que é utilizado a vírgula flutuante, equivalente a float(24). Aliás, há um extenso verbete na wikipedia sobre vírgula flutuante. Mas, como consta na documentação do tipo de dados real, “os dados de vírgula flutuante são aproximados; portanto, nem todos os valores no intervalo de tipo de dados podem ser representados de maneira exata”.

Para mitigar esse erro, em nosso exemplo seria necessário arredondar para centésimos cada valor numérico antes de cada operação que envolva valor do tipo de dados real. Caso contrário pode ocorrer a propagação de erro.

decimal

Este tipo de dados permite o armazenamento de valores reais mas o tamanho da parte fracionária é fixo, definido no momento da declaração da coluna. Ou seja, assim como os valores monetários. A declaração é do tipo

decimal (p, s)

onde

p é precisão; isto é, a quantidade total de algarismos. Seu valor varia de 1 a 38; e

s é a escala; ou seja, a quantidade de algarismos na parte fracionária. Seu valor varia de 0 a p.

O armazenamento depende do valor da precisão:

Avaliação dos tipos de dados numéricos

Ao trabalhar com sistemas financeiros é necessária a exatidão, principalmente quando envolve multiplicação e divisão de valores monetários cujo resultado excede a quantidade de algarismos da parte fracionária. Por exemplo, ao dividir o valor $2.003,11 por 2 obtemos $1.001,555. Entretanto, como temos que armazenar o resultado em duas casas decimais, qual o valor a armazenar: $ 1.001,55 (truncamento) ou $ 1.001,56 (arredondamento)? No caso de truncamento, ao realizar a operação inversa (valor * 2), obtemos $ 2.003,10. Ops! Sumiu 1 centavo…  Já no caso de arredondamento, temos como resultado $ 2.003,12; agora apareceu 1 centavo…

Além disso, no caso de arredondamento qual regra utilizar: a que está implementada na função round() do sql server ou a definida pela norma ABNT NBR 5891?

Independente da escolha, truncamento ou arredondamento, o que se percebe é que a programação do sistema deve prever tais particularidades.

tinyint, smallint, int, bigint. Porque alguém adotaria um tipo de dados inteiro para armazenar valores monetários, que são valores reais? Resolvi pesquisar na web se havia alguma indicação e encontrei a seguinte citação: You can store the amount as either an integral type or a fixed decimal type. The decimal type is easier for some manipulations, the integral for others. (livro Patterns of Enterprise Application Architecture, de Martin Fowler).
Pintou a dúvida: quais manipulações seriam mais eficientes com tipo de dados inteiro?  🤔

smallmoney e money. Se a regra escolhida é arredondamento, os tipos de dados smallmoney e money não podem ser utilizados. Outro fato que desaconselha o uso desses tipo de dados é que eles trabalham com 4 algarismos na parte fracionária (4 casas decimais). Isso pode acarretar propagação de erro em cálculos, caso os resultados tenham que ser sempre centesimais.

Pesquisando na web sobre estes tipo de dados, me deparei com o artigo “Avoid use of the MONEY and SMALLMONEY datatypes”, de Phil Streiff, onde temos a seguinte explicação adicional: While simple addition or subtraction is fine, more complicated calculations that can be done for financial reports can show errors.

real. Este tipo de dados exige atenção especial em todos os cálculos de multiplicação e divisão (principalmente), sempre arredondando o resultado para centesimal. Quem garante que a equipe de desenvolvimento e manutenção terá conhecimento desse cuidado necessário, principalmente ao longo do tempo em que ocorre substituição de programadores?

decimal. É o tipo de dados ideal, caso a opção seja arredondamento nos resultados de divisão e multiplicação.

Material de estudo

Documentação

Tipos de dados

tinyint, smallint, int e bigint

money and smallmoney

decimal

real

Artigos

Arredondamento de números seguindo a norma ABNT NBR 5891  (Dirceu Resende)

Avoid use of the MONEY and SMALLMONEY datatypes  (Phil Streiff)

How SQL Server stores data types: money

Verbetes

Arredondamento

Exatidão e precisão

Truncamento

Vírgula flutuante

 

 

 

 

 

Deixe um comentário

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.