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

176

Каков наилучший способ получить значение 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). Обратите внимание, что я не хочу полагаться на необработанный счетчик дочерних строк в случае, если унаследованное приложение вводит пробелы в значениях счетчика (возможно). Моя плохая попытка сделать вопрос слишком общим.

gfrizzle
источник

Ответы:

206

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

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

Или в C #:

var x = (from y in context.MyTable
         where y.MyField == value
         select (int?)y.MyCounter).Max();
Джейкоб Проффитт
источник
1
Чтобы исправить VB, Select должен быть «Select CType (y.MyCounter, Integer?)». Я должен сделать оригинальную проверку, чтобы преобразовать Nothing в 0 для моих целей, но мне нравится получать результаты без исключения.
gfrizzle
2
В LINQ to SQL поддерживается одна из двух перегрузок DefaultIfEmpty, которая не принимает параметры.
DamienG
Возможно, эта информация устарела, так как я только что успешно протестировал обе формы DefaultIfEmpty в LINQ to SQL
Нил
3
@Neil: пожалуйста, ответь. DefaultIfEmpty не работает для меня: я хочу Maxa DateTime. Max(x => (DateTime?)x.TimeStamp)до сих пор единственный путь ..
duedl0r
1
Хотя DefaultIfEmpty теперь реализован в LINQ to SQL, этот ответ остается лучшим IMO, поскольку использование DefaultIfEmpty приводит к SQL-выражению «SELECT MyCounter», которое возвращает строку для каждого суммируемого значения , тогда как этот ответ приводит к MAX (MyCounter), который возвращает одиночный, суммированный ряд. (Проверено в EntityFrameworkCore 2.1.3.)
Карл Шарман
107

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

int max = list.Max(i => (int?)i.MyCounter) ?? 0;
Эдди Дейо
источник
48

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

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).DefaultIfEmpty.Max
Джейкоб Проффитт
источник
Я не знаком с DefaultIfEmpty, но я получаю «Не удалось отформатировать узел 'OptionalValue' для выполнения в качестве SQL" при использовании приведенного выше синтаксиса. Я также попытался указать значение по умолчанию (ноль), но мне это тоже не понравилось.
gfrizzle
Ах. Похоже, DefaultIfEmpty не поддерживается в LINQ to SQL. Вы можете обойти это, приведя сначала к списку с помощью .ToList, но это значительный удар по производительности.
Джейкоб Проффитт
3
Спасибо, это именно то, что я искал. Использование методов расширения:var colCount = RowsEnumerable.Select(row => row.Cols.Count).DefaultIfEmpty().Max()
Jani
35

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

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

Точно так же, как правильное поведение компьютера - генерировать исключение, когда программист просит его делить на ноль, так и корректное поведение компьютера - генерировать исключение, когда программист просит его принять максимум пустого набора. Деление на ноль, взятие максимума пустого сета, вигер spacklerorke и езда летающего единорога в Neverland - все это бессмысленно, невозможно, неопределенно.

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

yfeldblum
источник
Хороший вопрос - я скоро обновлю свой вопрос с этими деталями. Достаточно сказать, что я знаю, что хочу 0, когда нет записей для выбора, что определенно влияет на возможное решение.
gfrizzle
17
Я часто пытаюсь доставить своего единорога в Неверлэнд, и я обижаюсь на ваше предположение, что мои усилия бессмысленны и неопределенны.
Крис Кричит
2
Я не думаю, что эта аргументация верна. Это ясно, linq-to-sql, а в sql Max больше нуля строк определяется как ноль, нет?
duedl0r
4
Обычно Linq должен выдавать идентичные результаты независимо от того, выполняется ли запрос в памяти объектов или выполняется ли запрос в базе данных по строкам. Запросы Linq - это запросы Linq, и они должны выполняться точно, независимо от того, какой адаптер используется.
yfeldblum
1
Хотя я согласен в теории, что результаты Linq должны быть идентичны, независимо от того, выполняются ли они в памяти или в sql, когда вы действительно копаете немного глубже, вы обнаруживаете, почему это не всегда может быть так. Выражения Linq переводятся в SQL с использованием сложного перевода выражений. Это не простой перевод один на один. Одним из различий является случай ноль. В C # "null == null" является истиной. В SQL соответствия «null == null» включены для внешних объединений, но не для внутренних объединений. Тем не менее, внутренние объединения почти всегда являются тем, что вы хотите, поэтому они используются по умолчанию. Это вызывает возможные различия в поведении.
Кертис Яллоп
25

Вы всегда можете добавить 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();
Дэвид Шмитт
источник
10
int max = list.Any() ? list.Max(i => i.MyCounter) : 0;

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

beastieboy
источник
3
Разве это не запустить 2 запроса?
andreapier
10

Начиная с .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 не будет таким элегантным, но приемлемым.

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

Фернандо Брустолин
источник
7

Я думаю, что проблема в том, что вы хотите, чтобы произошло, когда запрос не дал результатов. Если это исключительный случай, я бы обернул запрос в блок try / catch и обработал исключение, которое генерирует стандартный запрос. Если нормально, чтобы запрос не возвращал результатов, вам нужно выяснить, каким должен быть результат в этом случае. Это может быть ответ @ Дэвида (или что-то подобное будет работать). То есть, если 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
tvanfosson
источник
В моем случае возврат ни одного ряда происходит чаще, чем нет (устаревшая система, пациент может иметь или не иметь предыдущее право, бла-бла-бла). Если бы это был более исключительный случай, я бы, вероятно, пошел по этому пути (и все же, возможно, я не вижу намного лучше).
gfrizzle
6

Другая возможность - группировка, похожая на то, как вы можете подходить к ней в сыром 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 возвращается только одна строка, так что, возможно, это не так уж много различий в общей схеме вещей ,

Рекс Миллер
источник
6

немного поздно, но у меня была такая же проблема ...

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

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

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

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

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

var max = new[]{0}
          .Concat((From y In context.MyTable _
                   Where y.MyField = value _
                   Select y.MyCounter))
          .Max();
Дом Рибо
источник
3

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

Dim x = context.MyTable.Max(Function(DataItem) DataItem.MyField = Value)
правовой
источник
1

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

Рекс Миллер
источник
1

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

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

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

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

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

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

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

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

шухер
источник
Упорядочение может привести к серьезному снижению производительности при работе с большими наборами данных. Это очень неэффективный способ найти максимальное значение.
Питер Брюинз
1
decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;
Чон Су
источник
1

Я выбил 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;
    }
Стивен Кеннеди
источник
0

Для 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 вместо того, чтобы выдавать исключение

Ашот Мурадян
источник
-1

У меня просто была похожая проблема, мои модульные тесты прошли с использованием 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);

Менее эффективны? Наверное.

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

Себ
источник