Can I make SQL Server FORMAT deterministic? - sql-server

I want to make a UDF which returns an integer form of YYYYMM so that I can easily partition some things on month. I am trying to assign this function to the value of a PERSISTED computed column.
I currently have the following, which works fine:
CREATE FUNCTION dbo.GetYearMonth(#pDate DATETIME2)
RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
DECLARE #fYear VARCHAR(4) = RIGHT('0000' + CAST(YEAR(#pDate) AS VARCHAR),4)
DECLARE #fMonth VARCHAR(2) = RIGHT('00' + CAST(MONTH(#pDate) AS VARCHAR),2)
RETURN CAST(#fYear + #fMonth AS INT)
END
But I think it's cleaner to use FORMAT instead. I tried this:
CREATE FUNCTION dbo.GetYearMonth(#pDate DATETIME2)
RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
DECLARE #fYear VARCHAR(4) = FORMAT(#pDate,'yyyy', 'en-us')
DECLARE #fMonth VARCHAR(2) = FORMAT(#pDate,'MM', 'en-us')
RETURN CAST(#fYear + #fMonth AS INT)
END
But this function is nondeterministic.
Is there a way to make FORMAT deterministic? Or is there a better way to do this, making the UDF deterministic?

Interesting question so I did some digging and came up with nothing conclusive :)
Starting with Deterministic and Nondeterministic Functions
which doesn't explicitly list FORMAT but states:
All of the aggregate and string built-in functions are deterministic.
and links to String Functions
Again, this page states
All built-in string functions are deterministic. This means they return the same value any time they are called with a specific set of input values.
However, the page on FORMAT is silent on the subject.
FORMAT uses the CLR which doesn't preclude it from being deterministic but the doco is silent as to the actual implementation of FORMAT.
Finally, suspecting this is a bug in the doco (or the code) a quick search of connect reveals nothing.
How about a test case? (Comments welcome on the validity of this codeā€¦)
CREATE FUNCTION WhatAmI(#Number INTEGER)
RETURNS NVARCHAR
WITH SCHEMABINDING
AS
BEGIN
RETURN FORMAT(#Number, 'd')
END
GO
SELECT OBJECTPROPERTY(OBJECT_ID('WhatAmI'),'IsDeterministic')
-----------
0
(1 row(s) affected)
Nup, that's nondeterministic.
So, an interesting exercise but nothing conclusive.
So what did we learn (according to BOL)? If a built-in function is nondeterministic, there's no way of making it so. Some are both depending on the parameter types, and some are supposed to be but aren't.

I'm wondering why you are using a function.
You can use a simple formula:
CONVERT(varchar(6), getdate(), 112)

Microsoft isn't very clear about the format function being able to be determinstic. I couldn't find any info on the subject in its documentation. Being a CLR function i guess the awnser is it is not possible.
An even more simple solution to get an integer formatted the way you want would be:
YEAR(#pDate) * 100 + MONTH(#pDate)

Related

Why is my SQL function non-deterministic, when it shouldn't be?

According to MS docs DATEADD is a deterministic function hence my function below should be deterministic too:
CREATE FUNCTION [dbo].[Epoch2Date] (#i INT)
RETURNS DATETIME WITH SCHEMABINDING
BEGIN
RETURN DATEADD(SECOND,#i,'1970-01-01 00:00:00')
END
But when I check it with SELECT OBJECTPROPERTY(OBJECT_ID('[dbo].[Epoch2Date]'), 'IsDeterministic') it returns 0 (Non-deterministic).
Why it is non-deterministic?
How can I make my function deterministic?
There is a similar question but it uses non-deterministic function CAST which is not the case here.
DATEADD is. Implicitly converting a varchar to a datetime, however, is not. This is especially worse when the format you use is ambiguous for datetime (though at least the value would be the same).
You need to explicitly convert the value with a style:
DATEADD(SECOND,#i,CONVERT(datetime,'19700101',112))

Specifying style on T-SQL convert function causes error

I have a stored procedure which takes in dates in a variety of formats and converts them to datetimes. The following line works just as expected:
RETURN CONVERT(datetime,#vMonth+'/'+#vDay+'/'+#vYear)
However, I want to specify that the date should always be treated as being in US format, as this will return the wrong result if T-SQL is set to use British. If I do this:
RETURN CONVERT(datetime,#vMonth+'/'+#vDay+'/'+#vYear, 101)
Then I start getting the following error (on all values I've tried):
Conversion failed when converting date and/or time from character string.
I've done a search and none of the threads that mention this error seem to be the same problem. I'm confused as to why specifying us_english (101) for a string that's already being converted as us_english causes the procedure to break.
Edit: I've boiled this down to a minimum case that shows where I'm not understanding.
This works:
select CONVERT(datetime,'01/02/03')
This does not:
select CONVERT(datetime,'01/02/03',101)
This works fine for me. No errors.
declare #mydate datetime = getdate()
declare #vMonth varchar(10), #vDay varchar(10), #vYear varchar(10)
set #vMonth = cast(DATEPART(mm, #mydate) as varchar(10))
set #vDay = cast(DATEPART(dd, #mydate) as varchar(10))
set #vYear = cast(DATEPART(YYYY, #mydate) as varchar(10))
select CONVERT(datetime, #vMonth+'/'+#vDay+'/'+#vYear, 101)
Result:
2015-01-09 00:00:00.000
It turns out that when using 101 as the language, SQL Server expects the century to be in XXXX format. To handle 2-digit years correctly, you must use 1 as the language instead.

Convert Time zonoffset value to varchar

Query:
DECLARE #TimeZoneOffset datetimeoffset
SELECT #TimeZoneOffset = Time_Zone_Offset
FROM OFFSET_TABLE WHERE Active=1
Time_Zone_Offset column contains value like -6:00 (only offset)
When I do SELECT #TimeZoneOffset it throws me an error
Conversion failed when converting date and/or time from character string.
I know I am doing something wrong. I may need to CONVERT/CAST but can't get o/p so far.
Any help
To visualize what is happening here, try this:
DECLARE #x VARCHAR;
SET #x = 'abcdefghijklmnop';
SELECT #x;
Result:
----------
a
You have silently lost data from your variable, because you didn't bother declaring a length for your VARCHAR. In your case, I think you are ending up trying to use the string - somewhere, as opposed to the string -6:00.
I'm not sure how a simple SELECT yielded the error you mentioned; I suspect you are using it in some other context you haven't shown. But please try it again once your variable has been declared correctly.
Now I see why, your question wasn't correct - you said you were converting to VARCHAR but you weren't. This is not really unexpected, as -6:00 is not a valid DATETIMEOFFSET value; there is expected to be date and time components as well, otherwise the data type would just be called OFFSET. A valid DATETIMEOFFSET, according to the documentation, is:
DECLARE #d DATETIMEOFFSET = '1998-09-20 7:45:50.71345 -05:00';
So perhaps you have some datetime value and you want to apply the offset, well you can use SWITCHOFFSET() for that. However -6:00 is not a valid value; it needs to be in [+/-]hh:mm format (notice the leading 0 above, which seems to be missing from your sample data). So this would be valid:
DECLARE #datetime DATETIME = GETDATE(), #offset VARCHAR(6) = '-06:00';
SELECT SWITCHOFFSET(#datetime, #offset);
You need to correct the data in your offsets table and you need to change the way you are using the output. Personally, I've found it easier to stay away from DATETIMEOFFSET and SWITCHOFFSET(), especially since they are not DST-aware. I've had much better luck using a calendar table for offsets, storing the offsets in minutes, and using DATEADD to switch between time zones. YMMV.

How to return same type as the argument

In SQL Server, I need to wrap the DATEADD built-in function with another function.
The problem is I need to implement this behaviour:
Return Types
The return data type is the data type of the date argument[...]
For example, if I pass in a datetime as argument, DATEADD returns a datetime. If I pass in a date, DATEADD returns a date.
The following example always returns datetime...
create function add_months(#dt date, #interval int)
returns datetime
as
begin
return DATEADD(month, #interval, #dt)
end
How can I implement this in SQL Server?
(edit)
Context
I'm performing a database migration from informix to SQL Server. The database part is not the issue here, the code is. We have hundreds of programs that must be changed because of the SQL queries embedded in them. This is the main reason I'm trying to avoid to use DATEADD(MONTH, 1, foo). This automatic transformation, while simple in most cases, can be quite difficult in some cases. With a udf I could just replace the name of the informix function and not go into a deeper refactoring.
Overloaded functions aren't possible. I'm trying to think of a way to shoehorn SQL_VARIANT into this but all of the options I can think of lead to unnecessarily disgusting complications. I would make a function that accepts and returns date, and a function that accepts and returns datetime, and call the appropriate one. The ones with lower precision can call the ones with higher so you don't have to replicate code, e.g.
CREATE FUNCTION dbo.add_months_to_datetime
(#dt DATETIME, #interval INT)
RETURNS DATETIME
AS
BEGIN
RETURN DATEADD(MONTH, #interval, #dt);
END
GO
CREATE FUNCTION dbo.add_months_to_date
(#dt DATE, #interval INT)
RETURNS DATE
AS
BEGIN
RETURN dbo.add_months_to_datetime(#dt, #interval);
END
GO
Alternatively, just always use the highest precision (1st function), and worry about whether it's a date or datetime when you present the data. You can do this with an inline convert or wrapping it in another function appropriately.
EDIT
Or better yet, just replace all calls to this function of questionable value with proper inline dateadd calls.
dbo.add_months(foo, 1)
Becomes:
DATEADD(MONTH, 1, foo)
While it's not as automatic as you might like, in addition to maintaining your requirement that the output type remains the same as the input, this will also probably improve performance of some queries, depending on where these are used within the query.
Because of the poor performance of scalar functions in SQL Server, I would just generally avoid this or use Aaron's technique.
However, you can use an abomination like this if you have to have that syntax:
create function dbo.add_months(#dt sql_variant, #interval int)
returns sql_variant
as
begin
IF SQL_VARIANT_PROPERTY(#dt, 'BaseType') = 'datetime'
return DATEADD(month, #interval, CAST(#dt AS DATETIME))
ELSE IF SQL_VARIANT_PROPERTY(#dt, 'BaseType') = 'date'
return DATEADD(month, #interval, CAST(#dt AS DATE))
RETURN null
end
DECLARE #d DATE = '20120705';
DECLARE #dt DATETIME = '20120705 12:30:30';
SELECT dbo.add_months(#d, 1), dbo.add_months(#dt, 1);
You can see in this SQLFiddle that there are now issues getting the data out from the sql_variant into something useful: http://sqlfiddle.com/#!6/f95ba/4

SQL code inside SQL Scalar-value function - want to generalize - optimize

I got this code, I would like to optimize.
I basically can add new columns to "Disp" table later on, and I don't want to come back modify this function.
I cannot use dynamic SQL. Right? Is there anything else that would work in my case?
This is the function:
ALTER FUNCTION [GetDate]
(#hdrnumber INT, #DateColName VARCHAR(50))
RETURNS DATETIME
AS
BEGIN
DECLARE #dt DATETIME
SELECT #dt = CASE
WHEN #DateColName = 'ord_bookdate' THEN [ord_bookdate]
WHEN #DateColName = 'ord_startdate' THEN [ord_startdate]
WHEN #DateColName = 'ord_completiondate' THEN [ord_completiondate]
WHEN #DateColName = 'pack_date_from' THEN [pack_date_from]
WHEN #DateColName = 'pack_date_to' THEN [pack_date_to]
END
FROM [Disp]
WHERE [hdrnumber] = #hdrnumber
RETURN #dt
END
(removed some of the code, because it's a long one, but hopefully what I left in here will make sense to you guys)
how do i use this function?
well it basically looks like this:
insert into tablename (...)
select somedate, [GetDate](somedate, somecolumn)
from sometable
where 1 = 1
Certainly agree with comments provided in previous two answers.
Anyways, you could write following function to get Column names of a given table and then
compare the column names to #DatecolumName to return the value from it..
Create
function [dbo].[ftTableSchema](#TableName varchar(100)) returns table as
return
--Declare #tableName varchar(30); select #TABLENAME='excelInBom'
SELECT ColumnName=Column_Name
,DataType= case data_type
When 'DECIMAL' then 'DECIMAL('+convert(varchar,Numeric_precision)+','+Convert(varchar,Numeric_scale)+')'
When 'NUMERIC' then 'DECIMAL('+convert(varchar,Numeric_precision)+','+Convert(varchar,Numeric_scale)+')'
when 'VARCHAR' then 'VARCHAR('+Convert(varchar,Character_maximum_length)+')'
WHEN 'CHAR' THEN 'CHAR('+Convert(varchar,Character_maximum_length)+')'
ELSE data_type
end
,ColumnOrder=Ordinal_Position,* FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME=#tableName
You either need to use dynamic sql which you can't do within a user-defined function (so you'd need to remove it out of the function), or like you are doing with a CASE statement (which isn't fully generic).
I think I'd personally go with the CASE approach to start with and consider dynamic sql if proves to give better performance (maybe with a larger number of different possible fields).
You would have some maintenance work to do to keep the CASE up to date, but I can't imagine more fields would be added that often?
I think you have a bigger design issue here. How in the world are you using this? If you need to specify the column as a string (??!) then dynamic SQL will be >>SO<< much faster in many key situations. Something like this:
declare #sql NVARCHAR(MAX);
set #sql = 'SELECT ' + #DateColName + ' FROM disp WHERE hdrnumber = #hdrnumber';
EXEC (#sql, #hdrnumber);
(just watch out that someone cant SQL inject into #DateColName).
But a bigger issue is the design smell that's all over this code. The fact that this function exists worries me about how you could possibly be using it. Take a bigger look, and maybe post another, more detailed question about your design in general and I think you could have even more helpful answers.

Resources