BlackWasp
SQL Server
SQL 2005+

SQL Server User-Defined Functions

The fifty-ninth part of the SQL Server Programming Fundamentals tutorial examines the creation of user-defined functions. These are custom functions that can be used within scripts and procedures in the same manner as standard functions.

User-Defined Functions

Earlier in this tutorial we examined several different types of function, including aggregations, ranking functions, casting and conversion, mathematical functions, string processors and date and time functions. These functions accept zero or more parameters and return a single value. Sometimes you will find that the built-in functions do not meet your requirements. In these cases, you can create your own user-defined functions.

User-defined functions are similar to those provided by SQL Server as standard. They are given a name and, optionally, a set of parameters. User-defined functions are created for a database and can be used in scripts, stored procedures, triggers and other user-defined functions that are defined within the database. As with stored procedures, they help to modularise your Transact-SQL (T-SQL) code and improve maintainability by allowing you to centralise logic. For example, you could create a function that returns a standard tax rate. If the tax rules change, the function can be modified and all T-SQL that utilises the function would use the new tax rate automatically.

User-defined functions can be separated into two main categories. These are scalar functions and table-valued functions.

Scalar Functions

Scalar functions are similar to the functions we have used in earlier articles. They can optionally accept one or more parameters and return a single value. The return value is a standard data type but cannot be a Text, NText, Image or Timestamp.

Table-Valued Functions

The second type of user-defined function is the table-valued function. As the name suggests, a table-valued function returns a table variable. Table-valued functions are often used as "parameterised views" as they can be included in place of tables in a query and can accept arguments. Such queries select from the rows provided in the function's result.

Table-valued functions can be further categorised into inline and multi-function variations. These determine the syntax used when creating the function but do not change the way in which it is called. Inline functions contain a single statement, which must be a SELECT. The results of the query become the return value of the function.

Multi-function table-valued functions can include many statements, similar to those in a stored procedure. The table structure to be returned is defined within the function, rather than being implicitly created to hold a query's results. A batch of statements is used to populate the table variable before it is returned.

Creating User-Defined Functions

In this article we will create some user-defined functions in the JoBS database. This is the database that we have created throughout the course of the tutorial. You can download a script to generate the database using the link at the top of this page.

Creating a Simple Scalar Function

The first function that we will create will be a simple scalar function that returns a tax rate. This could be used throughout a database when calculating the tax on sales. If the government changes the tax rate, it would require a single change to modify all of the functionality in the database that uses the percentage. As the rate is fixed, no parameters are required for this function.

The syntax for creating a function with no parameters is as follows:

CREATE FUNCTION function-name()
RETURNS return-type
AS
BEGIN
    statement-1
    statement-2
    .
    .
    statement-X
    RETURN return-value
END

The signature of the function contains two key elements. The function-name is a unique name for the new function and return-type defines the data type that will be returned. The function's body appears between BEGIN and END keywords and can include multiple statements. The return-value is the value that the function returns to its caller and must be of the type declared in the signature.

To create the simple TaxRate function and set the value that it returns to 17.5, execute the following:

CREATE FUNCTION TaxRate()
RETURNS NUMERIC(5,2)
AS
BEGIN
    RETURN 17.5
END

Calling a Function

User-defined scalar functions can be used in the same places you might use built-in functions. These include within scripts, queries, INSERT and UPDATE commands and stored procedures. To simply show the value returned by the TaxRate function, we can use the PRINT command as follows:

PRINT dbo.TaxRate() -- 17.50

Note the inclusion of "dbo." as a prefix to the function name. This is the schema name and must be provided for scalar functions. Schema names are used to separate elements of a database and provide different schemas for different users. In the JoBS database we have only used the default schema, named "dbo".

Modifying a Function

To modify an existing function you can use the ALTER FUNCTION command. The syntax for the command is identical to that of the CREATE FUNCTION command. For example, if the tax rate were to increase from 17.5% to 18.75%, the function could be updated as follows:

ALTER FUNCTION TaxRate()
RETURNS NUMERIC(5,2)
AS
BEGIN
    RETURN 18.75
END

Adding Parameters

Most functions will include parameters that determine the operation of the function and the eventual return value. Parameters can be added as a comma-separated list within the parentheses that follow the function name. The syntax is very similar to that of stored procedures.

The following function adds a single parameter that accepts a monetary value. It performs a calculation that adds the tax rate, defined in the earlier function, and returns the result.

CREATE FUNCTION ValueWithTax (@Value MONEY)
RETURNS MONEY
AS
BEGIN
    RETURN @Value + (@Value * dbo.TaxRate() / 100)
END

To demonstrate the new function, execute the following query. This returns all of the contracts from the database, showing the contract number, contract value and the value with the tax rate added.

SELECT ContractNumber, ContractValue, dbo.ValueWithTax(ContractValue) AS WithTax
FROM Contracts

Using Data

The body of a user-defined function can include statements that read data from tables, views or other functions. This allows the creation of very powerful, reusable functions for a database. In the following example, the function accepts three parameters. The first parameter is used to specify the ID of an engineer, which must match the primary key value of a row in the Engineers table. The next two parameters allow the specification of a number of hours of normal and overtime working. The function reads the engineer's pay rates and calculates the amount earned accordingly.

CREATE FUNCTION EngineerCost
(
    @EngineerId INT,
    @NormalHours NUMERIC(4, 2),
    @OvertimeHours NUMERIC(4, 2)
)
RETURNS MONEY
AS
BEGIN
    DECLARE @HourlyRate MONEY
    DECLARE @OvertimeRate MONEY

    SELECT
        @HourlyRate = HourlyRate,
        @OvertimeRate = OvertimeRate
    FROM
        Engineers
    WHERE
        EngineerId = @EngineerId

    RETURN (@HourlyRate * @NormalHours) + (@OvertimeRate * @OvertimeHours)
END

In the JoBS database, Engineer 1 earns £19.75 per standard hour and £29.63 per hour at the overtime rate. Execute the following three statements to see this data used to calculate sample payments.

PRINT dbo.EngineerCost(1, 10, 0)    -- 197.50
PRINT dbo.EngineerCost(1, 0, 10)    -- 296.30
PRINT dbo.EngineerCost(1, 10, 10)   -- 493.80

Creating an Inline Table-Valued Function

Inline table-valued functions are generally used to create "parameterised views". They do not include statements in a BEGIN / END block. Instead, the body of the function is a SELECT statement that may use the values passed in as arguments. The syntax for an inline table-valued function is as follows:

CREATE FUNCTION function-name
(
    param-1
    param-2
    .
    .
    param-X
)
RETURNS TABLE
AS
RETURN
    query

We can create a simple inline function using the following script. This function retrieves a list of jobs that have the same name. The name of job to locate is supplied using the parameter.

CREATE FUNCTION JobListByJobName (@JobName VARCHAR(100))
RETURNS TABLE
AS
RETURN
    SELECT * FROM JobList
    WHERE JobName = @JobName

Once created, a table-valued function can be used as the data source in a SELECT statement. For example, the following query retrieves all of the jobs where the JobName column contains "Electrical Socket Failure". NB: You do not need to include the schema name prefix for table-valued functions.

SELECT * FROM JobListByJobName('Electrical Socket Failure')

As the function is simply replacing a table or view in the query, other clauses can be added. The following example retrieves jobs that are electrical socket failures repaired on 2 February 2008.

SELECT * FROM JobListByJobName('Electrical Socket Failure')
WHERE VisitDate = '2008-03-02'

Creating a Multi-Statement Table-Valued Function

Multi-statement table-valued functions can be used to create very powerful functions that return table data. They include a set of statements contained between BEGIN and END keywords. The body can read information from the database and perform many other tasks that you could include in stored procedures.

When defining a multi-statement function, the return value is declared as a table variable and includes the full structure of the table that will be returned. The body of the function performs actions that fill the table variable with data. At the end of the processing, the RETURN statement is used without a value and the previously declared table variable is returned.

Earlier in the tutorial we created a stored procedure that used a cursor and a temporary table to build a list of customers and the profit that each had provided to the company. The stored procedure returned data but in a format that would be difficult to process further with T-SQL. We can gain flexibility by recreating this stored procedure as a function:

CREATE FUNCTION ProfitPerCustomer()
RETURNS @CustomerValue TABLE
(
    CustomerNumber INT,
    TotalContractValue MONEY,
    EngineerCost MONEY,
    PartsCost MONEY,
    Profit MONEY
)
AS
BEGIN
    DECLARE @CustomerNumber INT
    DECLARE @TotalContractValue MONEY
    DECLARE @EngineerCost MONEY
    DECLARE @PartsCost MONEY

    INSERT INTO @CustomerValue (CustomerNumber)
    SELECT CustomerNumber FROM Customers

    DECLARE CustomerCursor CURSOR FOR
    SELECT CustomerNumber FROM @CustomerValue

    OPEN CustomerCursor

    FETCH NEXT FROM CustomerCursor
    INTO @CustomerNumber

    WHILE @@FETCH_STATUS = 0
    BEGIN

        SELECT @TotalContractValue = sum(ContractValue) FROM Contracts C
        INNER JOIN CustomerAddresses A ON C.CustomerAddressId = A.AddressId
        WHERE A.CustomerNumber = @CustomerNumber

        SELECT @EngineerCost = sum(J.Duration * E. HourlyRate) FROM Jobs J
        INNER JOIN Engineers E ON J.EngineerId = E.EngineerId
        INNER JOIN Contracts C ON J.ContractNumber = C.ContractNumber
        INNER JOIN CustomerAddresses A ON C.CustomerAddressId = A.AddressId
        WHERE A.CustomerNumber = @CustomerNumber

        SELECT @PartsCost = sum(TotalCost) FROM UsedParts P
        INNER JOIN Jobs J ON P.JobId = J.JobId
        INNER JOIN Contracts C ON J.ContractNumber = C.ContractNumber
        INNER JOIN CustomerAddresses A ON C.CustomerAddressId = A.AddressId
        WHERE A.CustomerNumber = @CustomerNumber

        UPDATE
            @CustomerValue
        SET
            TotalContractValue = isnull(@TotalContractValue, 0),
            EngineerCost = isnull(@EngineerCost, 0),
            PartsCost = isnull(@PartsCost, 0)
        WHERE
            CustomerNumber = @CustomerNumber	

        FETCH NEXT FROM CustomerCursor
        INTO @CustomerNumber
    END

    CLOSE CustomerCursor
    DEALLOCATE CustomerCursor

    UPDATE
        @CustomerValue
    SET
        Profit = TotalContractValue - EngineerCost - PartsCost

    RETURN
END

With the function created, the data can be used as if it were in a table. The statement below returns the data from the function. We could add further clauses to filter, sort, group and summarise this information.

SELECT * FROM ProfitPerCustomer()
Link to this Page20 February 2010
TwitterTwitter RSS Feed RSS