Nielsen c24.tex V4 - 07/23/2009 4:53pm Page 622 Part IV Developing with SQL Server FIGURE 24-2 The path and scope of return methods differ among the five possible methods of returning data. Client App Proc A Select Raiserror from A #temp table (insert…exec)(try…catch) Proc B DML Select exec exec Result set from A Result set from B Raiserror from B Output Parameters Return Return Result set from B Raiserror Output Parameters Best Practice W ith every returned record set, SQL Server will, by default, also send a message stating the number of rows affected or returned. Not only is this a nuisance, but I have found in my informal testing that it can slow a query by up to 17 percent depending on the que ry’s complexity. Therefore, get into the habit of beginning every stored procedure with the following code: CREATE PROC MyProc AS SET NOCOUNT ON; Summary Using stored procedures is a way to save and optimize batches. Stored procedures are compiled and stored in memory the first time they are executed. No method is faster at executing SQL commands, or more popular for moving the processing close to the data. Like a batch, a stored procedure can return a record set by simply executing a SELECT command. The next chapter covers user-defined functions, which combine the benefits of stored procedures with the benefits of views at the cost of portability. 622 www.getcoolebook.com Nielsen c25.tex V4 - 07/23/2009 4:55pm Page 623 Building User-Defined Functions IN THIS CHAPTER Creating scalar functions Replacing views with inline table-valued functions Using complex code within multi-statement table-valued functions to generate a result set S QL Server 2000 introduced user-defined functions (UDFs), and the SQL Server community was initially slow to adopt them. Nevertheless, UDFs were my personal favorite new feature in SQL Server 2000, and I still use them frequently. The community discovered that UDFs can be used to embed complex T-SQL logic within a query, and problems that were impossible or required cursors could now be solved with UDFs. The result is that UDFs have become a favorite tool in the toolbox of any serious SQL Server database developer. The benefits of UDFs can be easily listed: ■ UDFs can be used to embed complex logic within a query. This is huge. I’ve solved several nasty problems using user-defined functions. ■ UDFs can be used to create new functions for complex e xpressions. ■ UDFs offer the benefits of views because they can be used within the FROM clause of a SELECT statement or an expression, and they can be schema-bound. In addition, user-defined functions can accept parameters, whereas views cannot. ■ UDFs offer the benefits of stored procedures because they are compiled and optimized in the same way. The chief argument against developing with user-defined functions has to do with potential performance issues if they’re misused. Any function, user-defined or system that must be executed for every row in a WHERE clause will cripple performance (see the sidebar on algebra in Chapter 8, ‘‘Introducing Basic Query Flow.’’) User-defined functions come i n three distinct types (as shown in Figure 25-1.) Management Studio groups inline table-valued functions with multi-statement table-valued functions: ■ Scalar functions that return a single value 623 www.getcoolebook.com Nielsen c25.tex V4 - 07/23/2009 4:55pm Page 624 Part IV Developing with SQL Server ■ Inline table-valued functions similar to views ■ Multi-statement table-valued functions that build a result set with code New in 2008 U ser-defined functions haven’t changed much since they were introduced in SQL Server 2000. If you’re upgrading to SQL Server 2008 directly from SQL Server 2000, then it’s worth knowing that the APPLY keyword, covered later in this chapter, was added in SQL Server 2005. Microsoft system functions are changing. When they were introduced in SQL Server 2000, system functions were all prefixed with a double colon, such as ::fn_SystemFunction. The double colon has been deprecated and will be disabled in a future version of SQL Server. Instead, system functions are now in the sys. schema — for example, sys.fn_SystemFunction. FIGURE 25-1 Management Studio’s Object Explorer lists all the user-defined functions within a database, organized by table-valued and scalar-valued. 624 www.getcoolebook.com Nielsen c25.tex V4 - 07/23/2009 4:55pm Page 625 Building User-Defined Functions 25 Andrew Novick, who regularly presents about UDFs at user groups and SQL conferences, has compiled a great resource of sample UDFs. His website is www.novicksoftware.com. Scalar Functions A scalar function is one that returns a single specific value. The function can accept multiple parameters, perform a calculation, and then return a single value. For example, a scalar function could accept three parameters, perform a calculation, and return the answer. Within the code of a scalar function, the value is passed back through the function by means of a RETURN command. Every possible codepath in the user-defined function should conclude with a RETURN command. Scalar user-defined functions may be used within any expressions in SQL Server, even expres- sions within check constraints (although I don’t recommend scalar function within check constraints because that extends the duration of the transaction and it is difficult to locate and maintain later). Limitations The scalar function must be deterministic, meaning it must repeatedly return the same value for the same input parameters. For this reason, nondeterministic functions — such as newid() and rand() — are not allowed within scalar functions. Writing a UDF to return a random row is out of the question. User-defined scalar functions are not permitted to update the database or call DBCC commands, with the single exception that they may update table variables. They cannot return BLOB (binary large object) data such as text, ntext, timestamp,andimage data-type variables, nor can they return table variables or cursor data types. For error handling, UDFs may not include TRY CATCH or RAISERROR. A user-defined function may call other user-defined functions nesting up to 32 levels deep, or it can call itself recursively up to 32 levels deep before it blows up. Creating a scalar function User-defined functions are created, altered, or dropped with the same DDL commands used for other objects, although the syntax is slightly different to allow for the return value: CREATE FUNCTION FunctionName (InputParameters) RETURNS DataType AS BEGIN; Code; RETURN Expression; END; 625 www.getcoolebook.com Nielsen c25.tex V4 - 07/23/2009 4:55pm Page 626 Part IV Developing with SQL Server The input parameters include a data-type definition and may optionally include a default value similar to stored procedure parameters ( parameter = default). Function parameters differ from stored procedure parameters in that even if the default is desired, the parameter is still required to call the function. Parameters with defaults don’t become optional parameters. To request the default when calling the function, pass the keyword DEFAULT to the function. The following user-defined scalar function performs a simple mathematical function. The second param- eter includes a default value: CREATE FUNCTION dbo.fsMultiply (@A INT, @B INT =3) RETURNS INT AS BEGIN; RETURN @A * @B; END; go SELECT dbo.fsMultiply (3,4), dbo.fsMultiply (7, DEFAULT); Result: 12 21 While I’m not a stickler for naming conventions (as long as they’re consistent), I can under- stand the practice of prefacing user-defined functions with an f. For a more complex scalar user-defined function, the fGetPrice function from the OBXKites sample database returns a single result via an output parameter. It’s just a variation of the pGetPrice stored procedure. Both the stored procedure and function determine the correct price for any given date and for any customer discount. Because the task returns a single value, calculating the price is a prime candi- date for a scalar user-defined function. As a function, it can be plugged into any query, whereas a stored procedure is more difficult to use as a building block in other code. The function uses the same internal code as the stored procedure, except that the @CurrPrice is passed back through the final RETURN instead of an output variable. The function uses a default value of NULL for the contact code. Here is the code for the fsGetPrice user-defined scalar function: CREATE FUNCTION fsGetPrice ( @Code CHAR(10), @PriceDate DATETIME, @ContactCode CHAR(15) = NULL) RETURNS MONEY AS BEGIN; DECLARE @CurrPrice MONEY ; DECLARE @DiscountPercent NUMERIC (4,2); set the discount percent 626 www.getcoolebook.com Nielsen c25.tex V4 - 07/23/2009 4:55pm Page 627 Building User-Defined Functions 25 if no customer lookup then it’s zilch discount SELECT @DiscountPercent = CustomerType.DiscountPercent FROM dbo.Contact JOIN dbo.CustomerType ON contact.CustomerTypeID = CustomerType.CustomerTypeID WHERE ContactCode = @ContactCode; IF @DiscountPercent IS NULL SET @DiscountPercent = 0; SELECT @CurrPrice = Price * (1-@DiscountPercent) FROM dbo.Price JOIN dbo.Product ON Price.ProductID = Product.ProductID WHERE Code = @Code AND EffectiveDate = (SELECT MAX(EffectiveDate) FROM dbo.Price JOIN dbo.Product ON Price.ProductID = Product.ProductID WHERE Code = @Code AND EffectiveDate <= @PriceDate); RETURN @CurrPrice; END; Calling a scalar function Scalar functions may be used anywhere within any expression that accepts a single value. User- defined scalar functions must always be called by means of at least a two-part name (owner.name). The following script demonstrates calling the fGetPrice() function within OBXKites: USE OBXKites; SELECT dbo.fsGetPrice(’1006’,CURRENT_TIMESTAMP,DEFAULT), dbo.fsGetPrice(’1001’,’5/1/2001’,NULL); Result: 125.9500 14.9500 The user-defined scalar function dbo.fTitleCase is created in Chapter 9, ‘‘Data Types, Expressions, and Scalar Functions,’’ and is available on www.sqlserverbible.com. Inline Table-Valued Functions The second type of user-defined function, the inline table-valued function, is very similar to a view. Both are wrapped for a stored SELECT statement. An inline table-valued user-defined function retains the benefits of a view, and adds parameters. As with a view, if the SELECT statement is updateable, then the function will be updateable. 627 www.getcoolebook.com Nielsen c25.tex V4 - 07/23/2009 4:55pm Page 628 Part IV Developing with SQL Server Creating an inline table-valued function The inline table-valued user-defined function has no BEGIN/END body. Instead, the SELECT statement is returned as a virtual table: CREATE FUNCTION FunctionName (InputParameters) RETURNS Table AS RETURN (Select Statement); The following inline table-valued user-defined function is functionally equivalent to the vEventList view created in Chapter 14, ‘‘Projecting Data Through Views.’’ USE CHA2; go CREATE FUNCTION ftEventList () RETURNS Table AS RETURN( SELECT dbo.CustomerType.Name AS Customer, dbo.Customer.LastName, dbo.Customer.FirstName, dbo.Customer.Nickname, dbo.Event_mm_Customer.ConfirmDate, dbo.Event.Code, dbo.Event.DateBegin, dbo.Tour.Name AS Tour, dbo.BaseCamp.Name, dbo.Event.Comment FROM dbo.Tour INNER JOIN dbo.Event ON dbo.Tour.TourID = dbo.Event.TourID INNER JOIN dbo.Event_mm_Customer ON dbo.Event.EventID = dbo.Event_mm_Customer.EventID INNER JOIN dbo.Customer ON dbo.Event_mm_Customer.CustomerID = dbo.Customer.CustomerID LEFT OUTER JOIN dbo.CustomerType ON dbo.Customer.CustomerTypeID = dbo.CustomerType.CustomerTypeID INNER JOIN dbo.BaseCamp ON dbo.Tour.BaseCampID = dbo.BaseCamp.BaseCampID); Calling an inline table-valued function To retrieve data through ftEventList, call the function within the FROM portion of a SELECT statement: SELECT LastName, Code, DateBegin FROM dbo.ftEventList(); Result (abridged): 628 www.getcoolebook.com Nielsen c25.tex V4 - 07/23/2009 4:55pm Page 629 Building User-Defined Functions 25 LastName Code DateBegin Anderson 01-003 2001-03-16 00:00:00.000 Brown 01-003 2001-03-16 00:00:00.000 Frank 01-003 2001-03-16 00:00:00.000 Using parameters An advantage of inline table-valued f unctions over views is the function’s ability to include parameters within the pre-compiled SELECT statement. Views, conversely, do not include parameters, and restrict- ing the result at runtime is typically achieved by adding a WHERE clause to the SELECT statement that calls the view. The following examples compare adding a restriction to the view to using a function parameter. The following view returns the current price list for all products: USE OBXKites; go CREATE VIEW vPricelist AS SELECT P.Code, Price.Price FROM dbo.Price JOIN dbo.Product P ON Price.ProductID = P.ProductID WHERE EffectiveDate = (SELECT MAX(EffectiveDate) FROM dbo.Price WHERE ProductID = P.ProductID AND EffectiveDate <= CURRENT_TIMESTAMP); To retrieve the current price for a single product, the calling SELECT statement adds a WHERE-clause restriction when calling the view: SELECT * FROM vPriceList WHERE = ‘1001’; Result: Code Price 1001 14.9500 SQL Server internally creates a new SQL statement from vPricelist and the calling SELECT state- ment’s WHERE-clause restriction and then generates a query execution plan. 629 www.getcoolebook.com Nielsen c25.tex V4 - 07/23/2009 4:55pm Page 630 Part IV Developing with SQL Server In contrast, a function allows the restriction to be passed as a parameter to the SQL SELECT statement: CREATE FUNCTION dbo.ftPriceList ( @Code CHAR(10) = Null, @PriceDate DateTime) RETURNS Table AS RETURN( SELECT Code, Price.Price FROM dbo.Price JOIN dbo.Product P ON Price.ProductID = P.ProductID WHERE EffectiveDate = (SELECT MAX(EffectiveDate) FROM dbo.Price WHERE ProductID = P.ProductID AND EffectiveDate <= @PriceDate) AND (Code = @Code OR @Code IS NULL) ); If the function is called with default code, then the price for the entered date is returned for all products: SELECT * FROM dbo.ftPriceList(DEFAULT, ‘20020220’); Result: Code Price 1047 6.9500 1049 12.9500 If a product code is passed in the first input parameter, then the pre-compiled SELECT statement within the function returns the single product row: SELECT * FROM dbo.ftPriceList(’1001’, ‘2/20/2002’); Result: Code Price 1001 14.9500 Correlated user-defined functions The APPLY command may be used with a table-valued user-defined function so that the UDF accepts a different parameter value for each corresponding row being processed by the main query. 630 www.getcoolebook.com Nielsen c25.tex V4 - 07/23/2009 4:55pm Page 631 Building User-Defined Functions 25 Back in the SQL Server 2000 days, not having this capability was a serious limitation that caused me a considerable amount of time to work around, so I’m pleased to see that Microsoft added the APPLY function in SQL Server 2005. The APPLY command has two forms. The most common form, the CROSS APPLY,hasaconfusing name because it operates more like an inner join than a cross join. The CROSS APPLY command will join data from the main query with any table-value d data sets from the user-defined function. If no data is returned from the UDF, then the row from the main query is also not returned, as shown in the following example: USE CHA2; go CREATE FUNCTION ftEventList2 (@CustomerID INT) RETURNS Table AS RETURN( SELECT dbo.CustomerType.Name AS Customer, dbo.Customer.LastName, dbo.Customer.FirstName, dbo.Customer.Nickname, dbo.Event_mm_Customer.ConfirmDate, dbo.Event.Code, dbo.Event.DateBegin, dbo.Tour.Name AS Tour, dbo.BaseCamp.Name, dbo.Event.Comment FROM dbo.Tour INNER JOIN dbo.Event ON dbo.Tour.TourID = dbo.Event.TourID INNER JOIN dbo.Event_mm_Customer ON dbo.Event.EventID = dbo.Event_mm_Customer.EventID INNER JOIN dbo.Customer ON dbo.Event_mm_Customer.CustomerID = dbo.Customer.CustomerID LEFT OUTER JOIN dbo.CustomerType ON dbo.Customer.CustomerTypeID = dbo.CustomerType.CustomerTypeID INNER JOIN dbo.BaseCamp ON dbo.Tour.BaseCampID = dbo.BaseCamp.BaseCampID WHERE Customer.CustomerID = @CustomerID ); SELECT C.LastName, Code, DateBegin, Tour FROM Customer C CROSS APPLY ftEventList2(C.CustomerID) ORDER BY C.LastName; Result: LastName Code DateBegin Tour Anderson 01-003 2001-03-16 00:00:00.000 Amazon Trek Anderson 01-006 2001-07-03 00:00:00.000 Bahamas Dive 631 www.getcoolebook.com . introduced in SQL Server 2000. If you’re upgrading to SQL Server 2008 directly from SQL Server 2000, then it’s worth knowing that the APPLY keyword, covered later in this chapter, was added in SQL Server. result set S QL Server 2000 introduced user-defined functions (UDFs), and the SQL Server community was initially slow to adopt them. Nevertheless, UDFs were my personal favorite new feature in SQL Server. 4:55pm Page 624 Part IV Developing with SQL Server ■ Inline table-valued functions similar to views ■ Multi-statement table-valued functions that build a result set with code New in 2008 U ser-defined