You can define a user-defined function with a single statement or with multiple statements, as you will see later in this chapter in the "Creating and Dropping User-Defined Functions" section.
According to their return value, user-defined functions can be divided into three groups:
Scalar functions that return a single scalar value.
Table-valued functions that return a full result set, similar to a table.
Inline user-defined functions are a special case of table-valued user-defined functions, but they are limited to a single SELECT statement.
Scalar user-defined functions return a single value, and they can be used wherever an expression is accepted, such as
In the SELECT clause of a SELECT statement, as a part of an expression or as an individual column
In the SET clause of an UPDATE statement, as a value to insert into a field of the table being updated
In the FROM clause of any DML statement (SELECT, UPDATE, INSERT, DELETE), as a single-column, single-row result set–derived table
In the FROM clause of any DML statement, as part of the joining conditions in the ON clause
In the WHERE clause or HAVING clause of any DML statement
In the GROUP BY clause, as part of any grouping condition
In the ORDER BY clause of any statement, as sorting criteria
As a DEFAULT value for a column
Inside a CHECK CONSTRAINT definition
In a PRINT statement, if the user-defined function returns a string
As part of the condition of IF or WHILE statements
As part of the definition of a compute column
As a parameter to call a stored procedure or another user-defined function
As a return value of a stored procedure, if the user-defined function returns an integer value
As a return value of another scalar user-defined function
Scalar user-defined functions can be combined with other functions in an expression, as long as the data types are compatible with the operation.
Tip
You can identify scalar user-defined functions because they return a scalar data type and their definition is enclosed in a BEGIN...END block.
To create a scalar user-defined function, you must use the CREATE FUNCTION statement, as shown in Listing 10.4.
The first example creates the MaxProductID function, which selects the maximum available ProductID from the Products table. This function returns a scalar integer value.
The second example creates the WhoWhere function, which returns a scalar string with information about which is the login of the connected user and from which machine the user is connected.
The third example returns the date of the latest executed process in SQL Server, which we can consider today's date in general.
The fourth example is a bit more complex; it generates a random number, between 0.0 and 1.0, based on the time of the latest statement executed in SQL Server. This function is similar to the system function Rand; however, Rand cannot be used inside a user-defined function, whereas this PRand function can be used.
USE Northwind GO -- Returns the maximum ProductID from Products CREATE FUNCTION dbo.MaxProductID () RETURNS int AS BEGIN RETURN ( SELECT MAX(ProductID) FROM dbo.Products ) END GO -- Returns who and from where the query is executed CREATE FUNCTION dbo.WhoWhere () RETURNS nvarchar(256) AS BEGIN RETURN SYSTEM_USER + 'FROM ' + APP_NAME() END GO -- Returns the date of the latest executed statement -- which is usually today CREATE FUNCTION dbo.Today () RETURNS smalldatetime AS BEGIN DECLARE @sdt smalldatetime SELECT @SDT = CONVERT(varchar(10), MAX(last_batch), 112) FROM master.dbo.sysprocesses RETURN @SDT END GO -- Function that produces a non-predictable -- Pseudo-Random Number CREATE FUNCTION dbo.PRand () RETURNS float AS BEGIN DECLARE @dt datetime DECLARE @dts varchar(3) DECLARE @t1 float DECLARE @t2 float DECLARE @r float -- Obtain the time of latest executed statement SET @dt = ( SELECT MAX(last_batch) FROM master.dbo.sysprocesses ) -- Select only the milliseconds SET @dts = RIGHT(CONVERT(varchar(20), @dt, 114) , 3) -- Scramble the digits SET @t1 = CAST(SUBSTRING(@dts, 2, 1) + RIGHT(@dts, 1) + LEFT(@dts, 1) AS int) -- Obtain the time of latest executed statement SET @dt = ( SELECT MAX(last_batch) FROM master.dbo.sysprocesses ) -- Select only the milliseconds SET @dts = RIGHT(CONVERT(varchar(20), @dt, 114) , 3) -- Scramble the digits SET @t2 = CAST(SUBSTRING(@dts, 2, 1) + RIGHT(@dts, 1) + LEFT(@dts, 1) AS int) -- Select the random number SET @r = '0'+ LEFT(RIGHT(CONVERT(varchar(40), @t1 * @t2 / pi(), 2), 21), 16) -- Return the random number RETURN @r END GO |
You can identify several parts in the CREATE FUNCTION syntax for a scalar user-defined function:
CREATE FUNCTION ownername functionname, where you can specify the owner of the function and the name of the function.
() is an empty list of parameters. We will discuss parameters in the next section of this chapter.
RETURNS datatype, where you define the data type for the returned value as the function's result.
AS BEGIN...END to mark the function definition body.
The function definition body.
You can define the body of the function in a way similar to a stored procedure, declaring and using variables, using control-of-flow statements, accessing data from other tables, and other databases and servers.
Caution
Remember that you cannot modify data in existing tables inside a user-defined function directly. This includes the creation of temporary tables.
Because writing long user-defined functions can be complex, you can break down long functions into smaller ones that can be reused more often. Listing 10.5 creates a new version of the PRand function, created in Listing 10.4. This version uses a base function, called Get3Rand, to generate the scrambled three-digit number. The NewPRand function uses the Get3Rand function to generate two values and combine them to provide the new random number.
USE Northwind GO -- Create a base function to extract a three-digits -- number based on the scrambled version of the -- milliseconds information of the latest executed -- statement in SQL Server CREATE FUNCTION dbo.Get3Rand () RETURNS int AS BEGIN DECLARE @dt datetime DECLARE @dts varchar(3) SET @dt = ( SELECT MAX(last_batch) FROM master.dbo.sysprocesses ) SET @dts = RIGHT(CONVERT(varchar(20), @dt, 114) , 3) RETURN CAST(SUBSTRING(@dts, 2, 1) + RIGHT(@dts, 1) + LEFT(@dts, 1) AS int) END GO -- Create the new NewPRand Random function -- based on the Get3Rand function CREATE FUNCTION dbo.NewPRand () RETURNS float AS BEGIN DECLARE @r float SET @r = '0'+ LEFT(RIGHT(CONVERT(varchar(40), dbo.Get3Rand() * dbo.Get3Rand() / pi(), 2), 21), 16) RETURN (@r) END |
Tip
If your user-defined function requires using temporary tables, you can use table variables instead. Table variables are defined inside the user-defined function and can be modified inside it.
Listing 10.6 uses table variables defined internally inside a user-defined function to store intermediate results. This function calculates the medium UnitPrice for products stored in the Products table.
Note
The medium value is the central value of an ordered list of values. The medium does not have to be equal to the average value.
USE Northwind GO CREATE FUNCTION dbo.MediumProductUnitPrice () RETURNS money AS BEGIN -- Create hosting table variable DECLARE @t TABLE( id int identity(1,1), UnitPrice money) -- Inserts the product prices in ascending order INSERT INTO @t (UnitPrice) SELECT UnitPrice FROM Products ORDER BY UnitPrice ASC -- Selects the medium price RETURN ( SELECT MAX(UnitPrice) FROM @t WHERE ID <= (SELECT MAX(ID) FROM @t) / 2 ) END |
As you learned in Chapter 8, you can expand stored procedure capabilities by using parameters. You can create user-defined functions with parameters, too.
The examples from the preceding section do not use any parameter, which is why their execution does not depend on any value that the user might send. Most of the system-supplied mathematical functions accept one or more parameters and return a scalar result according to the mathematical operation to execute. Trigonometric functions use a number as a parameter and return a number as a result. String functions take one or more parameters and return a string.
You can create user-defined functions to expand the collection of system-supplied functions, using parameters. You must define a parameter list in the CREATE FUNCTION statement, after the function name. The parameters list is enclosed in parentheses. You must provide a data type for every parameter and, optionally, a default value.
Caution
The parameter list can be empty if the function does not use any input parameters. In this case, you must supply two parentheses () to specify an empty parameters list.
If you do not specify a parentheses-enclosed parameter list, or at least an empty one, you will get a syntax error when trying to create the user-defined function.
Listing 10.7 shows some examples of user-defined functions using parameters.
The first function, TotalPrice, computes the total price of a specific sale. You must provide the quantity sold, the unit price to apply, the agreed discount, and then the function returns the total price of the sale.
The second function in Listing 10.7, fn_FV, computes the future value of an annuity, the FV financial formula, as described in Microsoft Excel and Microsoft Visual Basic.
The third and fourth functions provide an example of how to create a user-defined function to perform basic encryption.
Caution
The intention of the SimpleEncrypt and SimpleDecrypt user-defined functions is only to show how to define a function to modify a string. The encryption used in these functions is too simple to be used in a production environment.
USE Northwind GO ----------------------------------------------------------- -- Generic function to compute the total price of a sale -- from the quantity, unitprice and discount ----------------------------------------------------------- CREATE FUNCTION dbo.TotalPrice (@Quantity float, @UnitPrice money, @Discount float = 0.0) RETURNS money AS BEGIN RETURN (@Quantity * @UnitPrice * (1.0 - @Discount)) END GO ----------------------------------------------------------- -- Compute the future value of an annuity based on -- periodic fixed payments with a fixed interest rate -- Parameters: -- @rate: interest rate between payments -- @nper: number of payments -- @pmt: payment to be made on every period -- @pv: present value. Default to 0.0 -- @type: 0 if the payment is made at the end of each period (default) -- 1 if the payment is made at the beginning of each period ----------------------------------------------------------- CREATE FUNCTION dbo.fn_FV (@rate float, @nper int, @pmt money, @pv money = 0.0, @type bit = 0) RETURNS money AS BEGIN DECLARE @fv money IF @rate = 0 SET @fv = @pv + @pmt * @nper ELSE SET @fv = @pv * POWER(1 + @rate, @nper) + @pmt * (((POWER(1 + @rate, @nper + @type) - 1) / @rate) - @type) RETURN (-@fv) END GO ----------------------------------------------------------- -- Encrypt the string increasing the Unicode value of every -- character by the number of characters in the string ----------------------------------------------------------- CREATE Function dbo.SimpleEncrypt (@string nvarchar(4000)) RETURNS nvarchar(4000) AS BEGIN DECLARE @output nvarchar(4000) DECLARE @i int, @l int, @c int SET @i = 1 SET @l = len(@string) SET @output = '' WHILE @i <= @l BEGIN SET @c = UNICODE(SUBSTRING(@string, @i, 1)) SET @output = @output + CASE WHEN @c > 65535 - @l THEN NCHAR(@c + @l - 65536) ELSE NCHAR(@c + @l) END SET @i = @i + 1 END RETURN @output END GO ----------------------------------------------------------- -- Decrypt the string decreasing the Unicode value of every -- character by the number of characters in the string ----------------------------------------------------------- CREATE Function dbo.SimpleDecrypt (@string nvarchar(4000)) RETURNS nvarchar(4000) AS BEGIN DECLARE @output nvarchar(4000) DECLARE @i int, @l int, @c int SET @i = 1 SET @l = len(@string) SET @output = '' WHILE @i <= @l BEGIN SET @c = UNICODE(SUBSTRING(@string, @i, 1)) SET @output = @output + CASE WHEN @c - @l >= 0 THEN NCHAR(@c - @l) ELSE NCHAR(@c + 65535 - @l) END SET @i = @i + 1 END RETURN @output END GO |
Listing 10.8 shows a more complex example. In this case, we want to create functions to convert angles from and to HMS format (hours, minutes, and seconds, or degrees, minutes, and seconds). We want to be able to use angles either in sexagesimal (1 circle = 360°) or centesimal (1 circle = 400g) units.
Note
Some mathematical books and scientific calculators refer to centesimal degrees as grades.
First, create the ANG_HMS user-defined function, which converts an angle into HMS format. To know the angle's units, define the @d_g parameter to specify the character that identifies the degrees unit. The @d_g parameter has only two possible values: 'g' (centesimal) or '°' (sexagesimal).
You can see in Listing 10.8 comments along the code of the ANG_HMS function, but in summary, it extracts from the angle in sequence: degrees, minutes, and seconds. The @precision parameter serves us to specify the number of digits to show for the fraction of the second.
Already having the generic ANG_HMS function, you can create the functions DEG_HMS and GRAD_HMS now to use directly in the ANG_HMS function.
Following a similar process, you can create the HMS_ANG, HMS_DEG, and HMS_GRAD functions.
USE Northwind GO ----------------------------------------------------------- -- Generic function to convert an angle -- into HMS format -- @d_g indicates if the angle is measured in -- sexagesimal degrees (CHAR(176) as the degrees symbol) or -- centesimal degrees ('g', for grades) ----------------------------------------------------------- CREATE FUNCTION dbo.ANG_HMS (@angle float, @precision int = 0, @d_g CHAR(1)) RETURNS varchar(21) AS BEGIN -- Declare variables DECLARE @sign float DECLARE @ncircles int DECLARE @degrees int DECLARE @minutes int DECLARE @seconds float DECLARE @secInt int DECLARE @secDec int DECLARE @frac float DECLARE @hms varchar(20) -- Save the sign of the angle SET @sign = SIGN(@angle) -- Take out the sign of the angle -- to avoid calculation problems SET @angle = ABS(@angle) -- Extract the integer part as degrees SET @degrees = CAST(FLOOR(@angle) AS int) -- Count how many complete circles the angle has SET @ncircles = @degrees / CASE @d_g WHEN 'g'THEN 400 ELSE 360 END -- Convert the angle into an angle from the first circle SET @degrees = @degrees % CASE @d_g WHEN 'g'THEN 400 ELSE 360 END -- Extract the decimal part from the angle SET @frac = @angle - @degrees - (@ncircles * CASE @d_g WHEN 'g'THEN 400 ELSE 360 END ) -- Extract minutes from the decimal part SET @minutes = FLOOR(@frac * CASE @d_g WHEN 'g'THEN 100 ELSE 60 END ) -- Extract the number of seconds SET @seconds = (@frac * CASE @d_g WHEN 'g'THEN 100 ELSE 60 END - @minutes) * CASE @d_g WHEN 'g'THEN 100 ELSE 60 END -- Calculate the number of complete seconds SET @secInt = FLOOR(@seconds) -- Set a limit for the fraction of a second to 9 digits IF @precision > 9 SET @precision = 9 -- Extract the fraction of a second in the given precision SET @secDec = (@seconds - @secInt) * POWER(10, @precision) -- Start creating the resulting string with the sign -- only for negative numbers SET @hms = CASE @sign WHEN -1 THEN '- ' ELSE ''END -- Create the HMS format SET @hms = @hms + CAST(@degrees AS varchar(3)) + CHAR(176) + '' + CAST(@minutes AS varchar(2)) + 'm ' + CAST(@secInt AS varchar(2)) + 's' + CAST(@secDec AS varchar(10)) RETURN @hms END GO ---------------------------------- -- Function to convert angles -- measured in centesimal degrees -- into HMS format ---------------------------------- CREATE FUNCTION dbo.GRAD_HMS (@angle float, @precision int = 0) RETURNS varchar(21) AS BEGIN -- Call the ANG_HMS function with the 'g'format RETURN dbo.ANG_HMS(@angle, @precision, 'g') END GO ----------------------------------- -- Function to convert angles -- measured in sexagesimal degrees -- into HMS format ----------------------------------- CREATE FUNCTION dbo.DEG_HMS (@angle float, @precision int = 0) RETURNS varchar(21) AS BEGIN -- Call the ANG_HMS function with the CHAR(176) format RETURN dbo.ANG_HMS(@angle, @precision, CHAR(176)) END GO ----------------------------------------------------------- -- Generic function to convert an angle -- from HMS format -- @d_g indicates if the angle is measured in -- sexagesimal degrees (CHAR(176) as the degrees symbol) or -- centesimal degrees ('g', for grades) ----------------------------------------------------------- CREATE FUNCTION dbo.HMS_ANG (@hms varchar(22), @d_g char(1)) RETURNS float AS BEGIN -- Declare variables DECLARE @sign float DECLARE @pos0 int DECLARE @posg int DECLARE @posm int DECLARE @poss int DECLARE @degrees float DECLARE @angle float DECLARE @minutes float DECLARE @seconds float DECLARE @secInt float DECLARE @secDec float -- Extract the sign IF RIGHT(@hms, 1) = '-' SELECT @sign = -1, @pos0 = 2 ELSE SELECT @sign = 1, @pos0 = 1 -- Search for the position in the string -- of the character dividing degrees, minutes, and seconds SET @posg = CHARINDEX(@d_g, @hms) SET @posm = CHARINDEX('m', @hms) SET @poss = CHARINDEX('s', @hms) -- Extract the value of the degrees SET @degrees = SUBSTRING(@hms, @pos0, @posg - @pos0) -- Extract the value of the minutes SET @minutes = SUBSTRING(@hms, @posg + 1, @posm - @posg - 1) -- Extract the value of the seconds -- as integer and decimal part SET @secInt = SUBSTRING(@hms, @posm + 1, @poss - @posm - 1) SET @secDec = SUBSTRING(@hms, @poss + 1, len(@hms) - @poss) / POWER(10.0, len(@hms) - @poss) -- Calculate the angle SET @angle = @sign * ( @degrees + @minutes / CASE @d_g WHEN 'g'THEN 100.0 ELSE 60.0 END + (@secInt + @secdec) / CASE @d_g WHEN 'g'THEN 10000.0 ELSE 3600.0 END) -- Return the value RETURN @angle END GO ---------------------------------- -- Function to convert angles -- measured in sexagesimal degrees -- from HMS format ---------------------------------- CREATE FUNCTION dbo.HMS_DEG (@hms varchar(22)) RETURNS float AS BEGIN -- Call the HMS_ANG function with the CHAR(176) format RETURN dbo.HMS_ANG(@hms, CHAR(176)) END GO ---------------------------------- -- Function to convert angles -- measured in centesimal degrees -- from HMS format ---------------------------------- CREATE FUNCTION dbo.HMS_GRAD (@hms varchar(22)) RETURNS float AS BEGIN -- Call the ANG_HMS function with the 'g'format RETURN (dbo.HMS_ANG(@hms, 'g')) END GO |
You can use scalar user-defined functions anywhere in any Transact-SQL statement in which an expression is allowed.
If you want to invoke a user-defined function, you must qualify the function name with its owner, usually dbo. Listing 10.9 shows some statements using the MaxProductID in different valid ways.
SELECT dbo.MaxProductID() GO SELECT ProductID, dbo.MaxProductID() AS 'MaxID' FROM Products GO UPDATE [Order Details] SET ProductID = dbo.MaxProductID() WHERE ProductID = 25 GO SELECT ProductID, MaxID FROM Products CROSS JOIN (SELECT dbo.MaxProductID() AS 'MaxID') AS MI GO SELECT P.ProductID, Quantity FROM Products AS P JOIN [Order Details] AS OD ON P.ProductID = OD.ProductID AND P.ProductID = dbo.MaxProductID() GO SELECT P.ProductID, Quantity FROM Products AS P JOIN [Order Details] AS OD ON P.ProductID = OD.ProductID WHERE P.ProductID = dbo.MaxProductID() GO SELECT P.ProductID, SUM(Quantity) FROM Products AS P JOIN [Order Details] AS OD ON P.ProductID = OD.ProductID GROUP BY P.ProductID HAVING P.ProductID = dbo.MaxProductID() GO SELECT ProductID, ProductName, CASE ProductID WHEN dbo.MaxProductID() THEN 'Last Product' ELSE ''END AS Note FROM Products GO DECLARE @ID int SET @ID = dbo.MaxProductID() |
Caution
User-defined functions are local to the database where they are created. Although it is possible to create global user-defined functions, Microsoft does not support this functionality.
If a user-defined function uses parameters, you must specify a value for each parameter on every call, even if they have default values. You can use the keyword DEFAULT to provide the default value for a parameter that has a default value. You cannot omit a parameter when calling a user-defined function—because it has a default value already—otherwise, you will receive a syntax error message, as in Listing 10.10, where you can use the TotalPrice function without providing a value for discount, hoping that the function will use the default value.
USE Northwind GO -- This is an illegal call, because it does not provide -- a value for the @discount parameter SELECT dbo.TotalPrice (12, 25.4) GO -- This is a valid call, because it does provide -- a value for every parameter SELECT dbo.TotalPrice (12, 25.4, 0.0) GO Server: Msg 313, Level 16, State 2, Line 1 An insufficient number of arguments were supplied for the procedure or function dbo .TotalPrice. --------------------- 304.8000 |
Listing 10.11 shows how to invoke some of the functions defined in Listings 10.4, 10.5, 10.6, 10.7, and 10.8. These examples show how to use these functions in different kinds of statements.
USE Northwind GO -- dbo.MaxProductID SELECT dbo.MaxProductID() AS MaxProductID GO -- dbo.WhoWhere SELECT dbo.WhoWhere() AS [Who from Where] GO -- Create a table using WHoWhere -- As a DEFAULT constraint IF OBJECT_ID('TestWhoWhere') IS NOT NULL DROP TABLE TestWhoWhere GO CREATE TABLE TestWhoWhere( ID int IDENTITY (1,1) PRIMARY KEY, Name nvarchar(40), WhoWhere nvarchar(256) DEFAULT dbo.WhoWhere()) INSERT TestWhoWhere (Name) VALUES ('New record') SELECT * FROM TestWhoWhere GO -- Create a trigger to Update automatically -- Who did it and from where the change was done CREATE TRIGGER trWhoWhere ON TestWhoWhere AFTER INSERT, UPDATE AS UPDATE TestWhoWhere SET WhoWhere = dbo.WhoWhere() FROM TestWhoWhere T JOIN Inserted I ON T.ID = I.ID GO INSERT TestWhoWhere (Name, WhoWhere) VALUES ('More records', 'nobody from nowhere') SELECT * FROM TestWhoWhere -- dbo.Today SELECT dbo.Today() AS Today -- Inserting an Order in the present month INSERT Orders (CustomerID, OrderDate, ShippedDate) VALUES ('WELLI', '2000-11-23', '2000-12-12') -- Searching for Orders in the last 30 days SELECT OrderID, CustomerID, OrderDate, ShippedDate FROM Orders WHERE DATEDIFF(day, ShippedDate, dbo.Today()) < 30 GO -- dbo.PRand SELECT dbo.PRand() AS PRand GO -- dbo.Get3Rand SELECT dbo.Get3Rand() as Get3Rand1 GO SELECT dbo.Get3Rand() as Get3Rand2 GO -- dbo.NewPRand SELECT dbo.NewPRand() as NewPRand GO -- dbo.MediumProductUnitPrice SELECT dbo.MediumProductUnitPrice() GO -- Get the Medium 10 products -- by UnitPrice PRINT CHAR(10) + 'Medium 10 Products'+ CHAR(10) SELECT * FROM (SELECT TOP 6 UnitPrice, ProductID, ProductName FROM Products WHERE UnitPrice >= dbo.MediumProductUnitPrice() ORDER BY UnitPrice ASC) AS A UNION SELECT * FROM (SELECT TOP 5 UnitPrice, ProductID, ProductName FROM Products WHERE UnitPrice <= dbo.MediumProductUnitPrice() ORDER BY UnitPrice DESC) AS B ORDER BY UnitPrice ASC -- dbo.fn_FV SELECT 0.07 AS Rate, 36 AS NPer, 1000 AS Pmt, 10000 AS Pv, 0 AS Type, dbo.fn_FV(0.07, 36, 1000, 10000, 0) AS FV -- dbo.SimpleEncrypt SELECT dbo.SimpleEncrypt('Hello World') AS [Encrypted Message] -- dbo.SimpleDecrypt SELECT dbo.SimpleDecrypt('Hello World') AS [Decrypted version of a non-encrypted message] SELECT dbo.SimpleDecrypt(dbo.SimpleEncrypt('Hello World')) AS [Decrypted version of an encrypted message] -- dbo.ANG_HMS SELECT dbo.ANG_HMS(12.3456, 3, 'g') AS 'GRAD to HMS Using ANG_HMS' SELECT dbo.ANG_HMS(12.3456, 3, CHAR(176)) AS 'DEG to HMS Using ANG_HMS' -- dbo.GRAD_HMS SELECT dbo.GRAD_HMS(12.3456, DEFAULT) AS 'GRAD to HMS Using GRAD_HMS' -- dbo.DEG_HMS SELECT dbo.DEG_HMS(12.3456, 2) AS 'DEG to HMS Using DEG_HMS' -- dbo.HMS_ANG SELECT dbo.HMS_ANG('12g 34m 56s789', 'g') AS 'HMS to GRAD Using HMS_ANG' SELECT dbo.HMS_ANG('12 34m 56s789', ' ') AS 'HMS to DEG Using HMS_ANG' -- dbo.HMS_DEG SELECT dbo.HMS_DEG('12 34m 56s789') AS 'HMS to DEG Using HMS_DEG' -- dbo.HMS_GRAD SELECT dbo.HMS_GRAD('12g 34m 56s789') AS 'HMS to GRAD Using HMS_GRAD' SELECT dbo.GRAD_HMS(dbo.HMS_GRAD('12g 34m 56s789'), 3) AS 'HMS to GRAD and to HMS again using HMS_GRAD and GRAD_HMS' GO MaxProductID ------------ 77 Who from Where ------------------------------------------- SQLBYEXAMPLESQLAdmin FROM SQL Query Analyzer ID Name WhoWhere -------- --------------- --------------------------------------------- 1 New record SQLBYEXAMPLESQLAdmin FROM SQL Query Analyzer ID Name WhoWhere -------- --------------- --------------------------------------------- 1 New record SQLBYEXAMPLESQLAdmin FROM SQL Query Analyzer 2 More records SQLBYEXAMPLESQLAdmin FROM SQL Query Analyzer Today ------------------------------------------------------ 2000-12-26 00:00:00 OrderID CustomerID OrderDate ShippedDate ----------- ---------- ------------------------ ----------------------- 11109 WELLI 2000-11-23 00:00:00.000 2000-12-12 00:00:00.000 11110 WELLI 2000-11-23 00:00:00.000 2000-12-12 00:00:00.000 11111 WELLI 2000-11-23 00:00:00.000 2000-12-12 00:00:00.000 PRand ----------------------------------------------------- 0.40721851426491701 Get3Rand1 ----------- 475 Get3Rand2 ----------- 575 NewPRand ----------------------------------------------------- 5.2412061195157997E-2 --------------------- 19.4500 Medium 10 Products UnitPrice ProductID ProductName --------------------- ----------- ---------------------------------------- 18.0000 1 Chai 18.4000 40 Boston Crab Meat 19.0000 2 Chang 19.0000 36 Inlagd Sill 19.4500 44 Gula Malacca 19.5000 57 Ravioli Angelo 20.0000 49 Maxilaku 21.0000 11 Queso Cabrales 21.0000 22 Gustaf's Knäckebröd 21.0500 65 Louisiana Fiery Hot Pepper Sauce Rate NPer Pmt Pv Type FV ---- ----------- ----------- ----------- ----------- --------------------- .07 36 1000 10000 0 -263152.8817 Encrypted Message --------------------------- Spwwz+bz} wo Decrypted version of a non-encrypted message --------------------------------------------- =Zaad_LdgaY Decrypted version of an encrypted message --------------------------------------------- Hello World GRAD to HMS Using ANG_HMS ------------------------- 12 34m 55s999 DEG to HMS Using ANG_HMS ------------------------ 12 20m 44s159 GRAD to HMS Using GRAD_HMS -------------------------- 12 34m 55s0 DEG to HMS Using DEG_HMS ------------`------------ 12 20m 44s15 HMS to GRAD Using HMS_ANG ----------------------------------------------------- 12.345678899999999 HMS to DEG Using HMS_ANG ----------------------------------------------------- 12.582441388888888 HMS to DEG Using HMS_DEG ----------------------------------------------------- 12.582441388888888 HMS to GRAD Using HMS_GRAD ----------------------------------------------------- 12.345678899999999 HMS to GRAD and to HMS again using HMS_GRAD and GRAD_HMS -------------------------------------------------------- 12 34m 56s788 |
You can use fields as parameters when calling a user-defined function in any statement, such as in Listing 10.12.
USE Northwind GO -- Use the TotalPrice function to retrieve -- information from the Order Details table SELECT OrderID, ProductID, dbo.TotalPrice(Quantity, UnitPrice, Discount) AS TotalPrice FROM [Order Details] WHERE ProductID = 12 -- Use the SimpleEncrypt function to encrypt product names SELECT ProductID, dbo.SimpleEncrypt(ProductName) AS EncryptedName FROM Products WHERE CategoryID = 3 -- Use the SimpleDecrypt function to decrypt -- a field encrypted with the SimpleEncrypt function SELECT ProductID, dbo.SimpleDecrypt(EncryptedName) AS ProductName FROM ( SELECT ProductID, CategoryID, dbo.SimpleEncrypt(ProductName) AS EncryptedName FROM Products ) AS P WHERE CategoryID = 3 OrderID ProductID TotalPrice ----------- ----------- --------------------- 10266 12 346.5600 10439 12 456.0000 10536 12 427.5000 10543 12 969.0000 10633 12 1162.8000 10678 12 3800.0000 10695 12 152.0000 10718 12 1368.0000 10968 12 1140.0000 10979 12 760.0000 11018 12 760.0000 11046 12 722.0000 11049 12 121.6000 11077 12 72.2000 ProductID ProductName ----------- ----------------------------- 11 Queso Cabrales 12 Queso Manchego La Pastora 31 Gorgonzola Telino 32 Mascarpone Fabioli 33 Geitost 59 Raclette Courdavault 60 Camembert Pierrot 69 Gudbrandsdalsost 71 Flotemysost 72 Mozzarella di Giovanni |
A scalar user-defined function is a special form of stored procedure, and you can execute scalar user-defined functions in the same way you execute stored procedures. In this case, you can omit parameters that have a default value and alter the order of the parameters, providing the parameter name in the user-defined function call in the same way you do for stored procedures. Listing 10.13 shows some examples of how to call a scalar user-defined function using the EXECUTE statement.
USE Northwind GO --Declare a variable to receive the result of the UDF DECLARE @Total money -- Use EXECUTE and provide values for every parameter EXECUTE @Total = dbo.TotalPrice 12, 25.4, 0.0 SELECT @Total -- Use EXECUTE and omit the @Discount parameter -- because it has a default value EXECUTE @Total = dbo.TotalPrice 12, 25.4 SELECT @Total -- Use EXECUTE and omit the UDF owner, because it defaults to dbo EXECUTE @Total = TotalPrice 12, 25.4 SELECT @Total -- Use EXECUTE and provide values for every parameter -- specifying parameter names EXECUTE @Total = TotalPrice @Quantity = 12, @UnitPrice = 25.4, @Discount = 0.2 SELECT @Total -- Use EXECUTE and provide values for every parameter -- specifying parameter by order and by name on any order EXECUTE @Total = TotalPrice 12, @Discount = 0.2, @UnitPrice = 25.4 SELECT @Total --------------------- 304.8000 --------------------- 304.8000 --------------------- 304.8000 --------------------- 243.8400 --------------------- 243.8400 |
Tip
If you invoke a user-defined function with the EXECUTE statement, you do not have to qualify the function with the owner name. However, it is always more efficient to qualify the objects you use, because in this way SQL Server does not have to search first for an object owned by you, before searching for the same object owned by dbo.
You can use views in any DML statement as if they were tables. You can use a WHERE clause inside the view definition to limit the view results to specific rows, but this restriction is fixed, because it is part of the view definition. In the query where you use the view, you can use another WHERE clause to limit the search. In this case, SQL Server combines both WHERE clauses in the final query plan, after the view definition is merged with the outer query.
SQL Server 2000 gives you a new feature to create something similar to parameterized views: inline user-defined functions.
An inline user-defined function contains a single SELECT statement but, unlike views, it can use several parameters to restrict the query, providing an easier call interface than views.
Tip
You can identify inline user-defined functions because they return a table and their definition has only a single SELECT statement, without a BEGIN...END block.
You can use an inline user-defined function wherever a table or view is accepted:
In the SELECT clause of a SELECT statement, as part of a subquery that returns a single value (a single row and single column result set).
In the SET clause of an UPDATE statement, as part of a subquery that provides a single value for a field in the table to be updated.
In the FROM clause of any DML statement.
In the WHERE or HAVING clauses of any DML statement, as part of a subquery that returns a single value to be compared to any field or variable.
In the WHERE clause of any DML statement, as part of a subquery introduced by EXISTS or NOT EXISTS.
In the WHERE or HAVING clause of any DML statement, as part of a subquery used with the IN or NOT IN operators, as long as the subquery returns a single column.
To create an inline user-defined function, you must use the CREATE FUNCTION statement, as shown in Listing 10.14.
The first example creates the GetCustomersFromCountry function, which selects customers based in a specific country. This function returns a result set with the same structure as the original Customers table.
Based on the GetCustomersFromCountry function, you can create the GetCustomersFromUSA function, which retrieves customers based in the USA only.
The third example returns orders from a specific day using the GetOrdersFromDay function.
The fourth example creates the GetOrdersFromToday function to retrieve the list of today's orders. Note that you cannot base this function in GetOrdersFromDay function, because you cannot invoke an inline user-defined function using a scalar function as a parameter value.
Note
Inside the GetOrdersFromToday function you use the Today() scalar user-defined function, because Getdate() is not valid inside the definition of a user-defined function.
Later in this chapter, in the "Deterministic and Nondeterministic Functions" section, you will see the lists of functions that are not valid inside a user-defined function.
The next function is the OrdersWithValue function, which uses the TotalPrice scalar function to calculate the total value for every order. This inline user-defined function retrieves a result set with the same structure as the Orders table, plus the TotalValue field.
Based on the OrdersWithValue function, you can create the next two functions. OrdersByValue retrieves the orders where TotalValue is greater than a specific target total value. TopTenOrders returns the top 10 orders by TotalValue.
USE Northwind GO -- Returns customers from a specific country CREATE FUNCTION dbo.GetCustomersFromCountry (@country nvarchar(15)) RETURNS TABLE AS RETURN ( SELECT * FROM Customers WHERE Country = @country ) GO -- Returns USA-based customers CREATE FUNCTION dbo.GetCustomersFromUSA () RETURNS TABLE AS RETURN ( SELECT * FROM dbo.GetCustomersFromCountry(N'USA') ) GO -- Returns the orders from a specific day CREATE FUNCTION dbo.GetOrdersFromDay (@date as smalldatetime) RETURNS TABLE AS RETURN ( SELECT * FROM Orders WHERE DATEDIFF(day, OrderDate, @date) = 0 ) GO -- Returns orders from today CREATE FUNCTION dbo.GetOrdersFromToday () RETURNS TABLE AS RETURN ( SELECT * FROM Orders WHERE DATEDIFF(day, OrderDate, dbo.Today()) = 0 ) GO -- Returns Orders with the total order value CREATE FUNCTION dbo.OrdersWithValue () RETURNS TABLE AS RETURN ( SELECT O.*, TotalValue FROM Orders O JOIN ( SELECT OrderID, SUM(dbo.TotalPrice(Quantity, UnitPrice, Discount)) AS TotalValue FROM [Order Details] GROUP BY OrderID) AS OD ON O.OrderID = OD.OrderID ) GO -- Returns orders with a value greater than -- a specific target value CREATE FUNCTION dbo.OrdersByValue (@total money) RETURNS TABLE AS RETURN ( SELECT * FROM dbo.OrdersWithValue() WHERE TotalValue > @total ) GO -- Returns the top 10 orders by total value CREATE FUNCTION dbo.TopTenOrders () RETURNS TABLE AS RETURN ( SELECT TOP 10 WITH TIES * FROM dbo.OrdersWithValue() ORDER BY TotalValue DESC ) GO |
You can identify several parts of the CREATE FUNCTION syntax for inline user-defined function:
CREATE FUNCTION ownername.functionname, where you can specify the owner of the function and the name of the function.
(parameter_lists), where every parameter is identified by its name and the data type. The parameter list can be empty.
RETURNS TABLE, because inline user-defined functions always return a tablelike result set.
AS RETURN () marks the function definition body, which has to be enclosed inside the parenthesis block.
The SELECT statement, which defines the result set to return.
In essence, the creation of an inline user-define function is not very different from the creation of a view, except for the parameters list, which gives inline user-defined functions extra functionality not available in views.
Tip
Inline user-defined functions have the same restrictions that apply to views; look at Chapter 3, "Working with Tables and Views," for more information about them. One of these restrictions is that you cannot use ORDER BY, but you can use TOP 100 PERCENT...ORDER BY to produce the same result. Listing 10.15 shows how to implement this trick.
USE Northwind GO -- Returns Products ordered by ProductName CREATE FUNCTION dbo.OrderedProducts() RETURNS TABLE AS RETURN (SELECT TOP 100 PERCENT * FROM Products ORDER BY ProductName ASC) GO -- Test the function SELECT TOP 10 ProductID, ProductName FROM dbo.OrderedProducts() ProductID ProductName ----------- ---------------------------------------- 17 Alice Mutton 3 Aniseed Syrup 40 Boston Crab Meat 60 Camembert Pierrot 18 Carnarvon Tigers 1 Chai 2 Chang 39 Chartreuse verte 4 Chef Anton's Cajun Seasoning 5 Chef Anton's Gumbo Mix |
You can invoke an inline user-defined function in the same way you invoke a table or a view in a DML statement, with the only exception that you must use parentheses after the function name, even if there are not any parameters to use.
SQL Server merges the definition of the Inline function with the definition of the query where the function is invoked, to create a unique query plan. This is the same way that SQL Server uses views to execute Transact-SQL statements.
Listing 10.16 shows how to invoke the inline user-defined functions defined in Listing 10.14 in different ways.
USE Northwind GO PRINT CHAR(10) + 'Use GetCustomersFromCountry(''Mexico'')'+ CHAR(10) SELECT CustomerID, CompanyName, City FROM dbo.GetCustomersFromCountry('Mexico') PRINT CHAR(10) + 'Use GetCustomersFromUSA()'+ CHAR(10) Select CustomerID, CompanyName, City FROM dbo.GetCustomersFromUSA() PRINT CHAR(10) + 'Use GetCustomersFromCountry(''Mexico'') with the IN operator' + CHAR(10) SELECT OrderID, CONVERT(varchar(10), OrderDate, 120) AS OrderDate FROM Orders WHERE CustomerID IN (SELECT CustomerID FROM dbo.GetCustomersFromCountry('Mexico')) PRINT CHAR(10) + 'Joins OrdersByValue to Customers' + CHAR(10) SELECT CompanyName, OrderID, TotalValue, CONVERT(varchar(10), OrderDate, 120) AS OrderDate FROM dbo.OrdersByValue(10000) AS OBV JOIN Customers C ON OBV.CustomerID = C.CustomerID PRINT CHAR(10) + 'Joins TopTenOrders to Customers' + CHAR(10) SELECT CompanyName, OrderID, TotalValue, CONVERT(varchar(10), OrderDate, 120) AS OrderDate FROM dbo.TopTenOrders() AS OBV JOIN Customers C ON OBV.CustomerID = C.CustomerID Use GetCustomersFromCountry('Mexico') CustomerID CompanyName City ---------- ---------------------------------------- --------------- ANATR Ana Trujillo Emparedados y helados México D.F. ANTON Antonio Moreno Taquería México D.F. CENTC Centro comercial Moctezuma México D.F. PERIC Pericles Comidas clásicas México D.F. TORTU Tortuga Restaurante México D.F. Use GetCustomersFromUSA() CustomerID CompanyName City ---------- ---------------------------------------- --------------- GREAL Great Lakes Food Market Eugene HUNGC Hungry Coyote Import Store Elgin LAZYK Lazy K Kountry Store Walla Walla LETSS Let's Stop N Shop San Francisco LONEP Lonesome Pine Restaurant Portland OLDWO Old World Delicatessen Anchorage RATTC Rattlesnake Canyon Grocery Albuquerque SAVEA Save-a-lot Markets Boise SPLIR Split Rail Beer & Ale Lander THEBI The Big Cheese Portland THECR The Cracker Box Butte TRAIH Trail's Head Gourmet Provisioners Kirkland WHITC White Clover Markets Seattle Use GetCustomersFromCountry('Mexico') with the IN operator OrderID OrderDate ----------- ---------- 10259 1996-07-18 10276 1996-08-08 10293 1996-08-29 10304 1996-09-12 10308 1996-09-18 10319 1996-10-02 10322 1996-10-04 10354 1996-11-14 10365 1996-11-27 10474 1997-03-13 10502 1997-04-10 10507 1997-04-15 10518 1997-04-25 10535 1997-05-13 10573 1997-06-19 10576 1997-06-23 10625 1997-08-08 10676 1997-09-22 10677 1997-09-22 10682 1997-09-25 10759 1997-11-28 10842 1998-01-20 10856 1998-01-28 10915 1998-02-27 10926 1998-03-04 10995 1998-04-02 11069 1998-05-04 11073 1998-05-05 Joins OrdersByValue to Customers CompanyName OrderID TotalValue OrderDate ---------------------------------- ----------- ------------- ---------- Simons bistro 10417 11188.4000 1997-01-16 Rattlesnake Canyon Grocery 10479 10495.6000 1997-03-19 QUICK-Stop 10540 10191.7000 1997-05-19 QUICK-Stop 10691 10164.8000 1997-10-03 Königlich Essen 10817 10952.8450 1998-01-06 QUICK-Stop 10865 16387.5000 1998-02-02 Rattlesnake Canyon Grocery 10889 11380.0000 1998-02-16 Hungry Owl All-Night Grocers 10897 10835.2400 1998-02-19 Hanari Carnes 10981 15810.0000 1998-03-27 Save-a-lot Markets 11030 12615.0500 1998-04-17 Joins TopTenOrders to Customers CompanyName OrderID TotalValue OrderDate ---------------------------------- ----------- ------------- ---------- QUICK-Stop 10865 16387.5000 1998-02-02 Hanari Carnes 10981 15810.0000 1998-03-27 Save-a-lot Markets 11030 12615.0500 1998-04-17 Rattlesnake Canyon Grocery 10889 11380.0000 1998-02-16 Simons bistro 10417 11188.4000 1997-01-16 Königlich Essen 10817 10952.8450 1998-01-06 Hungry Owl All-Night Grocers 10897 10835.2400 1998-02-19 Rattlesnake Canyon Grocery 10479 10495.6000 1997-03-19 QUICK-Stop 10540 10191.7000 1997-05-19 QUICK-Stop 10691 10164.8000 1997-10-03 |
You can update, insert, or delete rows in tables through inline user-defined functions with the same limits as updating, inserting, or deleting rows in tables through views. Listing 10.17 shows an example of how to insert, update, and delete rows in the Orders table through the GetOrdersWithValue inline user-defined function.
USE Northwind GO PRINT CHAR(10) + 'Before the Insert'+ CHAR(10) SELECT CustomerID, CompanyName, Country FROM dbo.GetCustomersFromUsa() WHERE CustomerID > 'W' INSERT dbo.GetCustomersFromUSA() (CustomerID, CompanyName, Country) VALUES ('ZZZZZ', 'Dummy Customer', 'USA') PRINT CHAR(10) + 'After the Insert'+ CHAR(10) SELECT CustomerID, CompanyName, Country FROM dbo.GetCustomersFromUsa() WHERE CustomerID > 'W' UPDATE dbo.GetCustomersFromUSA() SET CompanyName = 'New Customer' WHERE CustomerID = 'ZZZZZ' PRINT CHAR(10) + 'After the Update'+ CHAR(10) SELECT CustomerID, CompanyName, Country FROM dbo.GetCustomersFromUsa() WHERE CustomerID > 'W' DELETE dbo.GetCustomersFromUSA() WHERE CustomerID = 'ZZZZZ' PRINT CHAR(10) + 'After the Delete'+ CHAR(10) SELECT CustomerID, CompanyName, Country FROM dbo.GetCustomersFromUsa() WHERE CustomerID > 'W' GO DECLARE @ID int PRINT CHAR(10) + 'Before the Insert'+ CHAR(10) SELECT CustomerID, OrderID, TotalValue, OrderDate FROM dbo.OrdersWithValue() WHERE CustomerID = 'VINET' INSERT dbo.OrdersWithValue() (CustomerID, OrderDate) SELECT 'VINET', dbo.Today() -- Retrieve the latest Identity value in this session SET @ID = SCOPE_IDENTITY() INSERT [Order Details] (OrderID, ProductID, Quantity, UnitPrice, Discount) SELECT @ID, 28, 10, UnitPrice, 0.1 FROM Products WHERE ProductID = 28 PRINT CHAR(10) + 'After the Insert'+ CHAR(10) SELECT CustomerID, OrderID, TotalValue, OrderDate FROM dbo.OrdersWithValue() WHERE CustomerID = 'VINET' DELETE [Order Details] WHERE OrderID = @ID DELETE Orders WHERE OrderID = @ID PRINT CHAR(10) + 'After the Delete'+ CHAR(10) SELECT CustomerID, OrderID, TotalValue, OrderDate FROM dbo.OrdersWithValue() WHERE CustomerID = 'VINET' Before the Insert CustomerID CompanyName Country ---------- ---------------------------------------- --------------- WHITC White Clover Markets USA After the Insert CustomerID CompanyName Country ---------- ---------------------------------------- --------------- WHITC White Clover Markets USA ZZZZZ Dummy Customer USA After the Update CustomerID CompanyName Country ---------- ---------------------------------------- --------------- WHITC White Clover Markets USA ZZZZZ New Customer USA After the Delete CustomerID CompanyName Country ---------- ---------------------------------------- --------------- WHITC White Clover Markets USA Before the Insert CustomerID OrderID TotalValue OrderDate ---------- ----------- --------------------- ----------------------- VINET 10248 440.0000 1996-07-04 00:00:00.000 VINET 10274 538.6000 1996-08-06 00:00:00.000 VINET 10295 121.6000 1996-09-02 00:00:00.000 VINET 10737 139.8000 1997-11-11 00:00:00.000 VINET 10739 240.0000 1997-11-12 00:00:00.000 After the Insert CustomerID OrderID TotalValue OrderDate ---------- ----------- --------------------- ----------------------- VINET 10248 440.0000 1996-07-04 00:00:00.000 VINET 10274 538.6000 1996-08-06 00:00:00.000 VINET 10295 121.6000 1996-09-02 00:00:00.000 VINET 10737 139.8000 1997-11-11 00:00:00.000 VINET 10739 240.0000 1997-11-12 00:00:00.000 VINET 11118 410.4000 2000-12-26 00:00:00.000 After the Delete CustomerID OrderID TotalValue OrderDate ---------- ----------- --------------------- ----------------------- VINET 10248 440.0000 1996-07-04 00:00:00.000 VINET 10274 538.6000 1996-08-06 00:00:00.000 VINET 10295 121.6000 1996-09-02 00:00:00.000 VINET 10737 139.8000 1997-11-11 00:00:00.000 VINET 10739 240.0000 1997-11-12 00:00:00.000 |
Note
You cannot delete data from the OrdersWithValue function, because it joins multiple tables.
Multistatement table-valued user-defined functions (or table-valued functions) are similar to inline user-defined functions. Actually, you can use them in the same scenarios, but you are not restricted to defining them as a single SELECT statement.
A table-valued function returns a single result set—the contents of a table variable with a predefined format specified in the function declaration.
Caution
The result of a table-valued function is always read-only. The result of an inline user-defined function can be read-only or not, depending on the SELECT statement used to define the function.
As mentioned in the preceding section, inline user-defined functions are similar to views in both structure and use. Table-valued functions are similar to stored procedures, but they return a single result set.
You can define parameters in a table-valued function, as you did with scalar user-defined functions and stored procedures. However, you cannot create an OUTPUT parameter in a user-defined function.
To create a table-valued function, you must use the CREATE FUNCTION statement. Listing 10.18 shows functions similar to those in Listing 10.14, but in table-valued versions.
USE Northwind GO CREATE FUNCTION dbo.tv_GetCustomersFromCountry (@country nvarchar(15)) RETURNS @List TABLE (CustomerID nchar(5), CompanyName nvarchar(40), Country nvarchar(15)) AS BEGIN INSERT @List SELECT CustomerID, CompanyName, Country FROM Customers WHERE Country = @country RETURN END GO -- Returns USA-based customers CREATE FUNCTION dbo.tv_GetCustomersFromUSA () RETURNS @List TABLE (CustomerID nchar(5), CompanyName nvarchar(40), Country nvarchar(15)) AS BEGIN INSERT @List SELECT CustomerID, CompanyName, Country FROM dbo.GetCustomersFromCountry(N'USA') RETURN END GO -- Returns the orders from a specific day CREATE FUNCTION dbo.tv_GetOrdersFromDay (@date as smalldatetime) RETURNS @list TABLE (OrderID int, OrderDate datetime) AS BEGIN INSERT @List SELECT OrderID, OrderDate FROM Orders WHERE DATEDIFF(day, OrderDate, @date) = 0 RETURN END GO -- Returns orders from today CREATE FUNCTION dbo.tv_GetOrdersFromToday () RETURNS @list TABLE (OrderID int, OrderDate datetime) AS BEGIN INSERT @List SELECT OrderID, OrderDate FROM Orders WHERE DATEDIFF(day, OrderDate, dbo.Today()) = 0 RETURN END GO -- Returns Orders with the total order value CREATE FUNCTION dbo.tv_OrdersWithValue () RETURNS @list TABLE (OrderID int, CustomerID nchar(5), OrderDate datetime, TotalValue money) AS BEGIN INSERT @List SELECT O.OrderID, CustomerID, OrderDate, TotalValue FROM Orders O JOIN ( SELECT OrderID, SUM(dbo.TotalPrice(Quantity, UnitPrice, Discount)) AS TotalValue FROM [Order Details] GROUP BY OrderID) AS OD ON O.OrderID = OD.OrderID RETURN END GO -- Returns orders with a value greater than -- a specific target value CREATE FUNCTION dbo.tv_OrdersByValue (@total money) RETURNS @list TABLE (OrderID int, CustomerID nchar(5), OrderDate datetime, TotalValue money) AS BEGIN INSERT @List SELECT OrderID, CustomerID, OrderDate, TotalValue FROM dbo.OrdersWithValue() WHERE TotalValue > @total RETURN END GO -- Returns the top 10 orders by total value CREATE FUNCTION dbo.tv_TopTenOrders () RETURNS @list TABLE (OrderID int, CustomerID nchar(5), OrderDate datetime, TotalValue money) AS BEGIN INSERT @List SELECT TOP 10 WITH TIES OrderID, CustomerID, OrderDate, TotalValue FROM dbo.OrdersWithValue() ORDER BY TotalValue DESC RETURN END GO |
You can identify several parts in the CREATE FUNCTION syntax for table- valued functions:
CREATE FUNCTION ownername.functionname, where you can specify the owner of the function and the name of the function.
(parameter_lists), where every parameter is identified by its name and the data type. The parameter list can be empty. Parameters can have default values.
RETURNS @tablename TABLE, because table-valued functions always return a tablelike result set, which is the contents of the table variable defined in this line as @tablename.
(Field definition, ...), definition of every field on the @tablename table variable, following the same rules as declaring fields in a standard table object.
AS BEGIN...END marks the function definition body, which must be written inside the BEGIN END block.
The RETURN statement, standalone, which sends the table variable back to the invoking process as a result set.
The contents of a table-valued function are similar to a stored procedure that uses a temporary table to store intermediate results and select from that temporary table at the end.
Tip
Inside scalar or table-valued user-defined functions you cannot create temporary tables; instead, you can use table variables. However, it is a good practice to replace temporary tables with table variables in stored procedures and scripts whenever this might be possible.
Looking at the examples in Listing 10.18, you could say that table-valued functions are similar to inline user-defined functions, except that you must declare the fields of the returning table explicitly. However, table-valued functions give you far more flexibility than inline user-defined functions, in a similar way as stored procedures give you more flexibility than views.
Listing 10.19 shows two more complex examples in which the creation of a table-valued function solves the problem.
The first function is called MakeList, and it converts a string with several items into a single column result set where every item is stored in a single row. You can specify any separator to divide values, or use the default '|'. This function can be useful in a WHERE clause introduced with the IN operator.
The second function is a bit more complex. It produces a subset of a fact table from the Order Details table, adding some extra information as the CategoryName, ProductName, CompanyName, and OrderDate from the Categories, Products, Customers, and Orders tables, and the TotalValue using the TotalPrice scalar user-defined function.
To call this function, you can specify whether you want a full list (@Key IS NULL), a list for a specific order (@Key = 'ORD' and @ID = OrderID), a specific Customer (@Key = 'CUS' and @ID = CustomerID), a product (@Key = 'PRO' and ID = ProductID), or a category (@Key = 'CAT' and @ID = CategoryID).
At the beginning of the function, you can find a sequence of IF structures to apply the most efficient method to every case to prepare the initial list of rows to return.
At the end of the OrderDetailsComplete function, fill the missing information with values coming from other tables.
USE Northwind GO -- Converts a string containing a list of items -- into a single column table where every item -- is in a separate row -- using any character as a separator CREATE FUNCTION dbo.MakeList (@ParamArray as nvarchar(4000), @Separator as char(1) = '|') RETURNS @List TABLE (Item sql_variant) AS BEGIN DECLARE @pos int, @pos0 int SET @pos0 = 0 WHILE 1=1 BEGIN SET @pos = CHARINDEX(@Separator, @ParamArray, @pos0 + 1) INSERT @List SELECT CASE @pos WHEN 0 THEN SUBSTRING(@ParamArray, @pos0+1, LEN(@ParamArray) - @pos -1) ELSE SUBSTRING(@ParamArray, @pos0+1, @pos - @pos0-1) END IF @pos = 0 BREAK SET @pos0 = @pos END RETURN END GO -- Produces a list of orders -- with full descriptive information -- ProductName, CategoryName, CompanyName -- OrderDate and TotalValue -- with every primary key to link to other tables -- The list can be produced for every -- Order (@Key = 'ORD'), -- Product (@Key = 'PRO'), -- Customer (@Key = 'CUS'), -- Category (@Key = 'CAT') -- Full List (@Key NOT IN ('ORD', 'PRO', 'CUS', 'CAT')) CREATE FUNCTION dbo.OrderDetailsComplete (@ID sql_variant = NULL, @Key char(3) = NULL) RETURNS @Details TABLE (OrderID int, ProductID int, CustomerID nchar(5) NULL, CategoryID int NULL, OrderDate smalldatetime NULL, Value money NULL, Category nvarchar(15) NULL, Product nvarchar(40) NULL, Company nvarchar(40) NULL) AS BEGIN IF @Key = 'ORD' BEGIN INSERT @Details (OrderID, ProductID, Value) SELECT OrderID, ProductID, dbo.TotalPrice(Quantity, UnitPrice, Discount) FROM [Order Details] WHERE OrderID = @ID END ELSE IF @Key = 'PRO' BEGIN INSERT @Details (OrderID, ProductID, Value) SELECT OrderID, ProductID, dbo.TotalPrice(Quantity, UnitPrice, Discount) FROM [Order Details] WHERE ProductID = @ID END ELSE IF @Key = 'CUS' BEGIN INSERT @Details (OrderID, ProductID, CustomerID, Value) SELECT O.OrderID, ProductID, CustomerID, dbo.TotalPrice(Quantity, UnitPrice, Discount) FROM [Order Details] OD JOIN Orders O ON O.OrderID = OD.OrderID WHERE CustomerID = @ID END ELSE IF @Key = 'CAT' BEGIN INSERT @Details (OrderID, ProductID, CategoryID, Value) SELECT OD.OrderID, P.ProductID, CategoryID, dbo.TotalPrice(Quantity, OD.UnitPrice, Discount) FROM [Order Details] OD JOIN Products P ON P.ProductID = OD.ProductID WHERE CategoryID = @ID END ELSE BEGIN INSERT @Details (OrderID, ProductID, Value) SELECT OrderID, ProductID, dbo.TotalPrice(Quantity, UnitPrice, Discount) FROM [Order Details] END UPDATE D SET D.CustomerID = O.CustomerID, D.OrderDate = O.OrderDate FROM @Details D JOIN Orders O ON O.OrderID = D.OrderID WHERE D.CustomerID IS NULL UPDATE D SET D.CategoryID = P.CategoryID, D.Product = P.ProductName FROM @Details D JOIN Products P ON P.ProductID = D.ProductID WHERE D.CategoryID IS NULL UPDATE D SET D.Category = C.CategoryName FROM @Details D JOIN Categories C ON C.CategoryID = D.CategoryID UPDATE D SET D.Company = C.CompanyName FROM @Details D JOIN Customers C ON C.CustomerID = D.CustomerID RETURN END GO |
You can invoke table-value functions the same way you do for inline user-defined functions. Listing 10.20 shows examples of how to call some functions defined in Listing 10.18.
Note
Listing 10.20 is a revised version of Listing 10.16, calling table-value functions instead of inline user-defined functions. The output is the same as in Listing 10.16; therefore, it is not necessary to show it again.
USE Northwind GO PRINT CHAR(10) + 'Use tv_GetCustomersFromCountry(''Mexico'')'+ CHAR(10) SELECT CustomerID, CompanyName, Country FROM dbo.tv_GetCustomersFromCountry('Mexico') PRINT CHAR(10) + 'Use tv_GetCustomersFromUSA()'+ CHAR(10) Select CustomerID, CompanyName, Country FROM dbo.tv_GetCustomersFromUSA() PRINT CHAR(10) + 'Use tv_GetCustomersFromCountry(''Mexico'') with the IN operator' + CHAR(10) SELECT OrderID, CONVERT(varchar(10), OrderDate, 120) AS OrderDate FROM Orders WHERE CustomerID IN (SELECT CustomerID FROM dbo.tv_GetCustomersFromCountry('Mexico')) PRINT CHAR(10) + 'Joins tv_OrdersByValue to Customers' + CHAR(10) SELECT CompanyName, OrderID, TotalValue, CONVERT(varchar(10), OrderDate, 120) AS OrderDate FROM dbo.tv_OrdersByValue(10000) AS OBV JOIN Customers C ON OBV.CustomerID = C.CustomerID PRINT CHAR(10) + 'Joins tv_TopTenOrders to Customers' + CHAR(10) SELECT CompanyName, OrderID, TotalValue, CONVERT(varchar(10), OrderDate, 120) AS OrderDate FROM dbo.tv_TopTenOrders() AS OBV JOIN Customers C ON OBV.CustomerID = C.CustomerID |
However, the way SQL Server executes table-valued functions and inline user-defined functions is completely different:
The definition of inline user-defined functions is merged with the definition of the outer query, producing a single query plan, in which you cannot find any traces of the user-defined function. Figure 10.1 shows the query plan of this query, in which you can see an Index Scan on the Customers table's clustered index. Every time you use an inline user-defined function its definition must be merged again with the outer query, producing a new query plan.
The first time you call a table-valued function, it is compiled and a query plan is placed in memory. Every time you use this function, the calling query forces the execution of the saved query plan and the function returns the table variable, which will be used in the outer query. Figure 10.2 shows the query plan of this execution, simpler than the one from Figure 10.1, in which you can see a Table Scan on the table variable returned from the function. In this case, most of the work is done in a separate query plan, corresponding to the table- valued function.
3.19.75.133