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

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))

Related

Sql Server Computed Column Specification [duplicate]

I have this function for a computed column :
CREATE FUNCTION [dbo].[GetAllocatedStartTime](#Year INT, #Week INT)
RETURNS DATETIME
WITH schemabinding
AS BEGIN
RETURN dateadd(week,#Week-(1),dateadd(day,(-1),dateadd(week,datediff(week,(0),CONVERT([varchar](4),#Year,(0))+'-01-01'),(1))))
END
GO
I added the WITH schemabinding in the hope it would make it deterministic so I can persist it. It should be as the two inputs [Week] and [Year] will always yield the same results.
The exact error is :
Computed column 'AllocatedTimeStart' in table 'Tmp_Bookings' cannot be persisted because the column is non-deterministic.
I am using this formula in the column :
([dbo].[GetAllocatedStartTime]([Year],[Week]))
And the column defs :
[Week] [int] NOT NULL,
[Year] [int] NOT NULL,
[AllocatedTimeStart] AS ([dbo].[GetAllocatedStartTime]([Year],[Week])),
Any ideas?
EDIT:
Changed line to :
RETURN dateadd(week,#Week-(1),dateadd(day,(-1),dateadd(week,datediff(week,(0),CONVERT(datetime,CONVERT([varchar](4),#Year,(0))+'0101',112)),(1))))
But now I get an error saying the formula for the column is invalid. Even though the function saves fine.
EDIT 2:
I've shown exactly what I am doing (or atleast I've tried). There is nothing extra really. As it says the previous function (original one) coupled with the formula ref [dbo].AllocatedStartDate(...) to it in the column worked, but was not persisting, it said it was non deterministic. So according to the suggestion I changed the FUNCTION, replacing the conversion part with the new code, so the function now looks like :
FUNCTION [dbo].[GetSTime](#Year INT, #Week INT)
RETURNS DATETIME
WITH schemabinding
AS BEGIN
RETURN dateadd(week,#Week-(1),dateadd(day,(-1),dateadd(week,datediff(week,(0),CONVERT(datetime,CONVERT([varchar](4),#Year,(0))+'0101',112)),(1))))
END
Then I tried the same formula as before in the computed field (([dbo].[GetAllocatedStartTime]([Year],[Week]))) ... and it rejects the formula, says its not valid... which is strange as the formula is the same, so it must be doing some sort of check of the changed function and finding that to be invalid, which is also strange because I did a plain SELECT dbo.GetAllocatedStartTime(2012,13) and it worked...
So yes I am confused, and I've never seen SqlFiddle never mind use it. But really there is nothing more than what I have just said.
CONVERT([varchar](4),#Year,(0))+'-01-01' is being passed to a DATEDIFF call, in a position where a date is expected, forcing an implicit conversion to occur.
From the rules for deterministic functions:
CAST
Deterministic unless used with datetime, smalldatetime, or sql_variant.
CONVERT
Deterministic unless one of these conditions exists:
...
Source or target type is datetime or smalldatetime, the other source or target type is a character string, and a nondeterministic style is specified. To be deterministic, the style parameter must be a constant. Additionally, styles less than or equal to 100 are nondeterministic, except for styles 20 and 21. Styles greater than 100 are deterministic, except for styles 106, 107, 109 and 113.
Well, you're calling neither, but you're relying on an implicit conversion, which I'd expect to act like CAST. Rather than rely on this, I'd switch to using CONVERT and give a deterministic style parameter.
So, I'd do: CONVERT(datetime,CONVERT([varchar](4),#Year,(0))+'0101',112) in its place. Having done so, the function itself becomes deterministic

Can I make SQL Server FORMAT deterministic?

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)

TSQL Datediff from a string

I have a simple query that I am trying to use this select statement on.
I am trying to get the difference in seconds from the date provided (a UNIX timestamp).
select
DATEDIFF(s, '19700101', CAST(CAST('2014-08-27 08:59:56.0000000' AS DATETIME) AS INT))
When I try this, I get the following error :
Conversion failed when converting date and or time from character string.
I figured casting the string as datetime would have resolved that?
First of all, you should try to avoid using shorthands for the datepart, instead of s, use SECOND.
Another good practice when casting a string to a date is to use CONVERT so you can specify the format to use on the conversion (in your case, it would be 121. Finally, for the whole thing to work, you'll need to use less miliseconds (up to 3):
DECLARE #Date VARCHAR(50)
SET #Date = '2014-08-27 08:59:56.0000000'
SELECT DATEDIFF(SECOND,'19700101',CONVERT(DATETIME,LEFT(#Date,23),121))

Cannot persist computed column - not deterministic

I have this function for a computed column :
CREATE FUNCTION [dbo].[GetAllocatedStartTime](#Year INT, #Week INT)
RETURNS DATETIME
WITH schemabinding
AS BEGIN
RETURN dateadd(week,#Week-(1),dateadd(day,(-1),dateadd(week,datediff(week,(0),CONVERT([varchar](4),#Year,(0))+'-01-01'),(1))))
END
GO
I added the WITH schemabinding in the hope it would make it deterministic so I can persist it. It should be as the two inputs [Week] and [Year] will always yield the same results.
The exact error is :
Computed column 'AllocatedTimeStart' in table 'Tmp_Bookings' cannot be persisted because the column is non-deterministic.
I am using this formula in the column :
([dbo].[GetAllocatedStartTime]([Year],[Week]))
And the column defs :
[Week] [int] NOT NULL,
[Year] [int] NOT NULL,
[AllocatedTimeStart] AS ([dbo].[GetAllocatedStartTime]([Year],[Week])),
Any ideas?
EDIT:
Changed line to :
RETURN dateadd(week,#Week-(1),dateadd(day,(-1),dateadd(week,datediff(week,(0),CONVERT(datetime,CONVERT([varchar](4),#Year,(0))+'0101',112)),(1))))
But now I get an error saying the formula for the column is invalid. Even though the function saves fine.
EDIT 2:
I've shown exactly what I am doing (or atleast I've tried). There is nothing extra really. As it says the previous function (original one) coupled with the formula ref [dbo].AllocatedStartDate(...) to it in the column worked, but was not persisting, it said it was non deterministic. So according to the suggestion I changed the FUNCTION, replacing the conversion part with the new code, so the function now looks like :
FUNCTION [dbo].[GetSTime](#Year INT, #Week INT)
RETURNS DATETIME
WITH schemabinding
AS BEGIN
RETURN dateadd(week,#Week-(1),dateadd(day,(-1),dateadd(week,datediff(week,(0),CONVERT(datetime,CONVERT([varchar](4),#Year,(0))+'0101',112)),(1))))
END
Then I tried the same formula as before in the computed field (([dbo].[GetAllocatedStartTime]([Year],[Week]))) ... and it rejects the formula, says its not valid... which is strange as the formula is the same, so it must be doing some sort of check of the changed function and finding that to be invalid, which is also strange because I did a plain SELECT dbo.GetAllocatedStartTime(2012,13) and it worked...
So yes I am confused, and I've never seen SqlFiddle never mind use it. But really there is nothing more than what I have just said.
CONVERT([varchar](4),#Year,(0))+'-01-01' is being passed to a DATEDIFF call, in a position where a date is expected, forcing an implicit conversion to occur.
From the rules for deterministic functions:
CAST
Deterministic unless used with datetime, smalldatetime, or sql_variant.
CONVERT
Deterministic unless one of these conditions exists:
...
Source or target type is datetime or smalldatetime, the other source or target type is a character string, and a nondeterministic style is specified. To be deterministic, the style parameter must be a constant. Additionally, styles less than or equal to 100 are nondeterministic, except for styles 20 and 21. Styles greater than 100 are deterministic, except for styles 106, 107, 109 and 113.
Well, you're calling neither, but you're relying on an implicit conversion, which I'd expect to act like CAST. Rather than rely on this, I'd switch to using CONVERT and give a deterministic style parameter.
So, I'd do: CONVERT(datetime,CONVERT([varchar](4),#Year,(0))+'0101',112) in its place. Having done so, the function itself becomes deterministic

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

Resources