At the end of yesterdays internal study group meeting, the organizer mentioned that I would be giving a presentation on NHibernate the following week. I plan to give a simple dog and pony show of persisting a simple object to the database and all of the setup needed to accomplish the task.

One of the participants asked for a brief description of NHibernate. I explained that it is an ORM framework (Object Relational Mapping). I expanded a bit by saying that NHibernate removes the dependency between your domain objects and your relational database by handling mapping from one to the other.

After a couple more questions it was determined that NHibernate generates SQL and is therefore inferior to stored procedures. It was the general feeling that stored procedures were the be all end all for performance and optimization. I attempted to explain that NHibernate generates parameterized queries which have all the advantages of cached query plans that stored procedures do. By this time the meeting was breaking up and I had a mission.

I wanted to challenge the dogmatic urban legend passed down from .NET developer to .NET developer since the classic ASP days. That dogma simply states you should always access your database via stored procedure for "performance reasons". Inline SQL is BAAAAAAD.

So I fired up Visual Studio, created a class and started writing tests.

The first thing I wanted to do was to create a baseline. I created a User table in a fresh SQLExpress database and wrote a test to pop 1000 rows into it in the crustiest "I just read my first Wrox book" way I could think of. I added some timing code and let it rip. The test looks like this:

 

       [Test]
        public void Time_Inline_Inserts_To_User_Table()
        {
            DateTime start = DateTime.Now;

            using (SqlConnection c = new SqlConnection(connectionString))
            {
                c.Open();
                for (int i = 0; i < INSERT_COUNT; i++)
                {
                    using (SqlCommand co = new SqlCommand())
                    {
                        co.Connection = c;
                        co.CommandText =
                            "INSERT INTO dbo.Users "
                            + "(Handle, FirstName, LastName, Password, EmailAddress, LastLogon) "
                            + "VALUES ("
                            + "'UserHandle" + i + "',"
                            + "'UserFirstName" + i + "',"
                            + "'UserLastName" + i + "',"
                            + "'UserPassword" + i + "',"
                            + "'User" + i + "@email.com',"
                            + DateTime.Now.ToShortDateString() + ")";

                        co.ExecuteNonQuery();
                    }
                }
                c.Close();
            }
            DateTime end = DateTime.Now;
            TimeSpan time = end - start;

            Console.WriteLine(
                string.Format("{0} milliseconds to run {1} inserts by inline query", 
                time.TotalMilliseconds, INSERT_COUNT));
        }

From a fresh load of my test assembly into MbUnit, this code takes 1312.5 milliseconds to run 1000 inserts. Running the test repeatedly drops the time to 437.5 miliseconds. It appears that SQL Server does some optimizations for inline inserts regardless of what the tribe tells me.

Next up, I wanted to see if parameterizing the query would improve the performance. The test case looks like this:

        [Test]
        public void Time_Parameterized_Query_Inserts_To_User_Table()
        {
            DateTime start = DateTime.Now;

            using (SqlConnection c = new SqlConnection(connectionString))
            {
                c.Open();
                for (int i = 0; i < INSERT_COUNT; i++)
                {
                    using (SqlCommand co = new SqlCommand())
                    {
                        co.Connection = c;
                        co.CommandText =
                            "INSERT INTO dbo.Users (Handle, FirstName, LastName, Password, EmailAddress, LastLogon) VALUES (@p1,@p2,@p3,@p4,@p5,@p6)";
                        co.CommandType = System.Data.CommandType.Text;
                        co.Parameters.AddWithValue("@p1", "UserHandle" + i);
                        co.Parameters.AddWithValue("@p2", "UserFirstName" + i);
                        co.Parameters.AddWithValue("@p3", "UserLastName" + i);
                        co.Parameters.AddWithValue("@p4", "UserPassword" + i);
                        co.Parameters.AddWithValue("@p5", "User" + i + "@email.com");
                        SqlParameter param = new SqlParameter("@p6", SqlDbType.DateTime);
                        param.Value = new DateTime(2000, 1, 1);
                        co.Parameters.Add(param);

                        co.ExecuteNonQuery();

                    }
                }
                c.Close();
            }

            DateTime end = DateTime.Now;
            TimeSpan time = end - start;

            Console.WriteLine(
                string.Format("{0} milliseconds to run {1} inserts by parameterized query", 
                    time.TotalMilliseconds, INSERT_COUNT));

        }

This test runs in 1296.9 milliseconds on a fresh load of my test assembly. Repeated runs drops the test run to ~350 milliseconds a slight advantage over the inline query. I imagine this gap would widen with a more complex query.

Finally, we have the acclaimed SQL Stored Procedure dogmatic method of how everything should be done! The stored procedure in the code below is pretty much exactly what you expect it to be.

       [Test]
        public void Time_Stored_Proceedure_Inserts_To_User_Table()
        {
            DateTime start = DateTime.Now;

            using (SqlConnection c = new SqlConnection(connectionString))
            {
                c.Open();
                for (int i = 0; i < INSERT_COUNT; i++)
                {
                    using (SqlCommand co = new SqlCommand())
                    {
                        co.Connection = c;
                        co.CommandText = "AddUser";
                        co.CommandType = CommandType.StoredProcedure;
                        co.Parameters.AddWithValue("@p1", "UserHandle" + i);
                        co.Parameters.AddWithValue("@p2", "UserFirstName" + i);
                        co.Parameters.AddWithValue("@p3", "UserLastName" + i);
                        co.Parameters.AddWithValue("@p4", "UserPassword" + i);
                        co.Parameters.AddWithValue("@p5", "User" + i + "@email.com");
                        SqlParameter param = new SqlParameter("@p6", SqlDbType.DateTime);
                        param.Value = new DateTime(2000, 1, 1);
                        co.Parameters.Add(param);

                        co.ExecuteNonQuery();

                    }
                }
                c.Close();
            }

            DateTime end = DateTime.Now;
            TimeSpan time = end - start;

            Console.WriteLine(
                string.Format("{0} milliseconds to run {1} inserts by stored proceedure", 
                    time.TotalMilliseconds, INSERT_COUNT));

        }

This test runs unexpectedly in 1390.6ms. This seems like quite a long time for a stored procedure that is stored in the database and from my understanding with a query plan in place. Subsequent executions of the test yield execution times ~320ms. Just to verify this, I closed the MbUnit test runner application and reran these tests. I got the same results.

Now that we have a baseline of the various direct SQL methods that are well accepted in the .NET community, I want to see how NHibernate performs at these tasks. I see two methods available though NHibernate spinning up a session and cramming User objects into its Save() method and adding a transaction into the mix.

My first NHibernate test looks like this:

       [Test]
        public void Time_NHibernate_Inserts_To_User_Table()
        {
            DateTime start = DateTime.Now;
            using (ISession session = factory.OpenSession())
            {
                for (int i = 0; i < INSERT_COUNT; i++)
                {
                    User u = new User();
                    u.Handle = "UserHandle" + i;
                    u.FirstName = "UserFirstName" + i;
                    u.LastName = "UserLastName" + i;
                    u.Password = "UserPassword" + i;
                    u.EmailAddress = "User" + i + "@email.com";
                    u.LastLogon = DateTime.Now;

                    session.Save(u);

                }
                session.Close();

            }

            DateTime end = DateTime.Now;
            TimeSpan time = end - start;

            Console.WriteLine(
                string.Format("{0} milliseconds to run {1} inserts by nhibernate", 
                    time.TotalMilliseconds, INSERT_COUNT));

        }

This test runs in 1656.3ms with reruns clocking in at ~560ms. It looks like NHibernate might have a slight performance hit when compared to stored procedures.

My last test using NHibernate transactions looks like this:

       [Test]
        public void Time_NHibernate_Tansaction_Inserts_To_User_Table()
        {
            DateTime start = DateTime.Now;
            using (ISession session = factory.OpenSession())
            {

                ITransaction transaction = session.BeginTransaction();
                for (int i = 0; i < INSERT_COUNT; i++)
                {
                    User u = new User();
                    u.Handle = "UserHandle" + i;
                    u.FirstName = "UserFirstName" + i;
                    u.LastName = "UserLastName" + i;
                    u.Password = "UserPassword" + i;
                    u.EmailAddress = "User" + i + "@email.com";
                    u.LastLogon = DateTime.Now;

                    session.Save(u);

                }
                transaction.Commit();
                session.Close();

            }

            DateTime end = DateTime.Now;
            TimeSpan time = end - start;

            Console.WriteLine(
                string.Format("{0} milliseconds to run {1} inserts by nhibernate with transaction", 
                    time.TotalMilliseconds, INSERT_COUNT));

        }

This test runs in 1406.3ms with follow up runs in 300ms. A dramatic increase in performance over over the non transactional processing. So the final performance test results looks something like this:

Test 1st Run nth Run
Inline SQL 1312.5ms ~430ms
Parameterized Query 1296.9ms ~350ms
Stored Procedure 1390.6ms ~320ms
NHibernate 1565.3ms ~560ms
NHibernate w/Transaction 1406.3ms ~300ms

I am not going to try to interpret these results, but simply publish them and see what feedback I get from the collective. My next steps are to increase the complexity of what I am doing; add some joins into the mix. I want to see if the performance gap widens or a clear winner emerges.

If you would like to see the entire code base for these tests, I have published them to my CodePlex repository and would love to hear your ideas on squeezing out performance with both SQL and NHibernate. Look for the NHibTester solution.


 
Tuesday, July 15, 2008 11:03:10 PM (Pacific Standard Time, UTC-08:00)
Hi,

You example might lend itself to attack by old-school fans because it covers only a trivial case. What the tribe is probably referring to regarding performance are not the trivial insert or retrive cases, but the performance hit when dealing with complex operations. It's not uncommon to implement business logic on the database as stored procedures, because that way a programmer can just send the parameters for whatever they wish executed and get a response back, instead of hitting the database multiple times in order to process everything on, say, C#.

Having said that, I'm a Hibernate and NHibernate user myself, and like them much better than the old way of doing things. Even if there was a significant performance hit (and "significant" would need to be evaluated in a case-by-case basis), what it really needs to be balanced against is the advantages in readability and maintainability.

Those will win for me almost every time. Processors are cheap.
Tuesday, July 15, 2008 11:20:26 PM (Pacific Standard Time, UTC-08:00)
Wouldn't it be more fair to wrap all tests in one transaction as in the last NHibernate one?
Zbigniew
Wednesday, July 16, 2008 12:13:13 AM (Pacific Standard Time, UTC-08:00)
Yes, all of the tests should be in a transaction, for fair comparison.

The explanation as to the poor first run is most likely caching: the DBMS will suck the blocks you're inserting into into its cache on the first run. You didn't say if you were deleting the data between runs; I assume so.

An explanation as to why the first approach (no bind parameters) is slower: the DBMS must re-parse the SQL with every insert, because it's a new statement every time (that also ruins the statement cache, assuming SQL Server has one. I'm more familiar with Oracle, which does). With bind parameters, the statement is only parsed once, at the first use, and an execution plan (trivial for inserts, but more significant for other types of SQL statement) is cached too.

As Ricardo said: the primary reason that SQL Server developers prefer stored procedures over app SQL is that the stored procedure can encapsulate operations of arbitrary complexity, and these operations will be performed on the DBMS, with no or minimal network traffic. There's a price to pay, of course: Transact-SQL is an awful language to be writing application code in, and you end up with application code distributed between the .Net layer and the DBMS. And because Transact-SQL is so poo, there is typically enormous duplication of poorly-designed code.

You are right to challenge the "always use stored procs" dogma - and it is dogma - but you're gonna have to do a lot better than a simple insert statement to convince the zealots to give up stored procs.
Alistair Bayley
Wednesday, July 16, 2008 6:47:40 AM (Pacific Standard Time, UTC-08:00)
Thanks for the feedback folks. I plan to do a part 2 for this post where I take some of the more complicated schema in my current application and make some tests that are not so contrived.

I want to address the full range of CRUD operations performance.
Wednesday, July 16, 2008 9:31:34 AM (Pacific Standard Time, UTC-08:00)
Just thought I'd let you know that the reason that your first run of the SP was rubbish is that SQL Server doesn't compile the SP until the first run - so your first run also includes a compilation in addition to the execution time.

I kind of sit on the fence with the issue. Really it is down to the skill set of the team developing the application.
Duncan Handley
Wednesday, July 16, 2008 12:47:55 PM (Pacific Standard Time, UTC-08:00)
be sure to experiment with the adonet.batch_size configuration setting for NHibernate as well if you plan on testing inserts/updates :)
Wednesday, July 16, 2008 1:06:17 PM (Pacific Standard Time, UTC-08:00)
The NHibernate example is not valid because you are not clearing the session every time so you're adding an unfair tax to NHibernate (due to NHibernate's 1st level caching feature) that is not real world (you generally don't bulk load things using NHibernate -- at least that way you don't).

You should clear the NHibernate session (session.Clear()) every iteration or at least every 5th iteration or so.

Wednesday, July 16, 2008 2:06:03 PM (Pacific Standard Time, UTC-08:00)
NHibernate is slower then native sprocs/sql, that's a fact. Stored Procedures are faster then sql, another fact. No need to test that, the real question you should ask yourself is: "Do I really need to care about those milliseconds extra it takes when using NHibernate, while I gain a lot of functionality and features for it?". In most cases the answer is: No you don't need to care...

Some other people already mentioned the native sql vs sprocs issue, so I won't mention that further ;) But I don't really can take any conclusions from your article, because you will need much more information and data to intepretet the differences in milliseconds properly (compiling of queries, cached query paths, caching in general etc..).

1 tip: Try using the Stopwatch class. It's specifically designed for these kind of tests.
JV
Wednesday, July 16, 2008 2:06:35 PM (Pacific Standard Time, UTC-08:00)
Interesting Chad, I tried session.Clear() after eash session.Save() and it had no effect at all. What am I missing in what you are suggesting?
Wednesday, July 16, 2008 2:15:37 PM (Pacific Standard Time, UTC-08:00)
JV, I don't think the point of this post was to demonstrate that nHibernate is faster than the stored proc way, but more to prove in most situations it performs just as well. I think the dogmatic response that stored procs are always the way to go suffers from premature optimization. You are losing a tremendous amount of flexibility by tightly coupling to your database and risk letting business logic sink into your database because its easier...
Thursday, July 17, 2008 3:31:39 AM (Pacific Standard Time, UTC-08:00)
Surely the speed of any query always constrained by the underlying schema?
If you execute
SELECT /some non-primary key columns/ FROM /a table/ WHERE [non-primary key constraint]
against n rows (where n is very large), you're going to get better performance from optimising the schema for the query?

The benefit of using an ORM (such as NHibernate) is that it produces predictable queries which you can create indexes, statistics and what-not for, whereas, with SPs, you have to be disciplined to make sure you always write your queries so that they use indexes where available.

Either route, profiling your queries (leave SQL Profiler running whilst the test team are breaking your app!) is a better route to performance than whether or not you hand-write the queries yourself or not.
David Kemp
Thursday, July 17, 2008 7:10:06 AM (Pacific Standard Time, UTC-08:00)
Good stuff, can't wait for more.
Comments are closed.