Why does DECIMAL behave like FLOAT? - sql-server

Even though DECIMAL is an exact numeric type (unlike FLOAT, which is approximate), it behaves rather strangely in the following example:
DECLARE #DECIMAL_VALUE1 DECIMAL(20,9) = 504.70 / 0.151562
DECLARE #DECIMAL_VALUE2 DECIMAL(20,0) = 504.70 / 0.151562
DECLARE #INTEGER_VALUE INT = 504.70 / 0.151562
SELECT
#DECIMAL_VALUE1 AS DECIMAL_VALUE1, -- 3329.990366978
#DECIMAL_VALUE2 AS DECIMAL_VALUE2, -- 3330
#INTEGER_VALUE AS INTEGER_VALUE -- 3329
A value other than 3329 causes a bug in our application. Making the variable type an INTEGER solved our issue, but I cannot get my head around as to why it was caused in the first place.

You asked, "Why it was caused in the first place":
To know why you need to understand the nature of each datatype and how it operates within SQL Server.
Integer math truncates decimals (no rounding, same as "FLOOR" function (which is why you get 3329)).
Decimal with 0 places rounds (which is why you get 3330 as 3329.99 rounds up)
Decimal with precision/scale rounds to Nth scale (which is why you get 3329.990366978...).
So this isn't unexpected behavior, it's expected given the datatypes involved. It just may have been unanticipated behavior. The nuances of each datatype can be problematic until one runs into them.
I'll choose to ignore the float comment as it is not germane to the question.

Related

Convert from varchar to decimal without precision loss

I'm working with a table that contains numeric data with decimals stored as VARCHAR. These numbers have an unpredictable number of decimals (up to 10 I believe) but they can also come in scientific notation ('1231E-03')
I've been trying different workarounds to deal with them (like what is explained here https://stackoverflow.com/a/18452603/5866637)
I have no issues converting regular decimal numbers, but when dealing with the exponential notation I'm unable to get rid of the floating point precision issues.
DECLARE #Value VARCHAR(MAX) = '123456789.123e-5'
SELECT CONVERT(Decimal(38,18), CONVERT(float, #Value))
Will be resolved as
1234.567891229999986535
Is it possible to do any transformation so I can convert the number above to the decimal
1234.567891230000000000
I've tried concatenating a bunch of zeroes to the number but when I have to do the casting to float, it eats all those extra zeroes.
The other thing I've tried is to reduce the precision of my decimal values. If I use Decimal(38,13) I get the expected value but, could that cause issues? If I know that I will never get a value in the database with more than 13 decimal points, is it safe to use convert to (38,13) or shall I pursue a different workaround?
Any help would be appreciated and sorry if this is considered duplicate, but the closest match I found is the link above and it was formulated 6 years ago.
You can use ROUND
DECLARE #Value VARCHAR(MAX) = '123456789.123e-5'
SELECT round(CONVERT(Decimal(38,18), CONVERT(float, #Value)), 8)

dividing a decimal type in SQL Server results in unnecessary trailing zeros

I was performing some simple financial calculations in SQL Server when I discovered some odd behavior. I was trying to convert a string of numbers to a decimal type. While the string did not contain a decimal point, I knew from my specifications that the last 3 positions in the string were supposed to be behind the decimal point.
My first approach was flawed, but went something like this:
select convert(decimal(11,3),89456123/1000) as TotalUnits
This resulted in 89456.000. Performing the division before the cast resulted in the decimal parts being truncated.
So I moved the division operation outside the cast, like this:
select convert(decimal(11,3),89456123)/1000 as TotalUnits
This resulted in an explosion of positions after the decimal point. It returned 89456.12300000
According to my decimal specification, I wanted 11 digits, with 3 of them behind the decimal point. Now I have 13 total digits, with 8 behind the decimal. What happened?
To get what I want, I guess I have to double cast, like this:
select convert(decimal(11,3), convert(decimal(11,3),89456123)/1000)
which gives 89456.123.
It turns out no matter what I divide by, the resulting decimal point explosion is the same. Is the division converting the datatype into a double or something?
My question is this:
Why is this happening, and is there a more elegant way to compensate for it, instead of double-casting to decimal.
EDIT
I found this similar question on SO, but it looks like they are again double-casting.
SQL server does integer arithmetic, to force it to use numeric, you can multiply it by 1.0
No need of using convert twice. This gives 89456.123 with out double convert.
select convert(decimal(11,3),89456123*1.0/1000) as TotalUnits
Why does convert(decimal(11,3),89456123)/1000 end up with 6 decimal places? The rules demand it. numeric division has rather complicated rules about the resulting type.
When you say 1.0 you end up with a numeric with the least scale factors possible to represent this value:
SELECT SQL_VARIANT_PROPERTY(1.11, 'BaseType')
SELECT SQL_VARIANT_PROPERTY(1.11, 'Precision')
SELECT SQL_VARIANT_PROPERTY(1.11, 'Scale')
SELECT SQL_VARIANT_PROPERTY(1.11, 'TotalBytes')
What should you do? I think there is no really elegant solution because of the complicated rules. Any solution I can think of involves rather crazy type inference of intermediate results. I recommend pretty much the same solution that RADAR already gave:
select convert(decimal(11,3), convert(decimal(11, 3), 89456123)/1000) as TotalUnits
The main difference is that I think the *1.0 "trick" used as a short hand for a cast is obfuscating the meaning of the code. If you happen to like it feel free to use it, though.
select convert(decimal(11,3),89456123/CONVERT(decimal(11,3),1000))

SQL Server : SUM() weird behaviour

I run this example in SQL Server Management Studio:
SELECT CONVERT(REAL, -2101.12) n INTO #t
SELECT * FROM #t
SELECT SUM(n) FROM #t
The first SELECT creates a temp table #t with 1 column n of type real, and it puts 1 row in it with the value -2101.12.
The second SELECT confirms that the table is created with the intended content and the result is:
n
---------
-2101.12
The third SELECT sums the only number that is there, but the result is:
-2101.1201171875
So the question is: Where the 0.0001171875 comes from?
EDIT: I know the lack of precision for the real and float data types, unfortunately I cannot change the database schema because of this. What surprise me though, is that I would expect to see also the extra decimals in the second select since it is supposed to be stored with that lack of precision. Since it does not happens on the second select, then why the sum function picks it up?
You've just discovered real (aka floating point) data is approximate.
Use decimal datatype instead.
The FLOAT and REAL data types are known as approximate data types. The behavior of FLOAT and REAL follows the IEEE 754 specification on approximate numeric data types.
Approximate numeric data types do not store the exact values specified for many numbers; They store an extremely close approximation of the value. For many applications, the tiny difference between the specified value and the stored approximation is not noticeable. At times, though, the difference becomes noticeable. Because of the approximate nature of the FLOAT and REAL data types, do not use these data types when exact numeric behavior is required, such as in financial applications, in operations involving rounding, or in equality checks. Instead, use the integer, decimal, money, or smallmoney data types.
Avoid using FLOAT or REAL columns in WHERE clause search conditions, especially with the = or <> operators. It is best to limit FLOAT and REAL columns with > or < comparisons.
Source of above statement

Rounding issue with SQL Server and REAL datatype

I'm seeing some strange behavior when rounding in SQL Server 2008. Given the following code:
DECLARE #Value REAL
SELECT #Value = .35
SELECT ROUND(#Value, 1)
I would expect the value to be .4, however it outputs .3. I must assume this is because the value stored is actually less than .35, something like .34999999999999. Is this the case, or am I doing something wrong? Is there a way to ensure this behaves as expected, at least from the visible value?
When you are using floating-point values like REAL and FLOAT (same thing), the SQL Server ROUND() function follows IEEE Standard 754 and uses the "round up" algorithm.
But that means different things for different floating-point values. Some ".5" values end up getting stored as an approximation of ".49999999999", others as ".500000001", etc. It rounds up the value that is actually stored, not the value you gave it to begin with.
http://msdn.microsoft.com/en-us/library/ms187912.aspx
If exact decimal math matters to you, use DECIMAL, not FLOAT or REAL.
It's too bad the data is stored as REAL, but not all hope is lost. Convert the REAL to DECIMAL(10,2) before rounding it. That way the 0.3499999999999 (or whatever inaccurate value is being stored) will be rounded to .35, and then you'll round that to 0.4. You can even convert he result to DECIMAL(10,1) if you want it to be displayed as 0.4:
DECLARE #Value REAL
SELECT #Value = .35
SELECT CONVERT(DECIMAL(10,1), ROUND(CONVERT(DECIMAL(10,2), #Value), 1))

"Round half up" on floating point values

We are stuck with a database that (unfortunately) uses floats instead of decimal values. This makes rounding a bit difficult. Consider the following example (SQL Server T-SQL):
SELECT ROUND(6.925e0, 2) --> returns 6.92
ROUND does round half up, but since floating point numbers cannot accurately represent decimal numbers, the "wrong" result (from the point of view of the end-user) is displayed. I understand why this happens.
I already came up with two possible solutions (both returning a float, which is, unfortunately, also a requirement):
Convert to a decimal data type before rounding: SELECT CONVERT(float, ROUND(CONVERT(decimal(29,14), 6.925e0), 2))
Multiply until the third digit is on the left-hand side of the decimal point (i.e. accurately represented), and then do the rounding: SELECT ROUND(6.925e0 * 1000, -1) / 1000
Which one should I choose? Is there some better solution? (Unfortunately, we cannot change the field types in the database due to some legacy applications accessing the same DB.)
Is there a well-established best practice solution for this (common?) problem?
(Obviously, the common technique "rounding twice" will not help here since 6.925 is already rounded to three decimal places -- as far as this is possible in a float.)
Your first solution seems safer, and also seems like a conceptually closer fit to the problem: convert as soon as possible from float to decimal, do all relevant calculations within the decimal type, and then do a last minute conversion back to float before writing to the DB.
Edit: You'll likely still need to do an extra round (e.g. to 3 decimal places, or whatever's appropriate for your application) immediately after retrieving the float value and converting to decimal, to make sure that you end up with the decimal value that was actually intended. 6.925e0 converted to decimal would again be likely (assuming that the decimal format has > 16 digits of precision) to give something that's very close to, but not exactly equal to, 6.925; an extra round would take care of this.
The second solution doesn't look reliable to me: what if the stored value for 6.925e0 happens to be, due to the usual binary floating-point issues, a tiny amount too small? Then after multiplication by 1000, the result may still be a touch under 6925, so that the rounding step rounds down instead of up. If you know your value always has at most 3 digits after the point, you could fix this by doing an extra round after multiplying by 1000, something like ROUND(ROUND(x * 1000, 0), -1).
(Disclaimer: while I have plenty of experience dealing with float and decimal issues in other contexts, I know next to nothing about SQL.)
Old question, but I am surprised that the normal practice is not mentioned here, so I just add it.
Normally, you would add a small amount that you know is much smaller than the accuracy of the numbers you are working with, e.g. like this:
SELECT ROUND(6.925e0 + 1e-7, 2)
Of course the added amount must be larger than the precision of the floating point type that is used.
Use an arbitrary-precision format such as DECIMAL. That way you can leave it to the language to get it right (or wrong as the case may be).
I managed to round the float column correctly using the following command:
SELECT CONVERT(float, ROUND(ROUND(CONVERT(decimal(38,14),float_column_name),3),2))

Resources