posted on Tuesday, February 28, 2006 10:00 PM by amachanic

Running sums yet again: SQLCLR saves the day!

Back again!  Fourth post for the month of February, making this my best posting month in, well, months.  Expect this trend to continue.

After yesterday's post on running sums and the evils of cursors, Jamie Thompson came up with a faster solution than the curser I posted.  Alas, Jamie's solution uses an undocumented form of UPDATE syntax, and I am really not comfortable using it.  So I set out to find still another solution.  As promised at the end of my last post, SQLCLR is where I looked.  And my instinct proved correct.

Jamie's solution runs in 4 seconds on my laptop.  Compared to 14 seconds for the cursor I posted, that's a great enhancement.  But I knew that we could do better still, and without undocumented syntax and temp tables.

The answer?  A SQLCLR stored procedure.  Same logic as the cursor: Pull back the data in order, then loop over the rows and maintain the running sum in a variable.  But thanks to the SqlPipe's SendResults methods, we don't need a temporary table for this one -- the results can be sent back one row at a time, and will still show up on the client as a single result set.

Here's how I did it:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;


public partial class StoredProcedures
{
    [Microsoft.SqlServer.Server.SqlProcedure]
    public static void TransactionHistoryRunningSum()
    {
        using (SqlConnection conn = new SqlConnection("context connection=true;"))
        {
            SqlCommand comm = new SqlCommand();
            comm.Connection = conn;
            comm.CommandText = @"" +
                "SELECT TransactionID, ActualCost " +
                "FROM Production.TransactionHistory " +
                "ORDER BY TransactionID";

            SqlMetaData[] columns = new SqlMetaData[3];
            columns[0] = new SqlMetaData("TransactionID", SqlDbType.Int);
            columns[1] = new SqlMetaData("ActualCost", SqlDbType.Money);
            columns[2] = new SqlMetaData("RunningTotal", SqlDbType.Money);

            decimal RunningSum = 0;

            SqlDataRecord record = new SqlDataRecord(columns);

            SqlContext.Pipe.SendResultsStart(record);

            conn.Open();

            SqlDataReader reader = comm.ExecuteReader();

            while (reader.Read())
            {
                decimal ActualCost = (decimal)reader[1];
                RunningSum += ActualCost;

                record.SetInt32(0, (int)reader[0]);
                record.SetDecimal(1, ActualCost);              
                record.SetDecimal(2, RunningSum);

                SqlContext.Pipe.SendResultsRow(record);
            }

            SqlContext.Pipe.SendResultsEnd();
        }
    }
};

Results?  3 seconds on my laptop.  25% better than the previous best solution.  Not bad!

And, I even did better than that.  By creating a CLR table-valued user-defined function that uses a custom class wrapping a SqlDataReader, I was able to get this operation down to 2 seconds on my laptop.  However, due to restrictions related to passing around context connections, that solution can not use a context connection and is therefore highly suboptimal.  Until I figure out how to pass around a context connection (or if it's even possible), I'll keep that one off the blog.

So to recap: We certainly have not gotten rid of the cursor.  This SQLCLR solution is really just a cursor in disguise.  But we've built a better cursor, because this one doesn't require temporary tables.  And that I can live with--for now.

Thanks to Jamie Thompson for prompting me to not wait several weeks before following up as I usually do with these posts!

Comments

# SS: Calculate Running Totals (2) @ Wednesday, March 01, 2006 3:12 AM

You may have read my post yeterday talking about a method of calculating running totals that was based...

Anonymous

# Interesting Finds @ Wednesday, March 01, 2006 5:52 AM

Anonymous

# Interesting Finds @ Wednesday, March 01, 2006 5:55 AM

Anonymous

# re: Running sums yet again: SQLCLR saves the day! @ Tuesday, March 07, 2006 3:58 AM

This update feature is not undocumented:

@variable = column = expression [ ,...n ]

ms-help://MS.SQLCC.v9/MS.SQLSVR.v9.en/tsqlref9/html/40e63302-0c68-4593-af3e-6d190181fee7.htm

István Sáfár

# re: Running sums yet again: SQLCLR saves the day! @ Saturday, March 11, 2006 2:34 AM

Interesting post, Adam, and a very creative solution.

I was hoping that a recursive Common Table Expression might give your SQL CLR a run for its money, but alas, the maximum number of recursions it handles tops out at 32,767.

This took four seconds to run on my laptop with an empty data cache; return was sub-second once the data was cached.

For what it's worth, here's the syntax (I'm obviously taking some liberties here, but I was looking to compare runtimes):

with RunningTotal (TransactionId, ActualCost, RunningTotal)
as
(
select TransactionId, ActualCost, ActualCost as RunningTotal
from Production.TransactionHistory
where transactionid = (select min(transactionid) from Production.TransactionHistory)
union all
select th.TransactionId, th.ActualCost, RunningTotal + th.ActualCost as RunningTotal
from Production.TransactionHistory th
inner join RunningTotal r
on r.TransactionId + 1 = th.TransactionId
)
select * from RunningTotal

Ward Pond

# Multi level aggregates in SQL 2005 @ Sunday, March 12, 2006 4:17 PM

I was looking to answer a users question on the forums this evening when I realised there is another...

Anonymous

# Running Sums the sequel part x @ Sunday, March 12, 2006 5:45 PM

If you've been following the trillogy (Adams posts 1,2 Jamies post ) of attempts at getting a running...

Anonymous

# Running Sums the sequel part x @ Tuesday, May 23, 2006 5:08 AM

If you've been following the trillogy (Adams posts 1,2 Jamies post ) of attempts at getting a running...

Anonymous

# Multi level aggregates in SQL 2005 @ Tuesday, May 23, 2006 5:08 AM

I was looking to answer a users question on the forums this evening when I realised there is another...

Anonymous

# Running sums yet again: SQLCLR saves the day! @ Thursday, July 13, 2006 12:53 AM

Originally posted here.

Back again!  Fourth post for the month of February, making this my best...

Anonymous

# Running sums yet again: SQLCLR saves the day! @ Monday, January 08, 2007 2:26 PM

Originally posted here . Back again! Fourth post for the month of February, making this my best posting

Anonymous

# Running Sums the sequel part x @ Saturday, August 11, 2007 7:54 PM

If you've been following the trillogy (Adams posts 1 , 2 Jamies post ) of attempts at getting a running

Anonymous