Let’s say you have a table that records transactions along with the date of when the transaction took place. Wait, I’m being told that Production.TransactionHistory in AdventureWorks sample database matches this description. So let’s use that for this demonstration.
Here’s the simplified schema of that table.
CREATE TABLE [Production].[TransactionHistory] ( [TransactionID] [INT] IDENTITY(100000, 1) NOT NULL , [ProductID] [INT] NOT NULL , [ReferenceOrderID] [INT] NOT NULL , [ReferenceOrderLineID] [INT] NOT NULL , [TransactionDate] [DATETIME] NOT NULL , [TransactionType] [NCHAR](1) NOT NULL , [Quantity] [INT] NOT NULL , [ActualCost] [MONEY] NOT NULL , [ModifiedDate] [DATETIME] NOT NULL );
Listing 1: Columns in the Production.TransactionHistory table
Some transactions are posted on holidays as well. I can tell by looking at the TransactionDate column.
Now, imagine you’ve a requirement to retrieve all rows from this table, and show next business day if the TransactionDate falls on a holiday.
Calendar Table
There’re several ways to achieve this, but I find using a calendar table to be the most efficient. Use the following script to create the calendar table.
CREATE TABLE dbo.Calendar ( CalendarDate DATE , WeekdayName VARCHAR(10) , IsHoliday BIT ); GO
Listing 2: Create script of a simple Calendar table
This table contains every single date between a desired timeframe. IsHoliday column specifies whether the date is a holiday or not. Obviously you can add additional columns based on your needs.
I use the following script to populate the Calendar table with dates between 01/01/2000 and 12/31/2030.
DECLARE @startdate DATE = '20000101' , @enddate DATE = '20301231'; WITH c AS ( SELECT Num = ROW_NUMBER() OVER ( ORDER BY ( SELECT NULL ) ) - 1 FROM sys.columns c CROSS JOIN sys.columns c1 ), d AS ( SELECT [date] = DATEADD(DAY, Num, @startdate) , Num FROM c WHERE Num >= 0 AND Num <= DATEDIFF(DAY, @startdate, @enddate) ) INSERT dbo.Calendar ( CalendarDate , WeekdayName , IsHoliday ) SELECT [date] , [DayName] = DATENAME(WEEKDAY, [date]) , IsHoliday = CASE WHEN DATENAME(WEEKDAY, [date]) IN ( 'Saturday','Sunday' ) THEN 1 ELSE 0 END FROM d;
Listing 3: Populate the Calendar table
Note that I’m specifying only Saturday and Sunday as the holidays since this is an example. I definitely don’t want to live in such a world. Obviously you’d have more holidays, use an Update statement to mark other holidays.
Baby Steps
09/01/2007 is marked as a holiday in this Calendar table. I can use the following query if I want to get the next business day.
SELECT MIN(c.CalendarDate) AS NextBusinessDay FROM dbo.Calendar AS c WHERE c.CalendarDate >= '09/01/2007' AND c.IsHoliday = 0;
Listing 4: Find the next business day, a simple Select statement
Filter conditions CalendarDate >= ’09/01/2007′ and IsHoliday = 0 ensures that only business days greater or equal to the given date are returned from the table. Min() function is applied on this result set in the Select clause which returns the smallest – first in other words – date.
Table Valued Function and Cross Apply
I can convert the above Select statement to a table valued function as below.
CREATE FUNCTION dbo.ufnGetNextBusinessDay ( @TransactionDate DATE ) RETURNS @retNextBusinessDate TABLE ( NextBusinessDate DATE ) AS BEGIN DECLARE @NextBusinessDate DATE; SELECT @NextBusinessDate = MIN(c.CalendarDate) FROM dbo.Calendar AS c WHERE c.CalendarDate >= @TransactionDate AND c.IsHoliday = 0; INSERT @retNextBusinessDate ( NextBusinessDate ) SELECT @NextBusinessDate; RETURN; END; GO
Listing 5: Table valued function to get next business day in SQL Server
This function accepts TransactionDate as an input parameter and returns a table with just one column (NextBusinessDate). The input parameter is passed to the Where clause replacing the static date 09/01/2007. This is the only change I made to the Select statement, all other lines of code are for the sake of the function syntax.
Lastly I write a Select statement and use Cross Apply to reference the table valued function as shown below.
SELECT th.TransactionID , th.ProductID , th.TransactionDate , th.Quantity , th.ActualCost , n.NextBusinessDate FROM Production.TransactionHistory AS th CROSS APPLY dbo.ufnGetNextBusinessDay(TransactionDate) AS n;
Listing 6: Cross apply and table valued function to get next business day
Each TransactionDate from Production.TransactionHistory is passed to the function. The function returns the NextBusinessDate which is listed in the Select statement.
How Not To Do It
I frequently see people use a scalar user defined function in the Select statement as shown below (see line before From clause).
SELECT th.TransactionID , th.ProductID , th.TransactionDate , th.Quantity , th.ActualCost , dbo.ufnGetNextBusinessDay_scalar(th.TransactionDate) FROM Production.TransactionHistory AS th;
Listing 7: Don’t use scalar UDF is the select statement
This sure gives the same result as using table valued function and Cross Apply, however, scalar UDFs used in the Select statement are the worst in terms of performance. Why? Because scalar UDFs operate one row at a time. They’re just bad and ugly.
Want more proof? See for yourself below. Scalar function is used in the left solution and Table Valued function in the right solution. 199198 ms is 200 seconds and 2276 ms is 2 seconds. Just saying.