Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the most efficient way to get only the final row of a SQL table using EF4?

I'm looking to retrieve the last row of a table by the table's ID column. What I am currently using works:

var x = db.MyTable.OrderByDescending(d => d.ID).FirstOrDefault();

Is there any way to get the same result with more efficient speed?

like image 306
Bazinga Avatar asked Sep 02 '12 01:09

Bazinga


1 Answers

I cannot see that this queries through the entire table.

Do you not have an index on the ID column?

Can you add the results of analysing the query to your question, because this is not how it should be.

As well as the analysis results, the SQL produced. I can't see how it would be anything other than select top 1 * from MyTable order by id desc only with explicit column names and some aliasing. Nor if there's an index on id how it's anything other than a scan on that index.

Edit: That promised explanation.

Linq gives us a set of common interfaces, and in the case of C# and VB.NET some keyword support, for a variety of operations upon sources which return 0 or more items (e.g. in-memory collections, database calls, parsing of XML documents, etc.).

This allows us to express similar tasks regardless of the underlying source. Your query for example includes the source, but we could do a more general form of:

public static YourType FinalItem(IQueryable<YourType> source)
{
  return source.OrderByDesending(d => d.ID).FirstOrDefault();
}

Now, we could do:

IEnumerable<YourType> l = SomeCallThatGivesUsAList();
var x = FinalItem(db.MyTable);//same as your code.
var y = FinalItem(l);//item in list with highest id.
var z = FinalItem(db.MyTable.Where(d => d.ID % 10 == 0);//item with highest id that ends in zero.

But the really important part, is that while we've a means of defining the sort of operation we want done, we can have the actual implementation hidden from us.

The call to OrderByDescending produces an object that has information on its source, and the lambda function it will use in ordering.

The call to FirstOrDefault in turn has information on that, and uses it to obtain a result.

In the case with the list, the implementation is to produce the equivalent Enumerable-based code (Queryable and Enumerable mirror each other's public members, as do the interfaces they use such as IOrderedQueryable and IOrderedEnumerable and so on).

This is because, with a list that we don't know is already sorted in the order we care about (or in the opposite order), there isn't any faster way than to examine each element. The best we can hope for is an O(n) operation, and we might get an O(n log n) operation - depending on whether the implementation of the ordering is optimised for the possibility of only one item being taken from it*.

Or to put it another way, the best we could hand-code in code that only worked on enumerables is only slightly more efficient than:

public static YourType FinalItem(IEnumerable<YourType> source)
{
  YourType highest = default(YourType);
  int highestID = int.MinValue;
  foreach(YourType item in source)
  {
    curID = item.ID;
    if(highest == null || curID > highestID)
    {
      highest = item;
      highestID = curID;
    }
  }
  return highest;
}

We can do slightly better with some micro-opts on handling the enumerator directly, but only slightly and the extra complication would just make for less-good example code.

Since we can't do any better than that by hand, and since the linq code doesn't know anything more about the source than we do, that's the best we could possibly hope for it matching. It might do less well (again, depending on whether the special case of our only wanting one item was thought of or not), but it won't beat it.

However, this is not the only approach linq will ever take. It'll take a comparable approach with an in-memory enumerable source, but your source isn't such a thing.

db.MyTable represents a table. To enumerate through it gives us the results of an SQL query more or less equivalent to:

SELECT * FROM MyTable

However, db.MyTable.OrderByDescending(d => d.ID) is not the equivalent of calling that, and then ordering the results in memory. Because queries get processed as a whole when they are executed, we actually get the result of an SQL query more or less like:

SELECT * FROM MyTable ORDER BY id DESC

Finally, the entire query db.MyTable.OrderByDescending(d => d.ID).FirstOrDefault() results in a query like:

SELECT TOP 1 * FROM MyTable ORDER BY id DESC

Or

SELECT * FROM MyTable ORDER BY id DESC LIMIT 1

Depending upon what sort of database server you are using. Then the results get passed to code equivalent to the following ADO.NET-based code:

return dataReader.Read() ?
  new MyType{ID = dataReader.GetInt32(0), dataReader.GetInt32(1), dataReader.GetString(2)}//or similar
  : null;

You can't get much better.

And as for that SQL query. If there's an index on the id column (and since it looks like a primary key, there certainly should be), then that index will be used to very quickly find the row in question, rather than examining each row.

In all, because different linq providers use different means to fulfil the query, they can all try their best to do so in the best way possible. Of course, being in an imperfect world we'll no doubt find that some are better than others. What's more, they can even work to pick the best approach for different conditions. One example of this is that database-related providers can choose different SQL to take advantage of features of different versions of databases. Another is that the implementation of the the version of Count() that works with in memory enumerations works a bit like this;

public static int Count<T>(this IEnumerable<T> source)
{
  var asCollT = source as ICollection<T>;
  if(asCollT != null)
    return asCollT.Count;
  var asColl = source as ICollection;
  if(asColl != null)
    return asColl.Count;
  int tally = 0;
  foreach(T item in source)
    ++tally;
  return tally;
}

This is one of the simpler cases (and a bit simplified again in my example here, I'm showing the idea not the actual code), but it shows the basic principle of code taking advantage of more efficient approaches when they're available - the O(1) length of arrays and the Count property on collections that is sometimes O(1) and it's not like we've made things worse in the cases where it's O(n) - and then when they aren't available falling back to a less efficient but still functional approach.

The result of all of this is that Linq tends to give very good bang for buck, in terms of performance.

Now, a decent coder should be able to match or beat its approach to any given case most of the time†, and even when Linq comes up with the perfect approach there are some overheads to it itself.

However, over the scope of an entire project, using Linq means that we can concisely create reasonably efficient code that relates to a relatively constrained number of well defined entities (generally one per table as far as databases go). In particular, the use of anonymous functions and joins means that we get queries that are very good. Consider:

var result = from a in db.Table1
  join b in db.Table2
  on a.relatedBs = b.id
  select new {a.id, b.name};

Here we're ignoring columns we don't care about here, and the SQL produced will do the same. Consider what we would do if we were creating the objects that a and b relate to with hand-coded DAO classes:

  1. Create a new class to represent this combination of a's id and b's name, and relevant code to run the query we need to produce instances.
  2. Run a query to obtain all information about each a and the related b, and live with the waste.
  3. Run a query to obtain the information on each a and b that we care of, and just set default values for the other fields.

Of these, option 2 will be wasteful, perhaps very wasteful. Option 3 will be a bit wasteful and very error prone (what if we accidentally try to use a field elsewhere in the code that wasn't set correctly?). Only option 1 will be more efficient than what the linq approach will produce, but this is just one case. Over a large project this could mean producing dozens or even hundreds or thousands of slightly different classes (and unlike the compiler, we won't necessarily spot the cases where they're actually the same). In practice, therefore, linq can do us some great favours when it comes to efficiency.

Good policies for efficient linq are:

  1. Stay with the type of query you start with as long as you can. Whenever you grab items into memory with ToList() or ToArray etc, consider if you really need to. Unless you need to or you can clearly state the advantage doing so gives you, don't.
  2. If you do need to move to processing in memory, favour AsEnumerable() over ToList() and the other means, so you only grab one at a time.
  3. Examine long-running queries with SQLProfiler or similar. There are a handful of cases where policy 1 here is wrong and moving to memory with AsEnumerable() is actually better (most relate to uses of GroupBy that don't use aggregates on the non-grouped fields, and hence don't actually have a single SQL query they correspond with).
  4. If a complicated query is hit many times, then CompiledQuery can help (less so with 4.5 since it has automatic optimisations that cover some of the cases it helps in), but it's normally better to leave that out of the first approach and then use it only in hot-spots that are efficiency problems.
  5. You can get EF to run arbitrary SQL, but avoid it unless it's a strong gain because too much such code reduces the consistent readability using a linq approach throughout gives (I have to say though, I think Linq2SQL beats EF on calling stored procedures and even more so on calling UDFs, but even there this still applies - it's less clear from just looking at the code how things relate to each other).

*AFAIK, this particular optimisation isn't applied, but we're talking about the best possible implementation at this point, so it doesn't matter if it is, isn't, or is in some versions only.

†I'll admit though that Linq2SQL would often produce queries that use APPLY that I would not think of, as I was used to thinking of how to write queries in versions of SQLServer before 2005 introduced it, while code doesn't have those sort of human tendencies to go with old habits. It pretty much taught me how to use APPLY.

like image 168
Jon Hanna Avatar answered Nov 02 '22 17:11

Jon Hanna