Comment SQL Server détermine-t-il la précision / l'échelle?

8

J'exécute SQL Server 2012

SELECT 
   0.15 * 30 / 360,
   0.15 / 360 * 30 

Résultats:

 0.012500, 
 0.012480

Celui-ci est même plus déroutant pour moi:

DECLARE @N INT = 360
DECLARE @I DECIMAL(38,26) = 0.15 * 30 / 360     
DECLARE @C DECIMAL(38,26) = 1000000     

SELECT @C *  @I *  POWER(1 + @I, @N)  / ( POWER(1 + @I, @N) - 1 )
SELECT @C * (@I *  POWER(1 + @I, @N)  / ( POWER(1 + @I, @N) - 1 ) )

La première sélection me donne le résultat correct: 12644.44022 La seconde tronque le résultat: 12644.00000

cpacheco
la source
1
Voir cette réponse: stackoverflow.com/a/51445919/87015 , il contient un exemple de code qui détermine le type de données attendu en fonction du type de données d'entrée et de l'opération.
Salman A

Réponses:

13

La détermination de la précision et de l'échelle résultant des expressions est un nid de rat et je ne pense pas que quiconque comprenne les règles exactes dans chaque scénario, en particulier lors du mélange décimal (ou flottant!) Et int. Voir cette réponse par gbn .

Vous pouvez bien sûr adapter les expressions pour vous donner ce que vous voulez en effectuant des conversions explicites beaucoup plus verbeuses. C'est probablement exagéré mais:

SELECT 
   CONVERT(DECIMAL(15,6), CONVERT(DECIMAL(15,6), 0.15) 
   * CONVERT(DECIMAL(15,6), 30) 
   / CONVERT(DECIMAL(15,6), 360)),
   CONVERT(DECIMAL(15,6), CONVERT(DECIMAL(15,6), 0.15) 
   / CONVERT(DECIMAL(15,6), 360) 
   * CONVERT(DECIMAL(15,6), 30));

Aucun des deux résultats n'est arrondi à tort en raison d'un calcul en virgule flottante cassé ou d'une précision / échelle extrêmement erronée.

0.012500    0.012500
Aaron Bertrand
la source
6

Comme Aaron Bertrand l'a mentionné, les expressions sont très difficiles à prévoir.

Si vous osez y aller, vous pouvez essayer d'obtenir un aperçu en utilisant l'extrait de code suivant:

DECLARE @number SQL_VARIANT
SELECT @number = 0.15 / 360
SELECT @number
SELECT  
    SQL_VARIANT_PROPERTY(@number, 'BaseType') BaseType,
    SQL_VARIANT_PROPERTY(@number, 'MaxLength') MaxLength,
    SQL_VARIANT_PROPERTY(@number, 'Precision') Precision

Voici le résultat:

------------
0.000416

(1 row(s) affected)

BaseType     MaxLength    Precision
------------ ------------ ----------
numeric      5            6

(1 row(s) affected)
Oliver Rahner
la source
3

Malgré les excellentes réponses déjà ajoutées à cette question, il existe un ordre de priorité explicitement défini pour la conversion des types de données dans SQL Server.

Lorsqu'un opérateur combine deux expressions de types de données différents, les règles de priorité des types de données spécifient que le type de données avec la priorité la plus faible est converti en type de données avec la priorité la plus élevée. Si la conversion n'est pas une conversion implicite prise en charge, une erreur est renvoyée. Lorsque les deux expressions d'opérande ont le même type de données, le résultat de l'opération a ce type de données.

SQL Server utilise l'ordre de priorité suivant pour les types de données:

user-defined data types (highest)
sql_variant
xml
datetimeoffset
datetime2
datetime
smalldatetime
date
time
float
real
decimal
money
smallmoney
bigint
int
smallint
tinyint
bit
ntext
text
image
timestamp
uniqueidentifier
nvarchar (including nvarchar(max) )
nchar
varchar (including varchar(max) )
char
varbinary (including varbinary(max) )
binary (lowest)

Ainsi, par exemple, si vous SELECT 0.5 * 1(en multipliant une décimale par un int) vous obtenez un résultat qui est converti en une valeur décimale, car la priorité decimalest plus élevée que le inttype de données.

Voir http://msdn.microsoft.com/en-us/library/ms190309.aspx pour plus de détails.

Cela dit, cela SELECT @C * (@I * POWER(1 + @I, @N) / (POWER(1 + @I, @N) - 1 )); devrait probablement renvoyer une valeur décimale, car pratiquement toutes les entrées sont décimales. Fait intéressant, vous pouvez forcer un résultat correct en le modifiant SELECTpour:

DECLARE @N INT = 360;
DECLARE @I DECIMAL(38,26) = 0.15 * 30 / 360;
DECLARE @C DECIMAL(38,26) = 1000000;

SELECT @C *  @I *  POWER(1 + @I, @N)  / (POWER(1 + @I, @N) - 1);
SELECT @C * (@I *  POWER(1 + @I, @N)  / (POWER(1E0 + @I, @N) - 1));

Cela renvoie:

entrez la description de l'image ici

Je n'arrive pas à expliquer en quoi cela fait une différence, bien que ce soit clairement le cas . Ma supposition est que 1E0(un flottant explicite) dans la POWER(fonction force SQL Server à faire un choix différent sur les types de sortie pour la POWERfonction. Si ma supposition est correcte, cela indiquerait un bogue possible dans la POWERfonction, puisque la documentation indique que la première entrée de POWER()est un flottant, ou un nombre qui peut être implicitement converti en flottant.

Max Vernon
la source
2
Un lien vers la précision, l'échelle et la longueur est également approprié.
ypercubeᵀᴹ
2

Connaissez-vous la SELECT .. INTO syntaxe ? C'est une astuce utile pour déconstruire des situations comme celle-ci car elle crée une table à la volée avec juste les bons types de données pour la SELECTliste donnée .

Vous pouvez diviser votre calcul en ses étapes constitutives, en appliquant les règles de priorité des serveurs SQL au fur et à mesure, pour voir comment la définition change. Voici à quoi ressemblerait votre premier exemple:

use tempdb;

SELECT
   0.15             as a,
   0.15 * 30        as b,
   0.15 * 30 / 360  as c
into #Step1;

select * from #Step1;

select
    c.name,
    t.name,
    c.precision,
    c.scale
from sys.columns as c
inner join sys.types as t
    on t.system_type_id = c.system_type_id
where object_id = object_id('#Step1');

drop table #Step1;

Voici la sortie:

name    name        precision   scale
a       numeric     2           2
b       numeric     5           2
c       numeric     9           6
Michael Green
la source
1
Cela n'aide pas toujours cependant. Les deux (1+1)et 2sont de type intmais cette question a un exemple où ils finissent par produire un résultat typé différemment.
Martin Smith