TSQL decimal precision issues when dividing - tsql

I am having an issue with dividing numbers in TSQL. I have read the Precision, scale, and length (Transact-SQL) page but still I cannot figure out why the following issue occurs:
PRINT 1.4259276982890801000000/0.4259276982890801000000
-- Result 3.347816317222208
DECLARE #VAR1 DECIMAL(30,22) = 1.4259276982890801000000
DECLARE #VAR2 DECIMAL(30,22) = 0.4259276982890801000000
PRINT #VAR1/#VAR2
-- Result 3.34781631
question
So why do I see a different precision in those cases while the numbers are exactly the same?
How can I fix it?

The actual precision and scale of the literals in the first PRINT statement are decimal(23,22) and decimal(22,22). Per the documentation, the result type is decimal(38,15). You can observe this with sp_describe_first_result_set:
EXEC sp_describe_first_result_set N'SELECT 1.4259276982890801000000;';
EXEC sp_describe_first_result_set N'SELECT 0.4259276982890801000000;';
EXEC sp_describe_first_result_set N'SELECT 1.4259276982890801000000/0.4259276982890801000000;';
The variables are both decimal(30,22) so the result is decimal(38,8):
EXEC sp_describe_first_result_set N'
DECLARE #VAR1 DECIMAL(30,22) = 1.4259276982890801000000;
DECLARE #VAR2 DECIMAL(30,22) = 0.4259276982890801000000;
SELECT #VAR1/#VAR2;';
You can fix it by specifying a different precision and scale of the variables:
DECLARE #VAR1 DECIMAL(23,22) = 1.4259276982890801000000;
DECLARE #VAR2 DECIMAL(22,22) = 0.4259276982890801000000;
SELECT #VAR1/#VAR2;

Related

Subtracting in dynamic SQL

I was updating a stored procedure to change the reporting time to clients timezone. The stored procedure is comprised of a dynamic SQL, which contains the time #timeoffset parameter of smallint data type.
DECLARE #sql VARCHAR(MAX) =
N'SELECT DISTINCT cl.ClientId,
CASE WHEN DATEADD(HOUR,'+CONVERT(CHAR(2),#timeoffset)+', x.changedate) >= '''+ CONVERT(varchar, #start_date) +'''
AND DATEADD(HOUR,'+CONVERT(CHAR(2),#timeoffset)+', x.changedate) < '''+ CONVERT(varchar, #end_date_plus1) +'''
THEN DATEADD(HOUR,'+CONVERT(CHAR(2),#timeoffset)+',x.changedate)'
To change the time to the clients' time zone, I need to subtract the #timeoffset. Making it negative doesn't change the output.
Trying to add the (-) before the conversion, would raises an error as subtraction operator is invalid for varchar. Writing it
without the conversion raises an error
'Conversion failed when converting the nvarchar value to data type smallint.
Can anyone assist me with this please? Thanks.
You could try passing the negative number as another variable:
DECLARE #neg INT = -1;
DECLARE #sql VARCHAR(MAX) =
N'SELECT DISTINCT cl.ClientId,
CASE WHEN DATEADD(HOUR,'+CONVERT(CHAR(2),#timeoffset)*#neg+', x.changedate) >= '''+
CONVERT(varchar, #start_date) +'''
AND DATEADD(HOUR,'+CONVERT(CHAR(2),#timeoffset)*#neg+', x.changedate) < '''+
CONVERT(varchar, #end_date_plus1) +'''
THEN DATEADD(HOUR,'+CONVERT(CHAR(2),#timeoffset)*#neg+',x.changedate)'
I've noticed a few problems in the code you've posted:
You are declaring #Sql as varchar(max), but you are using the N prefix before setting it's value. You only need that when dealing with unicode data (nchar ,nvarcar). This is critical but you should be aware of this.
You are using convert without specifying the style parameter. It's not that bad when converting ints to strings, but it can cause unexpected behaviors when dealing with dates. Whenever you need to use string representation for date/datetime values, you should always use the ISO8601 standard, since it's guaranteed that Sql server will always convert it to date correctly, regardless of local settings. To convert a datetime value to ISO8061 standard, use style 126 in the convert statement.
You are using varchar without specifying the length. This is a bad habit since SQL Server have different default values to the length depending on context. It's 1 when declaring a variable, but 30 when used in cast and convert.
I've done some changes to your code, including changing the char(2) you've used to varchar(11) (because 11 chars will contain even the minimum value of the int data type which is -2147483648) for the #timeoffset parameter, and had no problems with it being negative.
Here is my test:
DECLARE #timeOffset int = -10,
#start_date datetime = getdate(),
#sql nvarchar(max)
SET #sql =
N'SELECT '''+ convert(char(23), #start_date, 126) +''' As GetDate,
DATEADD(HOUR, '+ CAST(#timeOffset as varchar(11)) +', '''+ convert(char(23), #start_date, 126) +''') As DateAdd';
SELECT #Sql
EXEC(#sql)
Results:
SELECT '2018-11-08T20:33:31.670' As GetDate,
DATEADD(HOUR, -10, '2018-11-08T20:33:31.670') As DateAdd
GetDate DateAdd
2018-11-08T20:33:31.670 08.11.2018 10:33:3

Dynamic SQL Not Converting VARCHAR To INT (shouldn't anyway)

I'm receiving an error:
Conversion failed when converting the varchar value 'INSERT INTO TableRowCount (IntFieldID, DecimalField) SELECT 'to data type int"
Using the following code:
DECLARE #start INT -- #start is an INT
SET #start = 1 -- INT
DECLARE #sql NVARCHAR(MAX)
SET #sql = 'INSERT INTO TableRowCount (IntFieldID, DecimalField)
SELECT ' + #start +', COUNT(*)
FROM dbo.somewhere' -- location is irrelevant
EXECUTE(#sql) -- this is where it fails
If I remove IntFieldID and the #start, it will work with an insert (though it defeats the purpose). I've tried including a SELECT CAST(' + #start + ' AS INT), which seems a little redundant since #start is an INT already (casting an INT as an INT), but that doesn't work either. I also tried beginning with an N' DYNAMIC-SQL, which didn't work, I tried using three ''' around everything (didnt' work), and in a few places that I read online, responses suggested putting the variable in the string, which generated the error:
Must declare scalar variable #start
(no surprise, as that didn't sound correct).
A better way than trying to concatenate an integer is to pass it in as a strongly-typed parameter:
DECLARE #start INT = 1;
DECLARE #sql NVARCHAR(MAX) = N'INSERT ...
SELECT #start, COUNT(*) FROM ' + #conn;
EXEC sp_executesql #sql, N'#start INT', #start;
You need to convert your #Start to a varchar.
DECLARE #sql NVARCHAR(MAX)
SET #sql = 'INSERT INTO TableRowCount (IntFieldID, DecimalField)
SELECT ' + CAST(#start as nvarchar(20)) +', COUNT(*)
FROM ' + #conn
SQL Server implicitly converts between datatypes on concatenation or addition based on some fairly complex criteria. Suffice to say if you try to combine an int and a string it will always attempt to convert the string to an int unless you tell it otherwise explicitly.
Below is a conversion chart for your reference from MSDN.

How to test tsql variable contents as expression [duplicate]

This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
How to execute mathematical expression stored in a varchar variable
Using pure TSQL, is it possible to evaluate an expression defined to a variable?
Here is a simple example, its only for proof in concept:
DECLARE #TestValue AS VARCHAR (2) = '7';
DECLARE #myRule AS VARCHAR (100) = 'case when (#val#>0 and #val#<10) then 1 else 0 end';
SET #myRule = replace(#myRule, '#val#', #TestValue);
PRINT #myRule;
-- case when (7>0 and 7<10) then 1 else 0 end
--How to evaluate the expression in #myRule for True/False?
--Based on How to execute mathematical expression stored in a varchar variable
DECLARE #out AS BIT, #sql AS NVARCHAR (4000);
SET #sql = N'SELECT #out = ' + #myRule;
EXECUTE sp_executesql #sql, N'#out bit OUTPUT', #out OUTPUT;
PRINT 'sp_executesql=' + CAST (#out AS VARCHAR (10));
--sp_executesql=1
I've tested this approach and it seems to be sound. Not sure if there are performance considerations, but for now I'll mark as answered.

T-SQL VARCHAR(MAX) Truncated

DECLARE #str VARCHAR (MAX);
SELECT #str = COALESCE(#str + CHAR(10), '') +
'EXECUTE CreateDeno ' + CAST(ID AS VARCHAR)
FROM GL_To_Batch_Details
WHERE TYPE = 'C' AND
Deno_ID IS NULL;
--PRINT #str;--SELECT #str;
**EXEC(#str);**
EDITED
Does EXECUTE statement truncate strings to 8,000 chars like PRINT? How can I execute a dynamic SQL statement having more than 8,000 chars?
Any suggestion would be warmly appreciated.
PRINT is limited to 8k in output.
There is also an 8k limit in SSMS results pane.
Go to
tools -> options -> query results
to see the options.
To verify the length of the actual data, check:
SELECT LEN(#str)
When concatenating strings and the result is of type VARCHAR(MAX) and is over 8000 characters, at least one parameter and/or element being used in the concatenation need to be of the VARCHAR(MAX) type otherwise truncation will occur in the resultant string and will not be executable in an EXEC statement.
Example:
DECLARE #sql AS VARCHAR(MAX);
/* DECLARE #someItem AS VARCHAR(100); -- WILL CAUSE TRUNCATION WHEN #sql HAS LEN > 8000 */
DECLARE #someItem AS VARCHAR(MAX); -- All string variables need to be VARCHAR(MAX) when concatenating to another VARCHAR(MAX)
SET #someItem = 'Just assume the resulting #sql variable goes over 8000 characters...';
SET #sql = 'SELECT Something FROM Somewhere WHERE SomeField = ''' + #someItem + '''';
EXEC (#sql);
--PRINT #sql;
More information on MSDN.
"If the result of the concatenation of strings exceeds the limit of
8,000 bytes, the result is truncated. However, if at least one of the
strings concatenated is a large value type, truncation does not
occur."
The default length of a varchar is 30 characters:
CAST (ID AS VARCHAR)
Is it possible that id is longer than 30 characters?
The PRINT command is certainly limited to 8000 chars, irrespective of the length of the output (or whether it is varchar(max)). To work around this you need to output the string in chunks of <8000 chars
Update: In answer to your edit, exec doesn't limit the string length. I've put together the following example to show this:
DECLARE #str VARCHAR (MAX);
;WITH CTE_Count AS
(
select counter = 1
union all
select counter = counter+1
from CTE_Count
Where counter < 2000
)
SELECT
#str=COALESCE(#str + CHAR (10) ,
'' ) + 'select value=' + CAST (counter AS VARCHAR)
from
CTE_Count
Option (MAXRECURSION 0)
PRINT len(#str);--SELECT #str;
exec (#str)
Running this prints the length as 34892 chars, and all 2000 execute statements do run (be warned, it may take a few mins!)
It happens when you concatenate literals if one is not a varchar(max) the result ill be "implicit casted" to varchar(8000).
To generate a literal varchar(max) all parts must be varchar(max).
Note: It happened to me doing updates on varchar(max) columns, never tested with the EXEC command.
Also as noted in previous answers the print command holds a limit but you can try selecting that variable instead of printing it. (also ther's a limit on that select length you can configure on MS-SMS)
I also wanted to see what I was sending to Exec, and was confused by the PRINT limit. Had to write a proc to print in chunks.
CREATE PROCEDURE [dbo].[KFX_PrintVarcharMax]
#strMax varchar(max)
AS
BEGIN
SET NOCOUNT ON;
DECLARE
#index int = 0,
#start int = 1,
#blkSize int = 2000;
WHILE #Start < LEN(#strMax)
BEGIN
IF #start + #blkSize >= LEN(#strMax)
BEGIN
-- If remainder is less than blocksize print the remainder, and exit.
PRINT SUBSTRING(#strMax, #start, #blkSize)
BREAK;
END
-- Else find the next terminator (beyond the blksize)
SET #index = CHARINDEX(CHAR(10), #strMax, #start + #blkSize);
if #index >= #start
BEGIN
PRINT SubString(#strMax, #start, #index - #start + 1)
SET #start = #index + 1;
SET #blkSize = CASE WHEN #start + 2000 < LEN(#strMax) THEN 2000
ELSE LEN(#strMax) - #start + 1 END
END
ELSE
BEGIN
-- No char(10) found. Just print the rest.
PRINT SUBSTRING(#strMax, #start, LEN(#strMax))
BREAK;
END
END
END

TSQL How do you output PRINT in a user defined function?

Basically I want to use PRINT statement inside a user defined function to aide my debugging.
However I'm getting the following error;
Invalid use of side-effecting or time-dependent operator in 'PRINT'
within a function.
Can this not be done?
Anyway to aid my user defined function debugging?
Tip:
generate error.
declare #Day int, #Config_Node varchar(50)
set #Config_Node = 'value to trace'
set #Day = #Config_Node
You will get this message:
Conversion failed when converting the varchar value 'value to trace'
to data type int.
No, sorry. User-defined functions in SQL Server are really limited, because of a requirement that they be deterministic. No way round it, as far as I know.
Have you tried debugging the SQL code with Visual Studio?
I got around this by temporarily rewriting my function to something like this:
IF OBJECT_ID ('[dbo].[fx_dosomething]', 'TF') IS NOT NULL
drop function [dbo].[fx_dosomething];
GO
create FUNCTION dbo.fx_dosomething ( #x numeric )
returns #t table (debug varchar(100), x2 numeric)
as
begin
declare #debug varchar(100)
set #debug = 'printme';
declare #x2 numeric
set #x2 = 0.123456;
insert into #t values (#debug, #x2)
return
end
go
select * from fx_dosomething(0.1)
I have tended in the past to work on my functions in two stages. The first stage would be to treat them as fairly normal SQL queries and make sure that I am getting the right results out of it. After I am confident that it is performing as desired, then I would convert it into a UDF.
Use extended procedure xp_cmdshell to run a shell command. I used it to print output to a file:
exec xp_cmdshell 'echo "mytextoutput" >> c:\debuginfo.txt'
This creates the file debuginfo.txt if it does not exist. Then it adds the text "mytextoutput" (without quotation marks) to the file. Any call to the function will write an additional line.
You may need to enable this db-server property first (default = disabled), which I realize may not be to the liking of dba's for production environments though.
No, you can not.
You can call a function from a stored procedure and debug a stored procedure (this will step into the function)
On my opinion, whenever I want to print or debug a function. I will copy the content of it to run as a normal SQL script. For example
My function:
create or alter function func_do_something_with_string(#input nvarchar(max)) returns nvarchar(max)
as begin
-- some function logic content
declare #result nvarchar(max)
set #result = substring(#input , 1, 10)
-- or do something else
return #result
end
Then I just copy and run this out of the function to debug
declare #input nvarchar(max) = 'Some string'
-- some function logic content
declare #result nvarchar(max)
set #result = substring(#input , 1, 10)
-- this line is added to check while debugging
print #result
-- or do something else
-- print the final result
print #result
You can try returning the variable you wish to inspect.
E.g. I have this function:
--Contencates seperate date and time strings and converts to a datetime. Date should be in format 25.03.2012. Time as 9:18:25.
ALTER FUNCTION [dbo].[ufn_GetDateTime] (#date nvarchar(11), #time nvarchar(11))
RETURNS datetime
AS
BEGIN
--select dbo.ufn_GetDateTime('25.03.2012.', '9:18:25')
declare #datetime datetime
declare #day_part nvarchar(3)
declare #month_part nvarchar(3)
declare #year_part nvarchar(5)
declare #point_ix int
set #point_ix = charindex('.', #date)
set #day_part = substring(#date, 0, #point_ix)
set #date = substring(#date, #point_ix, len(#date) - #point_ix)
set #point_ix = charindex('.', #date)
set #month_part = substring(#date, 0, #point_ix)
set #date = substring(#date, #point_ix, len(#date) - #point_ix)
set #point_ix = charindex('.', #date)
set #year_part = substring(#date, 0, #point_ix)
set #datetime = #month_part + #day_part + #year_part + ' ' + #time
return #datetime
END
When I run it.. I get:
Msg 241, Level 16, State 1, Line 1
Conversion failed when converting date and/or time from character string.
Arghh!!
So, what do I do?
ALTER FUNCTION [dbo].[ufn_GetDateTime] (#date nvarchar(11), #time nvarchar(11))
RETURNS nvarchar(22)
AS
BEGIN
--select dbo.ufn_GetDateTime('25.03.2012.', '9:18:25')
declare #day_part nvarchar(3)
declare #point_ix int
set #point_ix = charindex('.', #date)
set #day_part = substring(#date, 0, #point_ix)
return #day_part
END
And I get '25'. So, I am off by one and so I change to..
set #day_part = substring(#date, 0, #point_ix + 1)
Voila! Now it works :)