Wednesday, April 25, 2012

SQL Server : User Defined Functions


The ability to create a user-defined function (UDF) is a new feature added to SQL Server 2000. Developers have been asking Microsoft to add this feature for several versions of the product, so let's take a quick look at how to create a few simple UDFs to see if they can help you in your programming endeavors.

Creating a Simple UDF

A user-defined function is a database object that encapsulates one or more Transact-SQL statements for reuse. This definition is similar to the one for stored procedures, but there are many important differences between user-defined functions and stored procedures—the most pronounced being what types of data they can return. Let's create one so you can see how easy they are to create and reference.

The following statement shows how to create a function that accepts two input parameters, sums them together and then returns the sum to the calling statement.

CREATE FUNCTION fx_SumTwoValues  ( @Val1 int, @Val2 int )  RETURNS int  AS  BEGIN    RETURN (@Val1+@Val2)  END

The structure of the CREATE FUNCTION statement is fairly straightforward. You provide an object name (fx_SumTwoValues), input parameters (@Val1 and @Val2), the type of data the function will return () and the statement(s) the function executes are located between the BEGIN…END block. The following SELECT statement calls the function. Note that the two-part name (owner.object_name) is required when calling this function.

SELECT dbo.fx_SumTwoValues(1,2) AS SumOfTwoValues    SumOfTwoValues  --------------  3

When the SELECT is executed, the input parameters 1 and 2 are added together and the sum 3 is returned. You can use any values that either are, or can be, implicitly converted to an int data type for the input parameters. Keep in mind, though, that only an int can be returned, so the following statement will not produce the desired results.

SELECT dbo.fx_SumTwoValues(1.98,2.78) AS SumOfTwoValues    SumOfTwoValues  --------------  3

The function returns a 3, which indicates the decimal portion of the parameters are truncated before the calculation occurs.

SQL Server's ability to implicitly convert data allows the following to execute successfully.

SELECT dbo.fx_SumTwoValues('7','7') AS SumOfTwoValues    SumOfTwoValues  --------------  14

When values that cannot be converted to an int are passed to the function, the following error message is generated.

SELECT dbo.fx_SumTwoValues('Y','7') AS SumOfTwoValues    Server:Msg 245,Level 16,State 1,Line 1  Syntax error converting the varchar value 'Y'to a column of data type int.

Three Types of User-Defined Functions

Now that you have seen how easy it is to create and implement a simple function, let's cover the three different types of user-defined functions and some of the nuances of how they are implemented.

Scalar Functions

A scalar function returns a single value of the data type referenced in the RETURNS clause of the CREATE FUNCTION statement. The returned data can be of any type except text, ntext, image, cursor, or timestamp.

The example we covered in the previous section is a scalar function. Although the previous example only contained one statement in the BEGIN…END block, a scalar function can contain an unlimited number of statements as long as only one value is returned. The following example uses a WHILE construct to demonstrate this.

CREATE FUNCTION fx_SumTwoValues2  ( @Val1 int, @Val2 int )  RETURNS int  AS  BEGIN   WHILE @Val1 <100    BEGIN     SET @Val1 =@Val1 +1    END    RETURN (@Val1+@Val2)  END  go    SELECT dbo.fx_SumTwoValues2(1,7) AS SumOfTwoValues    SumOfTwoValues  --------------  107

The @Val1 input parameter is set to 1 when the function is called, but the WHILE increments the parameter to 100 before the RETURN statement is executed. Note that the two-part name (owner.object_name) is used to call the function. Scalar functions require that their two-part names be used when they are called. As you will see in the next two sections, this is not the case with the other two types of functions.

Inline Table-Valued Functions

An inline table-valued function returns a variable of data type table whose value is derived from a single SELECT statement. Since the return value is derived from the SELECT statement, there is no BEGIN/END block needed in the CREATE FUNCTION statement. There is also no need to specify the table variable name (or column definitions for the table variable) because the structure of the returned value is generated from the columns that compose the SELECT statement. Because the results are a function of the columns referenced in the SELECT, no duplicate column names are allowed and all derived columns must have an associated alias.

The following uses the Customer table in the Northwind database to show how an inline table-valued function is implemented.

USE Northwind  go  CREATE FUNCTION fx_Customers_ByCity  ( @City nvarchar(15) )  RETURNS table  AS  RETURN (          SELECT CompanyName          FROM Customers          WHERE City =@City  		 )  go  SELECT * FROM fx_Customers_ByCity('London')    CompanyName  ----------------------------------------  Around the Horn  . . .   Seven Seas Imports

Multi-Statement Table-Valued Functions

The multi-statement table-valued function is slightly more complicated than the other two types of functions because it uses multiple statements to build the table that is returned to the calling statement. Unlike the inline table-valued function, a table variable must be explicitly declared and defined. The following example shows how to implement a multi-statement table-valued function that populates and returns a table variable.

USE Northwind  go  CREATE FUNCTION fx_OrdersByDateRangeAndCount  ( @OrderDateStart smalldatetime,     @OrderDateEnd smalldatetime,     @OrderCount smallint )  RETURNS @OrdersByDateRange TABLE    (  CustomerID nchar(5),       CompanyName nvarchar(40),       OrderCount smallint,       Ranking char(1) )  AS  BEGIN   --Statement 1   INSERT @OrdersByDateRange   SELECT a.CustomerID,          a.CompanyName,          COUNT(a.CustomerID)AS OrderCount,         'B'   FROM Customers a   JOIN Orders b ON a.CustomerID =b.CustomerID   WHERE OrderDate BETWEEN @OrderDateStart AND @OrderDateEnd   GROUP BY a.CustomerID,a.CompanyName   HAVING COUNT(a.CustomerID)>@OrderCount    --Statement 2  UPDATE @OrdersByDateRange  SET Ranking ='A'  WHERE CustomerID IN (SELECT TOP 5 WITH TIES CustomerID                       FROM (SELECT a.CustomerID,                                    COUNT(a.CustomerID)AS OrderTotal                             FROM Customers a                             JOIN Orders b ON a.CustomerID =b.CustomerID                             GROUP BY a.CustomerID) AS DerivedTable                             ORDER BY OrderTotal DESC)  RETURN  END

The main difference between this example and the one in the previous section is that we were required to specify the structure of the @OrdersByDateRange table variable used to hold the resultset and list @OrdersByDateRange in the RETURNS clause. As you can see from the input parameter list, the function accepts a start date, an end date and an order count value to filter the resultset.

The first statement (--Statement 1) uses the input parameters to populate the table variable with customers who meet the specified criteria. The second statement (-Statement 2) updates the rows in table variable to identify the top five overall order placers. The IN portion of the UPDATE may seem a little confusing at first glance, but all its doing is using a derived table to select the CustomerID values of the top five order producers. Derived tables are discussed in Chapter 4. You can use the following to find the companies who have submitted more than two orders between 1/1/96 and 1/1/97.

SELECT *  FROM fx_OrdersByDateRangeAndCount ('1/1/96','1/1/97',2)  ORDER By Ranking    CustomerID  CompanyName                      OrderCount   Ranking  ----------  ------------------------------   ----------   -------  ERNSH       Ernst Handel                      6           A  FOLKO       Folk och fä HB                    3           A  HUNGO       Hungry Owl All-Night Grocers      5           A  QUICK       QUICK-Stop                        6           A  SAVEA       Save-a-lot Markets                3           A  SEVES       Seven Seas Imports                3           B  SPLIR       Split Rail Beer &Ale              5           B  ...

The rows ranking values of 'A' indicate the top five order placers of all companies. The function allows you to perform two operations with one object. Retrieve the companies who have placed more than two orders between 1/1/96 and 1/1/97 and let me know if any of these companies are my top five order producers.

One of the advantages of using this type of function over a view is that the body of the function can contain multiple SQL statements to populate the table variable, whereas a view is composed of only one statement. The advantage of using multi-statement table-valued function versus a stored procedure is that the function can be referenced in the FROM clause of a SELECT statement while a stored procedure cannot. Had a stored procedure been used to return the same data, the resultset could only be accessed with the EXECUTE command.

A Real-World Example

Now that you have an idea of the different types of functions available in SQL Server 2000, let's wrap up this article with an example you might be able to use on one of your projects. The following statements create a function that determines the last day of the month (LDOM) for a given date parameter.

CREATE FUNCTION fx_LDOM  (  @Date varchar(20) )  RETURNS datetime  AS  BEGIN   --ensure valid date   IF ISDATE(@Date) = 1    BEGIN     --determine first day of month     SET @Date = DATEADD(day,-DAY(@Date)+1,@Date)     --determine last day of month     SET @Date = DATEADD(day,-1,DATEADD(month,1,@Date))    END   ELSE    SET @Date = '1/1/80'      RETURN @Date  END

The function's parameter (@Date) is defined as varchar(20), so error-checking code can be implemented. The ISDATE function makes sure the supplied value is a valid date. When an invalid date value is supplied, the function returns '1/1/80' to the calling statement. If you do not use this type of error-checking, the call to the function will fail when an invalid date is supplied.

When a valid date value is supplied, the DATEADD function is used to:

  1. Determine the first day of the month, and
  2. Determine the last day of the month.

If you have never used DATEADD before this may seem a little confusing, but a quick explanation should eliminate any that might exist. You use DATEADD to add or substract a date/time unit from a given date. The first parameter (in this case 'day') indicates the portion of the date that should be incremented. You can also specify year, quarter, month...millisecond. The second parameter is the number of units to add or substract. When subtracting, you simply make the value negative as shown in the example. The third parameter is the date value on which the calculation is performed.

The first day of the month is determined by calculating the number of elapsed days in the supplied parameter with the DAY function, adding 1 and then substracting it from the parameter. For an @Date value of 1/15/01, it simply subtracts 14 (15-1) days to get 1/1/01.

The last day of the month is determined by adding 1 month to the current month value and subtracting one day. So, 1/1/01 plus 1 month is equal to 2/1/01 and when you substract one day you get: 1/31/01.

The following shows how fx_LDOM is used in a SELECT statement to calculate the number of days remaining in a month.

CREATE TABLE fx_Testing (DateValue datetime)  go  INSERT fx_Testing VALUES ('1/1/01')  INSERT fx_Testing VALUES ('2/15/01')  INSERT fx_Testing VALUES ('2/15/02')  INSERT fx_Testing VALUES ('2/15/03')  INSERT fx_Testing VALUES ('2/15/04')    SELECT DateValue,         dbo.fx_LDOM(DateValue) AS LDOM,         DATEDIFF(day,DateValue,dbo.fx_LDOM(DateValue)) AS DaysLeftInMonth  FROM fx_Testing    DateValue                 LDOM                    DaysLeftInMonth  ------------------------  ----------------------- ---------------  2001-01-01 00:00:00.000   2001-01-31 00:00:00.000  30  2001-02-15 00:00:00.000   2001-02-28 00:00:00.000  13  2002-02-15 00:00:00.000   2002-02-28 00:00:00.000  13  2003-02-15 00:00:00.000   2003-02-28 00:00:00.000  13  2004-02-15 00:00:00.000   2004-02-29 00:00:00.000  14

The DATEDIFF function is used to determine the difference between two dates. In this case, the number of days between the value in the DateValue column and the last day of the month calculated by fx_LDOM.

No comments:

Post a Comment