views:

11440

answers:

11

What is the best way to get the Max value from a LINQ query that may return no rows? If I just do

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).Max

I get an error when the query returns no rows. I could do

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter _
         Order By MyCounter Descending).FirstOrDefault

but that feels a little obtuse for such a simple request. Am I missing a better way to do it?

UPDATE: Here's the back story: I'm trying to retrieve the next eligibility counter from a child table (legacy system, don't get me started...). The first eligibility row for each patient is always 1, the second is 2, etc. (obviously this is not the primary key of the child table). So, I'm selecting the max existing counter value for a patient, and then adding 1 to it to create a new row. When there are no existing child values, I need the query to return 0 (so adding 1 will give me a counter value of 1). Note that I don't want to rely on the raw count of child rows, in case the legacy app introduces gaps in the counter values (possible). My bad for trying to make the question too generic.

+4  A: 

You could always add Double.MinValue to the sequence. This would ensure that there is at least one element and Max would return it only if it is actually the minimum. To determine which option is more efficient (Concat, FirstOrDefault or Take(1)), you should perform adequate benchmarking.

double x = context.MyTable
    .Where(y => y.MyField == value)
    .Select(y => y.MyCounter)
    .Concat(new double[]{Double.MinValue})
    .Max();
David Schmitt
+1  A: 

One interesting difference that seems worth noting is that while FirstOrDefault and Take(1) generate the same SQL (according to LINQPad, anyway), FirstOrDefault returns a value--the default--when there are no matching rows and Take(1) returns no results... at least in LINQPad.

I Have the Hat
+2  A: 

Sounds like a case for DefaultIfEmpty (untested code follows):

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).DefaultIfEmpty.Max
Jacob Proffitt
I'm not familiar with DefaultIfEmpty, but I get "Could not format node 'OptionalValue' for execution as SQL" when using the syntax above. I also tried providing a default value (zero), but it didn't like that either.
gfrizzle
Ah. Looks like DefaultIfEmpty isn't supported in LINQ to SQL. You could get around that by casting to a list first with .ToList but that's a significant performance hit.
Jacob Proffitt
+1 this worked great for my problem, which was linq to xml-based. thanks!
Brian MacKay
+1  A: 

Another possibility would be grouping, similar to how you might approach it in raw SQL:

from y in context.MyTable
group y.MyCounter by y.MyField into GrpByMyField
where GrpByMyField.Key == value
select GrpByMyField.Max()

The only thing is (testing again in LINQPad) switching to the VB LINQ flavor gives syntax errors on the grouping clause. I'm sure the conceptual equivalent is easy enough to find, I just don't know how to reflect it in VB.

The generated SQL would be something along the lines of:

SELECT [t1].[MaxValue]
FROM (
    SELECT MAX([t0].[MyCounter) AS [MaxValue], [t0].[MyField]
    FROM [MyTable] AS [t0]
    GROUP BY [t0].[MyField]
    ) AS [t1]
WHERE [t1].[MyField] = @p0

The nested SELECT looks icky, like the query execution would retrieve all rows then select the matching one from the retrieved set... the question is whether or not SQL Server optimizes the query into something comparable to applying the where clause to the inner SELECT. I'm looking into that now...

I'm not well-versed in interpreting execution plans in SQL Server, but it looks like when the WHERE clause is on the outer SELECT, the number of actual rows resulting in that step is all rows in the table, versus only the matching rows when the WHERE clause is on the inner SELECT. That said, it looks like only 1% cost is shifted to the following step when all rows are considered, and either way only one row ever comes back from the SQL Server so maybe it's not that big of a difference in the grand scheme of things.

I Have the Hat
+3  A: 

I think the issue is what do you want to happen when the query has no results. If this is an exceptional case then I would wrap the query in a try/catch block and handle the exception that the standard query generates. If it's ok to have the query return no results, then you need to figure out what you want the result to be in that case. It may be that @David's answer (or something similar will work). That is, if the MAX will always be positive, then it may be enough to insert a known "bad" value into the list that will only be selected if there are no results. Generally, I would expect a query that is retrieving a maximum to have some data to work on and I would go the try/catch route as otherwise you are always forced to check if the value you obtained is correct or not. I'd rather that the non-exceptional case was just able to use the obtained value.

Try
   Dim x = (From y In context.MyTable _
            Where y.MyField = value _
            Select y.MyCounter).Max
   ... continue working with x ...
Catch ex As SqlException
       ... do error processing ...
End Try
tvanfosson
In my case, returning no rows happens more frequently than not (legacy system, the patient may or may not have previous eligibility, blah blah blah). If this were a more exceptional case, I'd probably go this route though (and I may still, not seeing much better).
gfrizzle
I'd have marked this as an answer as well if I could.
gfrizzle
+23  A: 

Since DefaultIfEmpty isn't implemented in LINQ to SQL, I did a search on the error it returned and found a fascinating article that deals with null sets in aggregate functions. To summarize what I found, you can get around this limitation by casting to a nullable within your select. My VB is a little rusty, but I think it'd go something like this:

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select CType(y.MyCounter, Integer?)).Max

Or in C#

var x = (from y in context.MyTable
         where y.MyField == value
         select (int?)y.MyCounter).Max();
Jacob Proffitt
To correct the VB, the Select would be "Select CType(y.MyCounter, Integer?)". I have to do an original check to convert Nothing to 0 for my purposes, but I like getting the results without an exception.
gfrizzle
One of the two overloads of DefaultIfEmpty is supported in LINQ to SQL - the one that doesn't take parameters.
DamienG
Cool. Sorry it took so long to correct the VB according to the comment.
Jacob Proffitt
+2  A: 

Think about what you're asking!

The max of {1, 2, 3, -1, -2, -3} is obviously 3. The max of {2} is obviously 2. But what is the max of the empty set { }? Obviously that is a meaningless question. The max of the empty set is simply not defined. Attempting to get an answer is a mathematical error. The max of any set must itself be an element in that set. The empty set has no elements, so claiming that some particular number is the max of that set without being in that set is a mathematical contradiction.

Just as it is correct behavior for the computer to throw an exception when the programmer asks it to divide by zero, so it is correct behavior for the computer to throw an exception when the programmer asks it to take the max of the empty set. Division by zero, taking the max of the empty set, wiggering the spacklerorke, and riding the flying unicorn to Neverland are all meaningless, impossible, undefined.

Now, what is it that you actually want to do?

Justice
Good point - I'll update my question shortly with those details. Suffice to say that I know I want 0 when there are no records to select from, which definitely has an impact on the eventual solution.
gfrizzle
+3  A: 

Hi litt late, but I had the same concern...

Rephrasing your code from the original post, you want the max of the set S defined by

(From y In context.MyTable _
 Where y.MyField = value _
 Select y.MyCounter)

Taking in account your last comment

Suffice to say that I know I want 0 when there are no records to select from, which definitely has an impact on the eventual solution

I can rephrase your problem as: You want the max of {0 + S}. And it looks like the proposed solution with concat is semantically the right one :-)

var max = new[]{0}
          .Concat((From y In context.MyTable _
                   Where y.MyField = value _
                   Select y.MyCounter))
          .Max();
Dom Ribaut
A: 

This is actually a good question, and it has a valid answer as long as you're querying nullable types. The answer in this case is clearly "null". Unfortunately, there is no Max overload for DateTime, so I had to resort to a hack.

I also noted, that Max does not document the fact that it throws InvalidOperationException when the source contains no elements.

naasking
+1  A: 

Just to let everyone out there know that is using Linq to Entities the methods above will not work...

If you try to do something like

var max = new[]{0}
      .Concat((From y In context.MyTable _
               Where y.MyField = value _
               Select y.MyCounter))
      .Max();

It will throw an exception:

System.NotSupportedException: The LINQ expression node type 'NewArrayInit' is not supported in LINQ to Entities..

I would suggest just doing

(From y In context.MyTable _
                   Where y.MyField = value _
                   Select y.MyCounter))
          .OrderByDescending(x=>x).FirstOrDefault());

And the FirstOrDefault will return 0 if your list is empty.

Nix
+2  A: 

I just had a similar problem, but I was using LINQ extension methods on a list rather than query syntax. The casting to a Nullable trick works there as well:

int max = list.Max(i => (int?)i.MyCounter) ?? 0;
Eddie Deyo