List<T> Grabbing Data for Speed
Published: 10/17/2009 10:57:09 AM
Speeding up your Data Gathering
In my previous article I began writing about the virtues of the Generic List - List<T>. In this article I will demonstrate the efficiency of using the Generic List. Keep in mind that this is a precursor to the more elegant features of the List<T> but mandatory in understanding its elegance.
Let's take a look at a classic example of populating DataSet and then compare it to populating data into a Generic List. I will warn you, at first it might look like we have to write additional code and we will but only in the beginning. Ok let's create some data, make a stored procedure and then populate out DataSet.
Create a Table and stuff some data into it, then create a stored proc for retrieval
-- Create Our Table
CREATE TABLE [dbo].[MyCustomers](
[ID] [bigint] IDENTITY(1,1) NOT NULL,
[Guid] [uniqueidentifier] NOT NULL CONSTRAINT [DF_MyCustomers_Guid] DEFAULT (newid()),
[FName] [varchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[LName] [varchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
CONSTRAINT [PK_MyCustomers] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
-- Stuff our Table with some data
Declare @Count int
Set @Count = 0
While @Count < 50000
Begin
Insert Into dbo.MyCustomers (FName, LName)
Values (Cast(NewID() as varchar(50)), Cast(NewID() as varchar(50)))
Set @Count = @Count + 1
End
GO
-- Create a Stroed Proc to retrive our data
Create Procedure dbo.GetMyCustomerList
As
Set NoCount On
Select
ID
,Guid
,FName
,LName
From dbo.MyCustomers
Set NoCount Off
GO
-- Quick Test to see if everything went right
Select Top 100 *
From dbo.MyCustomers
Order By ID desc
Now if everything went right you should be seeing the bottom 100 rows of our data starting at row fifty thousand and counting down. I will assume you are ok.
The SqlDataAdapter Approach
In order to get our data onto a web page we need to crate a dataset and then fill it with data from our database and then manipulate it using the Asp.Net server side tools like DataSet, DataTable, DataRow etc... We do this by using the System.SqlClient, and System.Data namespaces. After populating our data we can begin to manipulate it. However for our purposes we are only going to grab the data and count how many rows we have. We will do this again populating a list to see if there is any performance gain.
Populate our Dataset and Count the Rows
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class x_test_data_set1 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Trace.Warn("Fill", "Started");
DataSet ds = Customers();
Trace.Warn("Fill", "Ended");
Response.Write(ds.Tables[0].Rows.Count.ToString());
}
private DataSet Customers()
{
SqlConnection objConn = new SqlConnection(ConfigurationManager.ConnectionStrings["DBLocal"].ConnectionString);
SqlCommand objCmd = new SqlCommand("dbo.GetMyCustomerList", objConn);
objCmd.CommandType = CommandType.StoredProcedure;
DataSet ds = new DataSet();
SqlDataAdapter objAdpt = new SqlDataAdapter();
objAdpt.SelectCommand = objCmd;
objAdpt.Fill(ds);
return ds;
}
}
Turn the trace on for the .aspx page and running the code above produces our timing of execution. Mine looks like the following. 810 milliseconds to perform that database retrieval. (Remember, we are retrieving 50k rows).
Asp Trace from DataAdapter.Fill
| Trace Information |
| Category |
Message |
From First(s) |
From Last(s) |
| aspx.page |
Begin PreInit |
|
|
| aspx.page |
End PreInit |
3.95025218508192E-05 |
0.000040 |
| aspx.page |
Begin Init |
7.93333542182263E-05 |
0.000040 |
| aspx.page |
End Init |
0.000124179067071834 |
0.000045 |
| aspx.page |
Begin InitComplete |
0.00016021051972056 |
0.000036 |
| aspx.page |
End InitComplete |
0.000191851132483318 |
0.000032 |
| aspx.page |
Begin PreLoad |
0.000225150841139062 |
0.000033 |
| aspx.page |
End PreLoad |
0.000259768804235456 |
0.000035 |
| aspx.page |
Begin Load |
0.000291965790545409 |
0.000032 |
| Fill |
Started |
0.000330014723849503 |
0.000038 |
| Fill |
Ended |
0.810523900880298 |
0.810194 |
| aspx.page |
End Load |
0.810590730866828 |
0.000067 |
| aspx.page |
Begin LoadComplete |
0.810614830362457 |
0.000024 |
| aspx.page |
End LoadComplete |
0.810638305817487 |
0.000023 |
| aspx.page |
Begin PreRender |
0.810662269979011 |
0.000024 |
| aspx.page |
End PreRender |
0.810696173678769 |
0.000034 |
| aspx.page |
Begin PreRenderComplete |
0.810719766924595 |
0.000024 |
| aspx.page |
End PreRenderComplete |
0.810743292503368 |
0.000024 |
| aspx.page |
Begin SaveState |
0.811033807211553 |
0.000291 |
| aspx.page |
End SaveState |
0.811170680116538 |
0.000137 |
| aspx.page |
Begin SaveStateComplete |
0.811195408665142 |
0.000025 |
| aspx.page |
End SaveStateComplete |
0.811218021991792 |
0.000023 |
| aspx.page |
Begin Render |
0.811241998684252 |
0.000024 |
| aspx.page |
End Render |
0.811494943140879 |
0.000253 |
Ok, now let's perform the same thing but use a Generic List to hold our data as opposed to a DataSet, In order to do this we first need to create a class that will hold our data. This will give us a strongly typed data wrapper which makes things a whole lot easier on the other side as you will soon see. After we create our class, we use and SqlDataReader to get our data, create an instance of our class and then add it to our Generic List. Let's get started. We create our class inside the App_Code foler.
Create a Data Wrapper Class
using System;
/// <summary>
/// Holds Data Related to a Customer Object
/// </summary>
public class Customer
{
private Int64 _id;
/// <summary>
/// ID (System.Int64)
/// </summary>
public Int64 ID
{
get { return this._id; }
set { this._id = value; }
}
private Guid _guid;
/// <summary>
/// ID (System.Guid)
/// </summary>
public Guid Guid
{
get { return this._guid; }
set { this._guid = value; }
}
private String _fname;
/// <summary>
/// FName (System.String)
/// </summary>
public String FName
{
get { return this._fname; }
set { this._fname = value; }
}
private String _lname;
/// <summary>
/// LName (System.String)
/// </summary>
public String LName
{
get { return this._lname; }
set { this._lname = value; }
}
}
OK We have our base class to hold our data, let's populate the same data into a Generic List and take a look at the performance results.
Generic List Code Behind
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class x_test_data_set2 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Trace.Warn("Reader", "Started");
List<Customer> l = GetCustomers();
Trace.Warn("Reader", "Ended");
Response.Write(l.Count.ToString());
}
private List<Customer> GetCustomers()
{
SqlConnection objConn = new SqlConnection(ConfigurationManager.ConnectionStrings["DBLocal"].ConnectionString);
SqlCommand objCmd = new SqlCommand("dbo.GetMyCustomerList", objConn);
objCmd.CommandType = CommandType.StoredProcedure;
SqlDataReader objRdr;
List<Customer> l = new List<Customer>();
Customer c;
objConn.Open();
objRdr = objCmd.ExecuteReader();
while (objRdr.Read())
{
c = new Customer();
c.ID = Convert.ToInt64(objRdr["ID"]);
c.Guid = (Guid)objRdr["Guid"];
c.FName = objRdr["FName"].ToString();
c.LName = objRdr["LName"].ToString();
l.Add(c);
}
objRdr.Close();
objConn.Close();
return l;
}
}
Now we run our page and look at the results. Still returning the same 50k rows, but look at the time it took to do this! We went from 810 milliseconds down to 330 milliseconds! And that's not all, read on...
Asp Trace from Generic List Approach
| Trace Information |
| Category |
Message |
From First(s) |
From Last(s) |
| aspx.page |
Begin PreInit |
|
|
| aspx.page |
End PreInit |
2.75079101531907E-05 |
0.000028 |
| aspx.page |
Begin Init |
5.23968547351273E-05 |
0.000025 |
| aspx.page |
End Init |
8.00275680586448E-05 |
0.000028 |
| aspx.page |
Begin InitComplete |
0.000102370226496664 |
0.000022 |
| aspx.page |
End InitComplete |
0.00012546724726669 |
0.000023 |
| aspx.page |
Begin PreLoad |
0.000146842517464992 |
0.000021 |
| aspx.page |
End PreLoad |
0.000171558535133611 |
0.000025 |
| aspx.page |
Begin Load |
0.000193981391560415 |
0.000022 |
| Reader |
Started |
0.000218542025625764 |
0.000025 |
| Reader |
Ended |
0.327289906957802 |
0.327071 |
| aspx.page |
End Load |
0.327363586353811 |
0.000074 |
| aspx.page |
Begin LoadComplete |
0.327409675135491 |
0.000046 |
| aspx.page |
End LoadComplete |
0.327446180257511 |
0.000037 |
| aspx.page |
Begin PreRender |
0.327479119075217 |
0.000033 |
| aspx.page |
End PreRender |
0.327517659221202 |
0.000039 |
| aspx.page |
Begin PreRenderComplete |
0.327542335139877 |
0.000025 |
| aspx.page |
End PreRenderComplete |
0.327566920835813 |
0.000025 |
| aspx.page |
Begin SaveState |
0.327857360358385 |
0.000290 |
| aspx.page |
End SaveState |
0.32799586729739 |
0.000139 |
| aspx.page |
Begin SaveStateComplete |
0.328020593339808 |
0.000025 |
| aspx.page |
End SaveStateComplete |
0.328042966072491 |
0.000022 |
| aspx.page |
Begin Render |
0.32806449923248 |
0.000022 |
| aspx.page |
End Render |
0.328313473888663 |
0.000249 |
OK, we have over twice the computing performance and if you haven't realized yet working with a DataSet and trying to find information inside of it, at least for me, is a time consuming task. When working with the Generic List and because we created our class Intellisense does our work for us. Instead of remembering column names and positions and everything else that you often have to look up references for the syntax you now have everything at you finger tips.
Consider the following scenarios. Lets find the value of the field "Guid" at index position 48709 and display that on a web page. If you are using a dataset, you would type something like this:
ds.Tables[0].Rows[48709]["Guid"].ToString()
And even that requires you to remember the Database field name "Guid"! However, with the Generic List we can type the following:
l[48709].Guid.ToString()
The beauty of this is that after you type the closing bracket and put a period, you are now offered a list of properties to choose from. Now that is very convenient indeed!
Anyway, you should now have firm grasp on my conceptual thinking. In the next article we will be looking at finding, manipulating and playing with our server side disconnected record sets.
Happy Coding! 
Aspx:
<%@ Page Language="C#" AutoEventWireup="true" trace="true" CodeFile="data-set.aspx.cs" Inherits="x_test_data_set" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
</div>
</form>
</body>
</html>
C#:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class x_test_data_set : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
// Get Our Data Using a DataSet
Trace.Warn("SqlDataAdapter", "Started");
DataSet ds = Customers();
Trace.Warn("SqlDataAdapter", "Ended");
Response.Write("DataSet Total Rows: " + ds.Tables[0].Rows.Count.ToString());
Response.Write("<br />");
Response.Write("Value at Index Position 48709: " + ds.Tables[0].Rows[48709]["Guid"].ToString());
Response.Write("<br />");
Response.Write("<br />");
// Get Our Data Using a Generic List
Trace.Warn("SqlDataReader", "Started");
List<Customer> l = GetCustomers();
Trace.Warn("SqlDataReader", "Ended");
Response.Write("Generic List Total Rows: " + l.Count.ToString());
Response.Write("<br />");
Response.Write("Value at Index Position 48709: " + l[48709].Guid.ToString());
}
private DataSet Customers()
{
// Create Instance of Connection and Command Object
SqlConnection objConn = new SqlConnection(ConfigurationManager.ConnectionStrings["DBLocal"].ConnectionString);
SqlCommand objCmd = new SqlCommand("dbo.GetMyCustomerList", objConn);
// Mark the Command as a SPROC
objCmd.CommandType = CommandType.StoredProcedure;
// Create a new Dataset
DataSet ds = new DataSet();
// Create a SqlDataAdapter and fill the DataSet
SqlDataAdapter objAdpt = new SqlDataAdapter();
objAdpt.SelectCommand = objCmd;
objAdpt.Fill(ds);
objAdpt.Dispose();
objConn.Close();
return ds;
}
private List<Customer> GetCustomers()
{
// Create Instance of Connection and Command Object
SqlConnection objConn = new SqlConnection(ConfigurationManager.ConnectionStrings["DBLocal"].ConnectionString);
SqlCommand objCmd = new SqlCommand("dbo.GetMyCustomerList", objConn);
// Mark the Command as a SPROC
objCmd.CommandType = CommandType.StoredProcedure;
// Create a SqlDataReader
SqlDataReader objRdr;
// Create a new Generic List of Articles
List<Customer> l = new List<Customer>();
Customer c;
// Fill our Generic List
objConn.Open();
objRdr = objCmd.ExecuteReader();
while (objRdr.Read())
{
c = new Customer();
c.ID = Convert.ToInt64(objRdr["ID"]);
c.Guid = (Guid)objRdr["Guid"];
c.FName = objRdr["FName"].ToString();
c.LName = objRdr["LName"].ToString();
l.Add(c);
}
objRdr.Close();
objConn.Close();
return l;
}
}
Back to top