Макс или по умолчанию?

Каков наилучший способ получить значение Max из запроса LINQ, который не может возвращать строки? Если я просто делаю

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

Я получаю сообщение об ошибке, когда запрос не возвращает строк. Я мог бы сделать

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

но это кажется немного тупым для такого простого запроса. У меня отсутствует лучший способ сделать это?

ОБНОВЛЕНИЕ: вот предыстория: я пытаюсь получить следующий счетчик приемлемости из дочерней таблицы (унаследованная система, не заставляйте меня начинать...). Первая строка соответствия для каждого пациента всегда 1, вторая - 2 и т.д. (Очевидно, это не первичный ключ дочерней таблицы). Итак, я выбираю максимальное значение счетчика для пациента, а затем добавляю 1 к нему для создания новой строки. Когда нет существующих дочерних значений, мне нужен запрос для возврата 0 (так что добавление 1 даст мне значение счетчика 1). Обратите внимание, что я не хочу полагаться на сырое количество дочерних строк, если унаследованное приложение вводит пробелы в значениях счетчика (возможно). Мой плохой для того, чтобы сделать вопрос слишком общим.

Ответ 1

Так как DefaultIfEmpty не реализован в LINQ to SQL, я выполнил поиск по ошибке, которую он вернул, и нашел увлекательную статью, которая имеет дело с нулевыми наборами в совокупных функциях. Чтобы обобщить то, что я нашел, вы можете обойти это ограничение, добавив в нулевое значение в своем выборе. Мой VB немного ржавый, но я думаю он бы сделал что-то вроде этого:

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

Или в С#:

var x = (from y in context.MyTable
         where y.MyField == value
         select (int?)y.MyCounter).Max();

Ответ 2

У меня была аналогичная проблема, но я использовал методы расширения LINQ в списке, а не в синтаксисе запроса. Там также работает кастинг с помощью Nullable трюка:

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

Ответ 3

Звучит как случай для DefaultIfEmpty (непроверенный код следует):

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

Ответ 4

Подумайте о том, что вы просите!

Максимум {1, 2, 3, -1, -2, -3}, очевидно, равен 3. Максимум {2}, очевидно, равен 2. Но что такое max пустого множества {}? Очевидно, что это бессмысленный вопрос. Максимум пустого множества просто не определен. Попытка получить ответ - математическая ошибка. Максимум любого множества сам должен быть элементом этого множества. Пустое множество не имеет элементов, поэтому утверждение, что какое-то конкретное число является максимумом этого набора, не будучи в этом множестве, является математическим противоречием.

Точно так же, как это правильно, когда компьютер запускает исключение, когда программист просит его делить на ноль, поэтому правильное поведение компьютера для исключения исключения, когда программист просит его взять максимум пустого задавать. Деление на ноль, взятие максимума пустого набора, зависание шлекелерке и верховая езда на Небесном, все бессмысленные, невозможные, undefined.

Теперь, что вы на самом деле хотите сделать?

Ответ 5

Вы всегда можете добавить Double.MinValue к последовательности. Это гарантирует, что есть хотя бы один элемент, а Max вернет его только в том случае, если он фактически является минимальным. Чтобы определить, какая опция более эффективна (Concat, FirstOrDefault или Take(1)), вы должны выполнить соответствующий бенчмаркинг.

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

Ответ 6

int max = list.Any() ? list.Max(i => i.MyCounter) : 0;

Если в списке есть какие-либо элементы (т.е. не пусто), он примет максимальное значение поля MyCounter, else вернет 0.

Ответ 7

С .Net 3.5 вы можете использовать DefaultIfEmpty(), передавая значение по умолчанию в качестве аргумента. Что-то вроде одного из следующих способов:

int max = (from e in context.Table where e.Year == year select e.RecordNumber).DefaultIfEmpty(0).Max();
DateTime maxDate = (from e in context.Table where e.Year == year select e.StartDate ?? DateTime.MinValue).DefaultIfEmpty(DateTime.MinValue).Max();

Первый разрешен при запросе столбца NOT NULL, а второй - в том, как он использовал его для запроса столбца NULLABLE. Если вы используете DefaultIfEmpty() без аргументов, значение по умолчанию будет определяться типом вашего вывода, как вы можете видеть в таблице Таблица значений по умолчанию.

Результат SELECT не будет таким изящным, но приемлемым.

Надеюсь, что это поможет.

Ответ 8

Я думаю, что проблема заключается в том, что вы хотите, когда запрос не имеет результатов. Если это исключительный случай, я бы обернул запрос в блок try/catch и обработал исключение, которое генерирует стандартный запрос. Если это нормально, чтобы запрос не возвращал никаких результатов, тогда вам нужно выяснить, что вы хотите, чтобы результат был в этом случае. Возможно, ответ @David (или что-то подобное будет работать). То есть, если MAX всегда будет положительным, тогда может быть достаточно, чтобы вставить в список известное "плохое" значение, которое будет выбрано только в том случае, если результатов нет. Как правило, я ожидаю, что запрос будет получать максимум, чтобы иметь некоторые данные для работы, и я бы пошел по пути try/catch, так как в противном случае вы всегда должны проверить правильность полученного вами значения. Я предпочел бы, чтобы не исключительный случай мог использовать полученное значение.

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

Ответ 9

Еще одна возможность - группировать, подобно тому, как вы можете обращаться к ней в сыром SQL:

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

Единственное (повторное тестирование в LINQPad), переключение на вкус VB LINQ дает синтаксические ошибки в предложении группировки. Я уверен, что концептуальный эквивалент достаточно легко найти, я просто не знаю, как отразить его в VB.

Сгенерированный SQL будет состоять из следующих строк:

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

Вложенный SELECT выглядит нехорошо, например, выполнение запроса будет извлекать все строки, а затем выбрать соответствующий из найденного набора... вопрос заключается в том, удаляет ли SQL Server запрос в нечто, сравнимое с применением предложения where к внутренний SELECT. Я сейчас изучаю это...

Я не разбираюсь в интерпретации планов выполнения в SQL Server, но похоже, что когда предложение WHERE находится на внешнем SELECT, количество фактических строк, в результате чего этот шаг представляет собой все строки в таблице, соответствующие строки, когда предложение WHERE находится на внутреннем SELECT. Тем не менее, похоже, что только 1% стоимости смещается на следующий шаг, когда рассматриваются все строки, и в любом случае только одна строка когда-либо возвращается с SQL Server, так что, возможно, это не такая большая разница в великой схеме вещей.

Ответ 10

Поздно, но у меня была такая же озабоченность...

Перефразируя свой код из исходного сообщения, вы хотите, чтобы максимальный набор S определялся

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

Принимая во внимание ваш последний комментарий

Достаточно сказать, что я знаю, что хочу 0 когда нет записей для выбора от, что определенно влияет на конечном решении

Я могу перефразировать вашу проблему как: вы хотите max {0 + S}. И похоже, что предлагаемое решение с concat семантически правильное: -)

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

Ответ 11

Почему не что-то более прямое:

Dim x = context.MyTable.Max(Function(DataItem) DataItem.MyField = Value)

Ответ 12

Просто, чтобы все знали, что использует Linq для Entities, методы выше не будут работать...

Если вы попытаетесь сделать что-то вроде

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

Он выдает исключение:

System.NotSupportedException: выражение LINQ node type 'NewArrayInit' не поддерживается в LINQ to Entities..

Я бы предложил просто сделать

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

И FirstOrDefault вернет 0, если ваш список пуст.

Ответ 13

Одна интересная разница, которую стоит отметить, заключается в том, что, хотя FirstOrDefault и Take (1) генерируют один и тот же SQL (в соответствии с LINQPad, в любом случае), FirstOrDefault возвращает значение - по умолчанию - когда нет соответствующих строк и Take ( 1) не возвращает никаких результатов... по крайней мере, в LINQPad.

Ответ 14

decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;

Ответ 15

У меня была аналогичная проблема, мои модульные тесты прошли с использованием Max(), но не удались при работе с живой базой данных.

Мое решение состояло в том, чтобы отделить запрос от выполняемой логики, а не объединять их в один запрос.
Мне понадобилось решение для работы в модульных тестах с использованием Linq-объектов (в Linq-objects Max() работает с нулями) и Linq-sql при выполнении в живой среде.

(Я издеваюсь над Select() в моих тестах)

var requiredDataQuery = _dataRepo.Select(x => new { x.NullableDate1, .NullableDate2 }); 
var requiredData.ToList();
var maxDate1 = dates.Max(x => x.NullableDate1);
var maxDate2 = dates.Max(x => x.NullableDate2);

Менее эффективен? Вероятно.

Мне все равно, пока мое приложение не упадет в следующий раз? Неа.

Ответ 16

Я выбил метод расширения MaxOrDefault. Там не так много, но его присутствие в Intellisense является полезным напоминанием о том, что Max в пустой последовательности вызовет исключение. Кроме того, метод позволяет указать значение по умолчанию, если требуется.

    public static TResult MaxOrDefault<TSource, TResult>(this 
    IQueryable<TSource> source, Expression<Func<TSource, TResult?>> selector,
    TResult defaultValue = default (TResult)) where TResult : struct
    {
        return source.Max(selector) ?? defaultValue;
    }

Использование в столбце или свойстве типа int с именем Id:

    sequence.DefaultOrMax(s => (int?)s.Id);

Ответ 17

Для Entity Framework и Linq to SQL мы можем достичь этого, указав метод расширения, который изменяет метод Expression, переданный методу IQueryable<T>.Max(...):

static class Extensions
{
    public static TResult MaxOrDefault<T, TResult>(this IQueryable<T> source, 
                                                   Expression<Func<T, TResult>> selector)
        where TResult : struct
    {
        UnaryExpression castedBody = Expression.Convert(selector.Body, typeof(TResult?));
        Expression<Func<T, TResult?>> lambda = Expression.Lambda<Func<T,TResult?>>(castedBody, selector.Parameters);
        return source.Max(lambda) ?? default(TResult);
    }
}

Использование:

int maxId = dbContextInstance.Employees.MaxOrDefault(employee => employee.Id);
// maxId is equal to 0 if there is no records in Employees table

Сгенерированный запрос идентичен, он работает так же, как обычный вызов метода IQueryable<T>.Max(...), но если нет записей, он возвращает значение по умолчанию типа T вместо того, чтобы бросать exeption