Por que “SELECT POWER (10.0, 38.0);” gera um erro de estouro aritmético?

15

Estou atualizando meu IDENTITYscript de verificação de estouro para contabilizar DECIMALe NUMERIC IDENTITYcolunas .

Como parte da verificação, calculo o tamanho do intervalo do tipo de dados para cada IDENTITYcoluna; Eu uso isso para calcular qual porcentagem desse intervalo foi esgotada. Pois DECIMALe NUMERIC o tamanho desse intervalo é2 * 10^p - 2 onde pestá a precisão.

Eu criei um monte de tabelas de teste com DECIMALe NUMERIC IDENTITYcolunas e tentou calcular seus intervalos da seguinte forma:

SELECT POWER(10.0, precision)
FROM sys.columns
WHERE 
       is_identity = 1
   AND type_is_decimal_or_numeric
;

Isso gerou o seguinte erro:

Msg 8115, Level 16, State 6, Line 1
Arithmetic overflow error converting float to data type numeric. 

Eu o reduzi às IDENTITYcolunas do tipo DECIMAL(38, 0)(ou seja, com a precisão máxima), então tentei o POWER()cálculo diretamente nesse valor.

Todas as seguintes consultas

SELECT POWER(10.0, 38.0);
SELECT CONVERT(FLOAT, (POWER(10.0, 38.0)));
SELECT CAST(POWER(10.0, 38.0) AS FLOAT);

também resultou no mesmo erro.

  • Por que o SQL Server tenta converter a saída de POWER(), que é do tipo FLOAT, para NUMERIC(especialmente quando FLOATtem uma precedência mais alta )?
  • Como posso calcular dinamicamente o intervalo de uma DECIMALou NUMERICcoluna para todas as precisões possíveis (incluindo p = 38, é claro)?
Nick Chammas
fonte

Respostas:

18

A partir da POWERdocumentação :

Sintaxe

POWER ( float_expression , y )

Argumentos

float_expression
É uma expressão do tipo float ou de um tipo que pode ser implicitamente convertido em float .

y
É o poder ao qual elevar float_expression . y pode ser uma expressão da categoria de tipo de dados numérico exato ou aproximado, exceto para o tipo de dados de bit .

Tipos de retorno

Retorna o mesmo tipo enviado em float_expression . Por exemplo, se um decimal (2,0) for enviado como float_expression, o resultado retornado será decimal (2,0).


A primeira entrada é convertida implicitamente em, floatse necessário.

O cálculo interno é realizado usando floataritmética pela função CRT (C Runtime Library) padrão pow.

A floatsaída de powé então convertida de volta ao tipo do operando esquerdo (implícito numeric(3,1)quando você usa o valor literal 10.0).

Usar um explícito floatfunciona bem no seu caso:

SELECT POWER(1e1, 38);
SELECT POWER(CAST(10 as float), 38.0);

Um resultado exato para 10 38 não pode ser armazenado em um SQL Server decimal/numericporque exigiria 39 dígitos de precisão (1 seguido por 38 zeros). A precisão máxima é 38.

Martin Smith
fonte
23

Em vez de me intrometer na resposta de Martin, acrescentarei o restante das minhas conclusões POWER()aqui.

Segure sua calcinha.

Preâmbulo

Primeiro, apresento a você A, a documentação do MSDN paraPOWER() :

Sintaxe

POWER ( float_expression , y )

Argumentos

float_expression É uma expressão do tipo float ou de um tipo que pode ser implicitamente convertido em float.

Tipos de retorno

O mesmo que float_expression.

Você pode concluir lendo a última linha que POWER()é o tipo de retorno FLOAT, mas leia novamente. float_expressioné "do tipo float ou de um tipo que pode ser implicitamente convertido em float". Portanto, apesar do nome, float_expressionpode realmente ser um FLOAT, um DECIMALou um INT. Como a saída de POWER()é a mesma de float_expression, também pode ser um desses tipos.

Portanto, temos uma função escalar com tipos de retorno que dependem da entrada. Poderia ser?

Observações

Apresento a você a exibição B, um teste demonstrando que POWER()lança sua saída para diferentes tipos de dados, dependendo de sua entrada .

SELECT 
    POWER(10, 3)             AS int
  , POWER(1000000000000, 3)  AS numeric0     -- one trillion
  , POWER(10.0, 3)           AS numeric1
  , POWER(10.12305, 3)       AS numeric5
  , POWER(1e1, 3)            AS float
INTO power_test;

EXECUTE sp_help power_test;

DROP TABLE power_test;

Os resultados relevantes são:

Column_name    Type      Length    Prec     Scale
-------------------------------------------------
int            int       4         10       0
numeric0       numeric   17        38       0
numeric1       numeric   17        38       1
numeric5       numeric   17        38       5
float          float     8         53       NULL

O que parece estar acontecendo é que POWER()moldes float_expressionpara o menor tipo que se encaixa-lo, não incluindo BIGINT.

Portanto, SELECT POWER(10.0, 38);falha com um erro de estouro porque 10.0é convertido para o NUMERIC(38, 1)qual não é grande o suficiente para conter o resultado de 10 38 . Isso porque 10 38 se expande para levar 39 dígitos antes do decimal, enquanto que NUMERIC(38, 1)pode armazenar 37 dígitos antes do decimal mais um depois dele. Portanto, o valor máximo que NUMERIC(38, 1)pode reter é 10 37 - 0,1.

Armado com esse entendimento, posso invocar outra falha de estouro da seguinte forma.

SELECT POWER(1000000000, 3);    -- one billion

Um bilhão (em oposição ao trilhão de milhões do primeiro exemplo, que é lançado para NUMERIC(38, 0)) é pequeno o suficiente para caber em um INT. Um bilhão levantado para a terceira potência, no entanto, é grande demais INT, daí o erro de transbordamento.

Várias outras funções exibem comportamento semelhante, em que o tipo de saída depende da entrada:

Conclusão

Nesse caso específico, a solução é usar SELECT POWER(1e1, precision).... Isso funcionará para todas as precisões possíveis desde que sejam 1e1lançadas FLOAT, o que pode conter números ridiculamente grandes .

Como essas funções são muito comuns, é importante entender que seus resultados podem ser arredondados ou causar erros de estouro devido ao comportamento deles. Se você espera ou confia em um tipo de dados específico para sua saída, expresse explicitamente a entrada relevante conforme necessário.

Então, crianças, agora que sabem disso, podem sair e prosperar.

Nick Chammas
fonte