Wednesday, August 13, 2008

Recursive SQL User Defined Functions

In this article we will cover the new feature of SQL Server 2000 and the ability to create user defined functions. We will focus on creating recursive queries in this article to extend upon previous articles.
Being the developer for this directory, I had a need to perform several recursive methods. Any time you are creating a tree like data structure like directories, org charts, etc. You will need to perform some kind of recursion. In this example, I wanted to be able to provide a count of all descendents sites under a particular category as you see on WWWCoder.com when navigating the site. Each category contains a count of descendent sites of all categories under that specific category.
In the past there were several ways to perform this, one simple method was to create a field in the categories table that would get incremented each time a site was added. This was accomplished using a recursive method in the ASP.Net code that would create a new connection to the database each time a new record was added. It did eliminate the need to call a recursive function for each time a person requested a page to display the category navigation. Basically the method performed would accept a category id and a string value to increment or decrement the count contained in the site count field:

       
Public Sub UpdateParentCount(ByVal ParentID As Integer, ByVal AddSubtract As String)
 Dim SQLQ As String = "UPDATE Categories Set SiteCount = SiteCount " & AddSubtract & " 1"
 If AddSubtract = "+" Then 'add a new icon.
  SQLQ = SQLQ & ", DateSiteAdded = '" & Date.Today & "'"
 End If
 SQLQ = SQLQ & " WHERE CategoryID=" & ParentID
 Dim secondConnection As New SqlConnection(GetDBConnectionString)
 Dim secondCommand As New SqlCommand(SQLQ, secondConnection)
 secondCommand.CommandType = CommandType.Text
 secondConnection.Open()
 secondCommand.ExecuteNonQuery()
 secondConnection.Close()
 secondConnection = Nothing
 Dim SQLQ2 As String = "SELECT ParentID FROM Categories Where CategoryID = " & ParentID
 Dim myConnection As New SqlConnection(GetDBConnectionString)
 Dim myCommand As New SqlCommand(SQLQ2, myConnection)
 myConnection.Open()
 Dim result As SqlDataReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection)
 If result.Read Then
  UpdateParentCount(result("ParentID"), AddSubtract)
 End If
 myConnection.Close()
 myConnection = Nothing
End Sub 
 
 

This wasn't too bad of solution, since I was able to eliminate the hits on the database for the count. The problem here is a count can become incorrect if any changes are performed outside of the application.
The solution for me had to meet the following requirements: reduce traffic between the Web server and the database server, and make sure an accurate count is always available in the database regardless of what modifies the records that it contains. In order to accomplish this, the count method was moved out of the ASP.Net code and into the database. SQL Server 2000 supports User Defined Functions that can be called from a stored procedure, in addition, the function can be recursive up to 32 levels. Since I can't see a need to go beyond 32 levels deep of categories, I opted to use the functions for creating the count. Here is an example of using a function from within a stored procedure:

ALTER procedure GetCategories
 
@ParentID   int
 
as
 
BEGIN
SELECT 
    CategoryID, CategoryName, Path, SiteCount, DateSiteAdded, 
    ParentID, SortColumn, dbo.CountChildren(CategoryID, 0) 
    As CulCount
FROM 
    Categories  
WHERE 
    ParentID = @ParentID
ORDER BY 
    SortColumn, CategoryName
END 
 

You'll notice in the SQL stored procedure's select statement there is a call to a function called CountChildren, in this function we pass the category id of the current category and the current cumulative count of the sites within the category.

ALTER FUNCTION dbo.CountChildren
(@id int, @cChildren int) 
RETURNS bigint 
AS
BEGIN
 
IF EXISTS (SELECT     
    Sites.SiteCatID
FROM         
    dbo.Categories 
INNER JOIN
    dbo.Sites 
ON 
    dbo.Categories.CategoryID = dbo.Sites.SiteCatID
WHERE 
    dbo.Categories.ParentID = @id OR dbo.Sites.SiteCatID = @id)
BEGIN 
   SET @cChildren = @cChildren + (
     SELECT 
        Count(SiteCatID) 
        FROM 
            Sites 
        WHERE 
            SiteCatID = @id AND SiteActive = 1)
  SELECT 
              @cChildren = dbo.CountChildren(CategoryID, @cChildren) 
            FROM 
              Categories 
            WHERE 
              ParentID = @id 
END 
  RETURN @cChildren 
END  
 
 

As you can see the function calls itself just as a recursive function in VB.Net would do, each time incrementing the cumulative count of all descendents of a particular category. In the end we have all the information generated on the SQL machine, and then it returns what we need without having to call a recursive method in the ASP.Net page and generate all the additional database calls over the network.
By: Patrick Santry, Microsoft MVP (ASP/ASP.NET), developer of this site, author of books on Web technologies, and member of the DotNetNuke core development team. If you're interested in the services provided by Patrick, visit his company Website at Santry.com.

Source: http://www.wwwcoder.com/

No comments: