In this test I want to examine the performance implications of passing a relatively long set of data to a SQL Stored Procedure. I compare performance for passing that data as XML, as Table variable or a list of NVARCHAR variables. The test consists of 10 000 iterations. There is nothing in the procedure, so I am not testing the cost for querying the data but only the cost for passing the data to the SqlServer stored procedure. Following are my procedures.

Parameter as XML:

CREATE proc [dbo].[Test_Xml_Parameter]
    @Xml XML
as
    SELECT 'Return'
GO

Parameter as Table:

CREATE TYPE TestTableType AS TABLE 
(
    ID INT,
    Name NVARCHAR(100), 
    Description NVARCHAR(2000)
);
GO
CREATE proc [dbo].[Test_Table_Parameter]
    @Tbl TestTableType READONLY
as
    SELECT 'Return'
GO

Parameter as VARCHARs

CREATE proc [dbo].[Test_Simple_Parameter]
    @Var1 NVARCHAR(2000) = NULL,
    @Var2 NVARCHAR(2000) = NULL,
    @Var3 NVARCHAR(2000) = NULL,
    @Var4 NVARCHAR(2000) = NULL,
    @Var5 NVARCHAR(2000) = NULL,
    @Var6 NVARCHAR(2000) = NULL,
    @Var7 NVARCHAR(2000) = NULL,
    @Var8 NVARCHAR(2000) = NULL,
    @Var9 NVARCHAR(2000) = NULL,
    @Var10 NVARCHAR(2000) = NULL,
    @Var11 NVARCHAR(2000) = NULL,
    @Var12 NVARCHAR(2000) = NULL,
    @Var13 NVARCHAR(2000) = NULL,
    @Var14 NVARCHAR(2000) = NULL,
    @Var15 NVARCHAR(2000) = NULL,
    @Var16 NVARCHAR(2000) = NULL,
    @Var17 NVARCHAR(2000) = NULL,
    @Var18 NVARCHAR(2000) = NULL,
    @Var19 NVARCHAR(2000) = NULL,
    @Var20 NVARCHAR(2000) = NULL
AS
    SELECT 'Return'
GO

This is the method I use to connect to the database:

private static long GetScalarSp(string spName, SqlParameter[] parameters)
{
    object returnObj = null;
    Stopwatch watch = new Stopwatch();
    using(var conn = new SqlConnection(CONNECTION_STRING))
    {
        var command = new SqlCommand(spName, conn);
        command.Parameters.AddRange(parameters);
        command.CommandType = CommandType.StoredProcedure;
        conn.Open();
        
        watch.Start();
        returnObj = command.ExecuteScalar();
        watch.Stop();
    }
    return watch.ElapsedMilliseconds;
}

As you can see I only measure the time for the execution of the command. And these are the methods I use to instantiate my calls:

private long RunXml(int rows)
{
    var param = new SqlParameter();
    param.ParameterName = "@Xml";
    param.SqlDbType = SqlDbType.Xml;
    var sb = new StringBuilder();
    sb.Append(@"<Data>");
    var str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + DateTime.Now;
    for(var i = 0; i < rows; i++)
    {
        sb.AppendFormat(@"<Record ID=""{0}"" Name=""{0}"">{1}</Record>", i + 1, str);
    }
    sb.Append(@"</Data>");
    param.Value = sb.ToString();

    return GetScalarSp(@"Test_Xml_Parameter", new []{param});
}
private long RunSimple(int rows)
{
    var list = new List<SqlParameter>();
    var str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + DateTime.Now;
    for (var i = 0; i < rows; i++)
    {
        list.Add(new SqlParameter("@Var" + (i + 1), str));
    }

    return GetScalarSp(@"Test_Simple_Parameter", list.ToArray());
}
public long RunTable(int rows)
{
    var param = new SqlParameter();
    param.ParameterName = "@Tbl";
    param.SqlDbType = SqlDbType.Structured;
    var dt = new DataTable();
    var str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + DateTime.Now;

    dt.Columns.Add("ID", typeof (Int32));
    dt.Columns.Add("Name", typeof(string));
    dt.Columns.Add("Description", typeof(string));

    for (var i = 0; i < rows; i++)
    {
        dt.Rows.Add(new object[] {i + 1, (i + 1).ToString(), str });
    }

    param.Value = dt;

    return GetScalarSp(@"Test_Table_Parameter", new[] { param });
}

Then my test looks like that:

[Test]
public void Test_All()
{
    const int iterationsEach = 100;
    const int iterationsAll = 100;

    double xmlAvg = 0;
    double tableAvg = 0;
    double simpleAvg = 0;
    var rows = 20;
    for (var j = 0; j < iterationsAll; j++)
    {
        for (var i = 0; i < iterationsEach; i++)
        {
            tableAvg += this.RunTable(rows);
        }
        for (var i = 0; i < iterationsEach; i++)
        {
            simpleAvg += this.RunSimple(rows);
        }
        for (var i = 0; i < iterationsEach; i++)
        {
            xmlAvg += this.RunXml(rows);
        }
        Console.WriteLine("Cycle " + (j + 1));
    }
    
    xmlAvg = xmlAvg / (iterationsAll * iterationsEach);
    tableAvg = tableAvg / (iterationsAll * iterationsEach);
    simpleAvg = simpleAvg / (iterationsAll * iterationsEach);



    Console.WriteLine("XML:{0} {1}%", xmlAvg , (int)(xmlAvg * 100 / tableAvg));
    Console.WriteLine("Table:{0} {1}%", tableAvg, 100);
    Console.WriteLine("Simple:{0} {1}%", simpleAvg , (int)(simpleAvg * 100 / tableAvg));
}

And finally the results are as follow:

Run 1 AVG Milliseconds % of longest
XML 4.287 86%
Table 4.9772 100%
Simple 3.7201 75%
Run 2    
XML 3.9507 82%
Table 4.7951 100%
Simple 3.4508 72%
Run 3    
XML 3.9515 81%
Table 4.8532 100%
Simple 3.4812 72%

As you can see the most inefficient way is passing a table but the difference between plain varchars and XML is less than 10 % of the table parameter average execution time or between 10% and 20% of the XML parameter average execution time.

A follow up article: http://blog.bodurov.com/Performance-comparison-between-passing-and-querying-XML-vs-Table-parameters-in-a-SqlServer-2008-Stored-Procedure

Share this post:   digg     Stumble Upon     del.icio.us     E-mail

Commenting temporarily disabled