Performance comparison - reading data from the database strongly typed

I'm a big fan of strongly typed database querying as well as returning strong typed results. Due to the nature of static languages, you'll get compile time checking of all our tables and columns. You can easily rename columns as you can be sure all your (internal) references are accounted for.

Returning strongly typed lists of objects instead of DataReaders/DataTables / any other object based containers will also make it easier to transfer through data layers as you're always certain of what's available for you to read and what's not.

But it comes at a cost. Performance.

I set out to test various different ways we could query our database and generate a strongly typed List with the results in it. I'm using the standard AdventureWorks 2005 database for my testing. For the actual profiling, I'll be using my CodeProfiler class from a previous blogpost.

I'll be using two different entities, Product and CompleteProduct:

[Table(Name = "Production.Product")]
public class Product
{
	[Column]
	public virtual int ProductID { get; set; }
	[Column]
	public virtual string Name { get; set; }
	[Column]
	public virtual string ProductNumber { get; set; }
	[Column]
	public virtual bool MakeFlag { get; set; }
	[Column]
	public virtual short SafetyStockLevel { get; set; }
	[Column]
	public virtual decimal ListPrice { get; set; }
	[Column]
	public virtual DateTime SellStartDate { get; set; }
}

[Table(Name = "Production.Product")]
public class CompleteProduct
{
	[Column]
	public int ProductID { get; set; }
	[Column]
	public string Name { get; set; }
	[Column]
	public string ProductNumber { get; set; }
	[Column]
	public bool MakeFlag { get; set; }
	[Column]
	public bool FinishedGoodsFlag { get; set; }
	[Column]
	public string Color { get; set; }
	[Column]
	public short SafetyStockLevel { get; set; }
	[Column]
	public short ReorderPoint { get; set; }
	[Column]
	public decimal StandardCost { get; set; }
	[Column]
	public decimal ListPrice { get; set; }
	[Column]
	public string Size { get; set; }
	[Column]
	public string SizeUnitMeasureCode { get; set; }
	[Column]
	public string WeightUnitMeasureCode { get; set; }
	[Column]
	public decimal? Weight { get; set; }
	[Column]
	public int DaysToManufacture { get; set; }
	[Column]
	public string ProductLine { get; set; }
	[Column]
	public string Class { get; set; }
	[Column]
	public string Style { get; set; }
	[Column]
	public int? ProductSubcategoryID { get; set; }
	[Column]
	public int? ProductModelID { get; set; }
	[Column]
	public DateTime SellStartDate { get; set; }
	[Column]
	public DateTime? SellEndDate { get; set; }
	[Column]
	public DateTime? DiscontinuedDate { get; set; }
	[Column]
	public Guid rowguid { get; set; }
	[Column]
	public DateTime ModifiedDate { get; set; }
}

They both map to the same table, Production.Product. CompleteProduct covers all columns, Product just covers the ones I'm interested in.

Method #1 - Manually mapping from DataReader
private static void performDataReader()
{
	var result = new List<Product>();

	using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
	{
		using (SqlCommand cmd = conn.CreateCommand())
		{
			cmd.CommandText = COMMAND_TEXT;

			conn.Open();
			using (SqlDataReader sqldr = cmd.ExecuteReader())
			{
				while (sqldr.Read())
				{
					Product p = new Product();
					p.ProductID = Convert.ToInt32(sqldr["ProductID"]);
					p.Name = sqldr["Name"].ToString();
					p.ProductNumber = sqldr["ProductNumber"].ToString();
					p.MakeFlag = Convert.ToBoolean(sqldr["MakeFlag"]);
					p.SafetyStockLevel = Convert.ToInt16(sqldr["SafetyStockLevel"]);
					p.ListPrice = Convert.ToDecimal(sqldr["ListPrice"]);
					p.SellStartDate = Convert.ToDateTime(sqldr["SellStartDate"]);

					result.Add(p);
				}
			}
		}
	}
}

Pros: Fast
Cons: Has to be written for each object model

Method #2 - Manully mapping from DataTable
private static DataTable getDT()
{
	DataTable result = new DataTable();

	using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
	{
		using (SqlCommand cmd = conn.CreateCommand())
		{
			cmd.CommandText = COMMAND_TEXT;

			using (SqlDataAdapter sqlda = new SqlDataAdapter(cmd))
				sqlda.Fill(result);
		}
	}

	return result;
}

private static void performDataTable()
{
	var result = new List<Product>();

	foreach (DataRow dr in getDT().Rows)
	{
		Product p = new Product();
		p.ProductID = Convert.ToInt32(dr["ProductID"]);
		p.Name = dr["Name"].ToString();
		p.ProductNumber = dr["ProductNumber"].ToString();
		p.MakeFlag = Convert.ToBoolean(dr["MakeFlag"]);
		p.SafetyStockLevel = Convert.ToInt16(dr["SafetyStockLevel"]);
		p.ListPrice = Convert.ToDecimal(dr["ListPrice"]);
		p.SellStartDate = Convert.ToDateTime(dr["SellStartDate"]);

		result.Add(p);
	}
}

Pros: Pretty fast, easy row access, disconnected from database
Cons: Has to be written for each object model
Comment: I often use a getDT()'ish way of accessing my tables. It's easier than using DataReaders as they're disconnected, you can traverse them back and forth multiple times.

Method #3 - Automatic mapping using DataContext.Translate
private static void performEntityQuery()
{
	List<Product> result;

	using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
	{
		using (DataContext dc = new DataContext(conn))
		{
			using (SqlCommand cmd = conn.CreateCommand())
			{
				cmd.CommandText = COMMAND_TEXT;

				conn.Open();
				using(SqlDataReader sqldr = cmd.ExecuteReader())
				{
					result = dc.Translate<Product>(sqldr).ToList();
				}
			}
		}
	}
}

Pros: Works for all object models, easy to use
Cons: Slow

Method #4 - Linq to SQL query for complete entity
private static void performLinqQuery()
{
	List<Product> result;

	using(DataContext dc = new DataContext(CONNECTION_STRING))
	{
		result = dc.GetTable<Product>().OrderBy(p => p.ProductID).Take(rowcount).ToList();
	}
}

Pros: Could it be easier? Works for all object models
Cons: Slow

Method #5 - Linq to SQL query for partial entity
private static void performSubsetLinqQuery()
{
	List<Product> result;

	using (DataContext dc = new DataContext(CONNECTION_STRING))
	{
		result = dc.GetTable<CompleteProduct>().OrderBy(p => p.ProductID).Take(rowcount).Select(p => new Product() {
			ListPrice = p.ListPrice,
			ProductID = p.ProductID,
			MakeFlag = p.MakeFlag,
			Name = p.Name,
			ProductNumber = p.ProductNumber,
			SafetyStockLevel = p.SafetyStockLevel,
			SellStartDate = p.SellStartDate }).ToList();
	}
}

Pros: Easy, works for all object models
Cons: Slow
Comments: In the previous method I retrieved complete Product entities. This time I'm retrieving only some columns of the CompleteProduct entity, mapping them over into a new Product. I've included this to see if there's any performance difference in comparison to loading complete entities.

Method #6 - Automatically mapping from DataReader
private static List<T> mapList<T>(SqlDataReader dr)
{
	List<T> list = new List<T>();

	PropertyInfo[] properties = typeof(T).GetProperties();
	T t = Activator.CreateInstance<T>();

	while(dr.Read())
	{
		foreach (PropertyInfo pi in properties)
			pi.SetValue(t, dr[pi.Name], null);

		list.Add(t);
	}

	return list;
}

private static void performAutomappedDataReader()
{
	List<Product> result;

	using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
	{
		using (SqlCommand cmd = conn.CreateCommand())
		{
			cmd.CommandText = COMMAND_TEXT;

			conn.Open();
			using (SqlDataReader sqldr = cmd.ExecuteReader())
			{
				result = mapList<Product>(sqldr);
			}
		}
	}
}

Pros: Simple to use, works for all object models
Cons: Slow, reflection based

Method #7 - Enhanced automatic mapping from DataReader
private static void performEnhancedAutomappedDataReader()
{
	List<Product> result;

	using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
	{
		using (SqlCommand cmd = conn.CreateCommand())
		{
			cmd.CommandText = COMMAND_TEXT;

			conn.Open();
			using (SqlDataReader sqldr = cmd.ExecuteReader())
			{
				result = EntityMapper.MapToEntities<Product>(sqldr).ToList();
			}
		}
	}
}

Pros: ???
Cons: ???
Comments: This is an enhanced version of the previous method that I've made. Explaining the inner workings is outside the scope of this particular topic so I'll have to explain it in my next post. For now, just imagine something very cool and sexy. UPDATE: you can read about the method here.

Method #8 - SubSonic SqlQuery
private static void performSubSonicQuery()
{
	List<Product> result =
		new Select
			(
				Subsonic.Product.ProductIDColumn.QualifiedName,
				Subsonic.Product.NameColumn.QualifiedName,
				Subsonic.Product.ProductNumberColumn.QualifiedName,
				Subsonic.Product.MakeFlagColumn.QualifiedName,
				Subsonic.Product.SafetyStockLevelColumn.QualifiedName,
				Subsonic.Product.ListPriceColumn.QualifiedName,
				Subsonic.Product.SellStartDateColumn.QualifiedName
			)
			.Top(rowcount.ToString())
			.From(Subsonic.Product.Schema)
			.OrderAsc(Subsonic.Product.ProductIDColumn.QualifiedName)
			.ExecuteTypedList<Product>();
}

Pros: Works for all object models
Cons: Slow
Comments: I've never used SubSonic before, so I may have overlooked some obvious performance enhancements, thus, take my numbers with a grain of salt.

Method #9 - NHibernate CreateCriteria
private static Configuration nhCfg;
private static ISessionFactory nhFactory;

private static void performNHibernateQuery()
{
	List<Product> result;

	using (ISession session = nhFactory.OpenSession())
	{
		result = (List<Product>)session.CreateCriteria(typeof(Product)).AddOrder(Order.Asc("ProductID")).SetMaxResults(rowcount).List<Product>();
	}
}

Pros: Easy, works for all object models, concise querying
Cons: Slow
Comments: I've never used NHibernate before, so I may have overlooked some obvious performance enhancements, thus, take my numbers with a grain of salt.

Method #10 - Compiled LinqQuery
private static Func<DataContext, int, IEnumerable<Product>> compiledQuery;
private static void performCompiledLinqQuery()
{
	List<Product> result;

	using (DataContext dc = new DataContext(CONNECTION_STRING))
	{
		result = compiledQuery(dc, rowcount).ToList();
	}
}

Pros: Among the fastest, no matter the rowcount, works for all object models
Cons: Requires a bit more setting up and helper functionality to store the compiled queries

The profiling
I profiled each of the test cases by returning 1, 10, 100, 1.000, 10.000 and 100.000 rows. The numbers are the total execution time of 50 iterations of each method. I'm on a quad core machine so I set the max parallelization degree to three, thus saving myself a core for running SQL Server and Windows.

static void Main(string[] args)
{
	// Make sure we don't get disturbed by other processes
	Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;

	// We'll let NHibernate cache it's Configuration and SessionFactory as we'd probably do this in most live applications
	nhCfg = new Configuration();
	nhCfg.AddAssembly(Assembly.GetExecutingAssembly());
	nhFactory = nhCfg.BuildSessionFactory();

	foreach(int rc in new int[] { 1, 10, 100, 1000, 10000, 100000 })//, 100, 1000, 10000, 100000 })
	{
		Console.WriteLine("Rowcount: " + rc);
		rowcount = rc;

		// Set the rowcount
		COMMAND_TEXT = ORIGINAL_COMMAND_TEXT.Replace("{rowcount}", rowcount.ToString());

		// Make sure the enhanced automapped datareader does not use cached resources from previous run
		EntityMapper.ClearCachedMapperMethods();

		// Compile the query for the compiled linq query test
		compiledQuery = CompiledQuery.Compile<DataContext, int, IEnumerable<Product>>((DataContext dc, int takeCount) => dc.GetTable<Product>().OrderBy(p => p.ProductID).Take(takeCount));

		Console.WriteLine("performDataReader: " + CodeProfiler.ProfileAction(performDataReader, iterations, 3));
		Console.WriteLine("performDataTable: " + CodeProfiler.ProfileAction(performDataTable, iterations, 3));
		Console.WriteLine("performEntityQuery: " + CodeProfiler.ProfileAction(performEntityQuery, iterations, 3));
		Console.WriteLine("performLinqQuery: " + CodeProfiler.ProfileAction(performLinqQuery, iterations, 3));
		Console.WriteLine("performCompiledLinqQuery: " + CodeProfiler.ProfileAction(performCompiledLinqQuery, iterations, 3));
		Console.WriteLine("performSubsetLinqQuery: " + CodeProfiler.ProfileAction(performSubsetLinqQuery, iterations, 3));
		Console.WriteLine("performAutomappedDataReader: " + CodeProfiler.ProfileAction(performAutomappedDataReader, iterations, 3));
		Console.WriteLine("performEnhancedAutomappedDataReader: " + CodeProfiler.ProfileAction(performEnhancedAutomappedDataReader, iterations, 3));
		Console.WriteLine("performSubSonicQuery: " + CodeProfiler.ProfileAction(performSubSonicQuery, iterations, 3));
		Console.WriteLine("performNHibernateQuery: " + CodeProfiler.ProfileAction(performNHibernateQuery, iterations, 3));
	}

	Console.Write("Done");
	Console.Read();
}

The results
The following two graphs shows the total runtime of 50 iterations for each different method, as well as the total runtime divided by number of rows. Runtime in seconds along the Y-axis, number of rows along the X-axis. It basically shows that all LINQ to SQL flavors suffer from initial setup, but they scale extremely well, ending up outperforming all other contestants by a fair margin. One could argue that it's rarely relevant in cases of more than a thousand rows as we'll rarely pull out that much data at once. Never the less, I find it interesting - it's something I'll have to look under the hood to find an explanation of. Ignoring some variance due to the low row number, DataReader, DataTable and my Enhanced Automapped DataReader functions outperform most contestants.

UPDATE:
It seems the reason the LINQ flavors are so speedy is because their object mapper methods are being cached in the local thread cache. And since the CodeProfiler will execute all iterations on the same thread, each iteration (except the first) will simply reuse the mapper methods. See System.Data.Linq.SqlClient.ObjectReaderCompiler.Compile() for reference.

UPDATE 2:
As Kris Vandermotten duly commented, I should have tested a compiled LINQ query as well as that really is a separate scenario. I've added a tenth method as well as a new line in the graphs. The compiled LINQ query, although requiring a bit more plumbing code, really blows the competition away. It's fast for even single row queries since we no longer suffer from the compilation time. For larger rowsets the advantage diminishes as the saved compilation time is spread out over a large amount of rows.

UPDATE 3:
As a friend of mine, Jakob Andersen points out, I really should be a bit more specific in regards to the purpose of these comparisons.

I am in no way trying to make the claim that it's unreasonable that an OR/M framework hurts performance. Neither am I claiming that my comparisons between LINQ to SQL, Subsonic and nHibernate are fair. Really, to make a fair comparison, one should compare the very specific features of the frameworks, not a feature set as general as what I'm doing in these tests.

The thing is, some of these frameworks (nHibernate, I'm looking at you especially) just offer so much more functionality that it's a burden they have to live with. While this extra functionality might cost a bit when we're just trying to test the actual object translation performance, it might gain you a whole lot more in real life scenarios. Take for instance, if you utilize nHibernates caching, you might save the time that goes towards the database lookups - saving you much more than the actual translation process in most cases.

So, as mentioned in a couple of tests - take these results with a grain of salt. They're rather out-of-the-box simple tests of direct querying & result translation performance. We're ignoring all of the extra OR/M features that we have available, and we're ignoring that these different frameworks offer very different functionality.

Total runtime
Image: totalgraph1
Runtime per row
Image: totalgraph2
Remember, even though there seems to be somewhat of a speed difference, the fastest method takes only 0,000074 seconds, while the slowest takes 0,00085 seconds. No matter what, you should'nt consider refactoring your solutions in regards to performance unless you really have a high hit count, or unless it's for valid architectural reasons. Otherwise you might just risk having Ted Dziuba shove his foot up your ass.

Downloads
PerformanceComparison_ReadingStronglyTyped.zip - Project code
Reading_Data_From_Database_Profiling.xlsx - Profiling times in Excel format

kick it on DotNetKicks.com


Comments

Kris Vandermotten | May 2nd, 2008, 6:37 PM

You might want to take a look at http://blogs.msdn.com/ricom/archive/2007/06/22/dlinq-linq-to-sql-performance-part-1.aspx

Mark S. Rasmussen | May 3rd, 2008, 2:12 AM

Yeah, that's a brilliant series of articles, once again proving the grounds that LINQ to SQL can perform on par of most other alternatives if used properly.

Kris Vandermotten | May 6th, 2008, 8:26 PM

Yeah, but the point is: you should store the results of linq query compilation for reuse, and your method 4 and 5 don't do that!

See also http://blogs.msdn.com/ricom/archive/2008/01/11/performance-quiz-13-linq-to-sql-compiled-queries-cost.aspx and http://blogs.msdn.com/ricom/archive/2008/01/14/performance-quiz-13-linq-to-sql-compiled-query-cost-solution.aspx.

Mark S. Rasmussen | May 7th, 2008, 11:45 PM

You are absolutely right, I should've tested that, I'm embarrased that I did not think of the CompiledQuery functionality!

Anyways, I've added a tenth method as well as updated the conclusion & charts.

Thanks for helping me out doing this properly ;)

Jakob Andersen | May 9th, 2008, 1:09 PM

Good work comparing those things. But i have a few reservations:

1. You write as pro for a few of the methods "Works for all object models". But in most cases you have to supply the relationsship between your object model and your relational model. And just because this is in XML and perhabs can be generated by a tool its still some important mapping logic thats stored here. And if mapping is wrongly typed or generated by your tool the performance impacts could be much larger than the small number you are working with here because the data store is actually queryed the wrong way.

2. You write: "you'll get compile time checking of all our tables and columns", that not true. You still not get checking of columnnames and tables in for instance SQL strings and mapping documents at compiletime. Given the right tools you only have to do a column rename one place instead of all queries. But still, you are not bulletproof because the compiler doesn't know the database structure at compiletime.

Last of all, most of the tools you are comparing here doesn't hide the fact that they have an overhead. But it is often a tradeoff when you choose these RAD-type tools that provides an higher abstraction. An important note on that point is that my humble opinion is that even though you move to a higher level of abstraction you still need to know whats going on behind the curtains.

Mark S. Rasmussen | May 10th, 2008, 2:16 AM

Jakob, I've appended a third update to my conclusion that ought ot clear up my intentions with these tests :)

End User Computing | Aug 11th, 2010, 2:49 PM

Really appreciate this post. It’s hard to sort the good from the bad sometimes, but I think you’ve nailed it!

IT support los angeles | Aug 11th, 2010, 2:49 PM

I really appreciate the kind of topics you post here. Thanks for sharing us a great information that is actually helpful. Good day!

Business Process Outsourcing | Aug 11th, 2010, 2:50 PM

I just read through the entire article of yours and it was quite good. This is a great article thanks for sharing this informative information. I will visit your blog regularly for some latest post.

Email Marketing Software | Aug 11th, 2010, 2:51 PM

Really i am impressed from this post....the person who create this post it was a great human..thanks for shared this with us.

Los Angeles Web Designers | Aug 11th, 2010, 2:52 PM

I am just new to your blog and just spent about 1 hour and 30 minutes lurking and reading. I think I will frequent your blog from now on after going through some of your posts. I will definitely learn a lot from them.

New York Web Design | Aug 11th, 2010, 2:52 PM

Hi, I appreciate your speculating as it arouses people’s concern today and lets this topic be discussable. We all know about it here..

Drupal Website Customization India | Aug 11th, 2010, 2:54 PM


Its always good to learn tips like you share for blog posting. As I just started posting comments for blog and facing problem of lots of rejections. I think your suggestion would be helpful for me. I will let you know if its work for me too.
Thanks and keep post such a informative blogs.

Ciplex | Aug 12th, 2010, 8:36 AM

I really love the way information presented in your post. I have added to you in my social bookmark…and i am w8ing ur next post..

Ciplex | Aug 12th, 2010, 8:37 AM

I've surfed the net more than three hours today, however, I haven't found such useful information. Thanks a lot, it is really useful to me.

Free Websites | Aug 12th, 2010, 8:41 AM

Thanks for taking the time to discuss this, I feel strongly about it and love learning more on this topic. If possible, as you gain expertise, would you mind updating your blog with more information? It is extremely helpful for me.

marketing company virginia | Aug 12th, 2010, 8:44 AM

I like your blog post, keep on writing this great material! I'll make sure to follow up on your blog in the future.

Internet WiFi de alta velocidad | Aug 12th, 2010, 8:45 AM

I would like to share it with all my friends and hope they will like it too.

Cabling Contractors MD | Aug 12th, 2010, 8:46 AM

Usually I do not post on blogs, but I would like to say that this blog really forced me to do so! Thanks, really nice post.

Subprime Affiliate Network | Aug 12th, 2010, 8:49 AM

You got a really useful blog I have been here reading for about an hour. I am a newbie and your success is very much an inspiration for me.

Point of sale | Aug 12th, 2010, 9:39 AM

I completely agree with the above comment, the internet is with a doubt growing into the most important medium of communication across the globe and its due to sites like this that ideas are spreading so quickly.

Material Testing | Aug 12th, 2010, 11:10 AM

Pretty Interesting post. Could not be written any better. Thanks for sharing!

Neil | Aug 18th, 2010, 2:02 PM

<a href="http://www.allied.com/san-jose-ca-moving-companies.aspx">San Jose moving companies</a>

San Jose moving companies | Aug 18th, 2010, 2:05 PM

San Jose moving companies

Binary option | Aug 23rd, 2010, 12:23 PM

You have a very good site, well constructed and very interesting i have bookmarked you, hopefully you keep posting new stuff, many thanks

SEO Company India | Aug 30th, 2010, 2:14 PM

Excellent read, I just passed this onto a colleague who was doing a little research on that. And he actually bought me lunch because I found it for him smile So let me rephrase that.

Ciplex | Aug 30th, 2010, 4:18 PM

I have not any word to appreciate this post.....Really i am impressed from this post....the person who create this post it was a great human..thanks for shared this with us.

Ciplex | Aug 30th, 2010, 4:19 PM

More and more information about home purchasing are mentioned in this blog. Please read them and keep in mind. --- Thanks

Online Blackjack | Aug 31st, 2010, 9:45 AM

So far, I managed to go though only some of the posts you have here, but I find them very interesting and informative. Just want say thank you for the information you have shared.

soniya | Aug 31st, 2010, 9:46 AM

i am a link builder

alameda dentist | Aug 31st, 2010, 10:16 AM

All the information on how to get your dream house is there for you on this blog. Take a look and and you will find all that you need to know.

ekta | Aug 31st, 2010, 12:39 PM

<a href="http://www.pyromarketing.co.uk/web-design-manchester/">Web Design Manchester</a>

Bluechopsticks engraved gifts | Sep 1st, 2010, 12:06 PM

DB is now the essential for projects and websites , i see your informations is more useful and i want to thank you .

Ciplex | Sep 1st, 2010, 1:26 PM

I wanted to thank you for this great read!! I definitely enjoying every little bit of it I have you bookmarked to check out new stuff you post..

Ciplex | Sep 1st, 2010, 1:26 PM

Hi… that was great stuff.. I really like reading on this subject Could you tell me more on that… I love to explore

Ciplex | Sep 1st, 2010, 1:27 PM

I wanted to thank you for this great read!! I definitely enjoying every little bit of it Smile I have you bookmarked to check out new stuff you post..

Add comment

After you have posted a comment, an email will be sent to the provided email address. Before your comment is activated, you will have to click the confirmation link within the email.

Name:

Email (only used for validation):

Website (optional):

Message:

Notify me when new comments are added:

Please type the following letters into the box below:  

Post!