O tipo NUMBER, no ORACLE Database, em C

Recentemente tenho tido a oportunidade de começar a portar um código escrito totalmente usando Oracle Call Interface, sem o uso de qualquer framework. A ideia é criar um daemon que execute processamento veloz (não necessáriamente relacionado com as consultas ao banco de dados)… Eis a primeira dúvida: Quais são os limites do tipo NUMBER, numa DDL (Data Definition Language)), em SQL?

Do manual da Oracle:

The NUMBER datatype stores fixed and floating-point numbers. Numbers of virtually any magnitude can be stored and are guaranteed portable among different systems operating Oracle Database, up to 38 digits of precision.

UAU! 38 digitos de precisão! 38 digitos em decimal, que fique bem claro! Isso é bem maior que o equivalente teórico do tipo double, da especificação IEEE 754, que tem 53 bits de precisão e, por consequência, 16 algarismos decimais:

\displaystyle d_{decimal}\approx\lceil n_{bits}\cdot\log_{10}{2}\rceil

Se n_{bits}=53, para ponto flutuante normalizados, então temos, aproximadamente, os 16 algarismos decimais significativos para o tipo double.

Adicione a essa extrema precisão o esquema de armazenamento interno para NUMBER, que é, também de acordo com os manuais, estruturado como um expoente de 1 byte seguido de uma mantissa de 20 bytes! Ou seja, a precisão binária da parte fracionária é de 161 bits (se tivermos o 1.0, implícito, como no caso de float e double). Estranhamente, isso nos daria uma precisão de 48 algarismos, não apenas 38… Suponho que a Oracle reserve os 10 bits inferiores para arredondamentos (um exagero, mas C’est la vie!).

Nos casos em que NUMBER é um inteiro, e se tiver mais que 18 algarismos, podemos usar mpz_t. No caso de não inteiros, com mais de 16 algarismos, mpf_t. Assim, quando NUMBER tem menos que 19 algarismos podemos usar int64_t (ou o tipo sb8, definido pela OCI), se tiver menos que 10, int32_t. Claro, se tivermos “escala”, com menos de 16 algarismos é seguro usar double e com menos de 8, float.

Como já discuti por esse blog, algumas vezes, não é nada prático usarmos o tipo “extendido” long double em nossos códigos. Em primeiro lugar, a precisão (em algarismos) não é lá grande coisa: 19, o que nos dá uma garantia de precisão se tivermos NUMBER, com casas decimais e precisão de até 18 algarismos… A lentidão das rotinas de ponto flutuante, que serão obrigadas a usar as instruções 80×87, não compensa esse “ganho” de apenas 2 algarismos decimais!

Embora para faixas maiores possamos usar libgmp, a conversão de dados e processamento ficarão um pouco prejudicados:

  1. Não temos como usar o tipo __int128 na OCI;
  2. Para usar a libgmp teremos que obter os dados em formato de string!

O primeiro item está ai justamente porque __int128 suporta 39 algarismos decimais em sua estrutura, para o caso de lidarmos apenas com valores inteiros, mas a OCI não o suporta!… O limite superior de __int128 é 2^{127}-1, ou 170141183460469231731687303715884105727, mas existe um outro problema: Sem alguma extensão específica, a glibc não suporta o tipo __int128 (embora o compilador o suporte)! Lembre-se que o valor pode ter parte fracionária também.

Obter o campo em forma de string não é problema. A OCI faz a conversão automaticamente à partir do momento que o campo é descrito previamente como do tipo SQLT_STR. Lembre-se apenas que o valor terá que caber no buffer da string… Embora a precisão nos diga o número de algarismos, temos que levar em conta que o valor pode ser negativo. Assim, para colocar um NUMBER(12,0) numa string teremos que ter um buffer de, pelo menos, 14 chars (os 12 algarismos, o sinal de ‘-‘, se houver, e o NUL char final). Teremos que adicionar um caracter extra ao buffer para o caso dele possuir parte fracionária, por causa do caracter ‘.’. Assim, um NUMBER(13,2) terá que ser colocado um char buffer[13+3];.

Mas, é claro, essas conversões de NUMBER para string e para mpz_t ou mpf_t (e ao contrário também) são lentas! Mas, infelizmente, se quisermos manter precisão, não tem outro jeito fácil…