OR/M and Performance
I spent most of the day going over an NHibernate application that was slow. The cause was most probably large number of queries for the database. I took my own advice and enabled performance logging for the application and let it run for a few days. When I read the logs today it looks like there were a number of pages where the number of queries was outside of any reasonable proportion.
When I started reading the code for this pages, I realized that the problem wasn't with mis-use of NHibernate, or rather, it wasn't specific for NHibernate. The most offending page had something like:
foreach(string idStr in claimsIds)
{
int id = int.Parse(idStr);
Claim claim = Context.Session.Load<Claim>(id);
claim.Status = ddlClaimsStatus.Value;
Context.Session.Save(claim);
}
Context.Session.Flush();
foreach(string idStr in paymentsIds)
{
int id = int.Parse(idStr);
Payment payment = Context.Session.Load<Payment>(id);
payment.LastUpdateDate = DateTime.Today;
Context.Session.Save(payment);
}
Context.Session.Flush();
I think that you can see why it is making a large number of calls to the database. This is one of the few cases that I would recommend the use of a stored procedure, since it meets my two criterias for a SP:
- Zero business logic
- Works on a large set of records
The second worst page had something like this:
foreach(DataTable dt in ds.Tables)
{
foreach(DataRow subItem in dt.Rows)
{
Claim claim = Context.Session.Load<Claim>.Load(subItem["id"]);
// do something with the claim
}
}
Hm, now why would this produce large number of queries to the database, I wonder...
Only on the fifth page we managed to find a true SELECT N+1 issue, which was something like;
//User and Groups, this actually cause two database calls, one to load the user (which would probably be loaded
// the second to load its users
ICollection<Group> groups = Context.User.Groups;
foreach(Group group in groups)
{
foreach(Section section in group.Sections) // database query
{
//do something with the sections
}
}
In this case, the fix was to explictly load all the data in advance, like this:
// only one database query here, get all the groups for the user as well as their sections
ICollection<Group> groups = Context.Session.CreateCriteria(typeof(Group))
.Add(Expression.Eq("User", Context.User))
.SetFetchMode("Sections", FetchMode.Eager)
.List<Group>();
foreach(Group group in groups)
{
foreach(Section section in group.Sections) // database query
{
//do something with the sections
}
}
That was a true two minutes fix, unlike the other four pages, which require at least a partial re-write.
Comments
Comment preview