Макс або за замовчуванням?


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

Відповіді:


206

Оскільки DefaultIfEmptyвін не реалізований в LINQ в SQL, я здійснив пошук по поверненій помилці і знайшов захоплюючу статтю, яка розглядає нульові набори в сукупності функцій. Підводячи підсумок того, що я знайшов, ви можете обійти це обмеження, відкинувши його до нуля в межах обраного вами. Мій 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?)". Я повинен зробити оригінальну перевірку, щоб перетворити Ніщо на 0 для своїх цілей, але мені подобається отримувати результати без винятку.
gfrizzle

2
Одна з двох перевантажень DefaultIfEmpty підтримується в LINQ до SQL - та, яка не приймає параметри.
ДамієнГ

Можливо, ця інформація застаріла, тому що я лише успішно протестував обидві форми DefaultIfEmpty в LINQ до SQL
Ніл

3
@Neil: будь ласка, дайте відповідь. DefaultIfEmpty не працює для мене: я хочу Maxотримати DateTime. Max(x => (DateTime?)x.TimeStamp)все-таки єдиний спосіб ..
duedl0r

1
Хоча DefaultIfEmpty тепер реалізований у LINQ до SQL, ця відповідь залишається кращою IMO, оскільки використання DefaultIfEmpty призводить до оператора SQL "SELECT MyCounter", який повертає рядок для кожного підсумовуваного значення , тоді як ця відповідь призводить до MAX (MyCounter), який повертає a одинарний, підсумований ряд. (Випробувано в 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 до SQL. Ви можете його обійти, перейшовши до списку спочатку за допомогою .ToList, але це важливий показник ефективності.
Джейкоб Проффітт

3
Дякую, саме це я шукав. Використання методів розширення:var colCount = RowsEnumerable.Select(row => row.Cols.Count).DefaultIfEmpty().Max()
Яні

35

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

Максимум {1, 2, 3, -1, -2, -3} очевидно 3. Макс {2} очевидно 2. Але що таке макс порожнього набору {}? Очевидно, що це безглузде питання. Максимум порожнього набору просто не визначається. Спроба отримати відповідь - це математична помилка. Макс будь-якого набору сам повинен бути елементом у цьому наборі. Порожній набір не містить елементів, тому твердження, що якесь конкретне число є максимумом цього набору, не будучи в ньому, є математичним протиріччям.

Так само, як правильна поведінка для комп’ютера кидати виняток, коли програміст просить його розділити на нуль, так це правильна поведінка для комп'ютера, щоб кинути виняток, коли програміст просить його взяти максимум порожнього набору. Ділення на нуль, взяття максимуму порожнього набору, розмітання шпаклюрке і їзда літаючого єдинорога до Neverland - все безглуздо, неможливо, не визначено.

Тепер, що ви насправді хочете зробити?


Хороший момент - я скоро оновлю своє питання з цими деталями. Досить сказати, що я знаю, що хочу 0, коли немає записів для вибору, що безумовно впливає на можливе рішення.
gfrizzle

17
Я часто намагаюся полетіти своїм єдинорогом до Neverland, і я ображаюся на вашу думку про те, що мої зусилля є безглуздими та невизначеними.
Кріс Криків

2
Я не думаю, що ця аргументація є правильною. Це чіткий зв'язок до sql, а в sql Макс за нуль рядків визначається як нульовий, ні?
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.


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

Я думаю, що питання полягає в тому, що ви хочете, коли запит не має результатів. Якщо це винятковий випадок, тоді я б загорнув запит у блок "спробу / ловити" та обробляти виняток, який генерує стандартний запит. Якщо це нормально, щоб запит не повертав результатів, тоді вам потрібно розібратися, яким ви хочете, щоб результат був у такому випадку. Можливо, відповідь @ David (або щось подібне спрацює). Тобто, якщо MAX завжди буде позитивним, то, можливо, буде достатньо вставити відоме "погане" значення у список, який буде обраний лише у тому випадку, якщо результатів не буде. Як правило, я б очікував на запит, який отримує максимум, щоб мати деякі дані для роботи, і я б пішов на спробу / ловити маршрут, оскільки в іншому випадку ви завжди змушені перевірити, чи отримане вами значення є правильним чи ні. Я '

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

У моєму випадку повернення жодних рядків відбувається частіше, ніж ні (застаріла система, пацієнт може мати або не мати попередньої придатності, бла-бла-бла). Якби це був більш винятковий випадок, я б, мабуть, пішов цим маршрутом (і, можливо, все ще не бачу набагато кращого).
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}. І схоже, що запропоноване рішення з конфатом семантично є правильним :-)

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 не допоможе ...

Якщо ви спробуєте зробити щось подібне

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

Це викине виняток:

System.NotSupportedException: Вузол виразу LINQ типу "NewArrayInit" не підтримується в LINQ для сутностей.

Я б запропонував просто зробити

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

І FirstOrDefaultповерне 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 до 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-object 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);

Менш ефективний? Ймовірно.

Мені все одно, доки мій додаток не перейде наступного разу? Ні.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.