web-dev-qa-db-ja.com

最大またはデフォルト?

行を返さない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番目は2などです(明らかに、これは子テーブルの主キーではありません)。したがって、患者の既存の最大カウンター値を選択し、1を追加して新しい行を作成しています。既存の子の値が存在しない場合、0を返すクエリが必要です(したがって、1を追加すると1のカウンター値が得られます)。従来のアプリでカウンター値にギャップが生じた場合(可能性がある場合)、子行の生のカウントに依存したくないことに注意してください。質問をあまりにも一般的なものにしようとするのは悪いことです。

168
gfrizzle

DefaultIfEmptyはLINQ to SQLに実装されていないため、返されたエラーを検索し、集計関数のnullセットを処理する 魅力的な記事 を見つけました。私が見つけたものを要約すると、select内で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();
203
Jacob Proffitt

同様の問題が発生しましたが、クエリ構文ではなくリストでLINQ拡張メソッドを使用していました。 Nullableトリックへのキャストもそこで機能します。

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

DefaultIfEmptyの場合のように聞こえます(テストされていないコードが続きます)。

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

あなたが何を求めているのか考えてください!

{1、2、3、-1、-2、-3}の最大値は明らかに3です。{2}の最大値は明らかに2です。しかし、空のセット{}の最大値は何ですか?明らかにそれは無意味な質問です。空のセットの最大値は単純に定義されていません。答えを得ようとすることは数学的エラーです。任意のセットの最大値は、それ自体がそのセットの要素でなければなりません。空のセットには要素がないため、特定の数がそのセット内に存在せずにそのセットの最大値であると主張することは数学的な矛盾です。

プログラマーがゼロで除算するように要求したときにコンピューターが例外をスローするのは正しい動作であるように、プログラマーが空のセットの最大値を取るように要求したときにコンピューターが例外をスローするのは正しい動作です。ゼロによる除算、空のセットの最大値の取得、spacklerorkeのウィガー、および飛行ユニコーンのネバーランドへの乗車は、すべて意味がなく、不可能で、未定義です。

さて、あなたが実際にしたいことは何ですか?

35
yfeldblum

シーケンスにはいつでもDouble.MinValueを追加できます。これにより、少なくとも1つの要素が存在し、Maxが実際に最小である場合にのみそれを返します。より効率的なオプション(ConcatFirstOrDefault、またはTake(1))を判別するには、適切なベンチマークを実行する必要があります。

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

.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列を照会するときに許可され、2番目の列はそれを使用してNULLABLE列を照会する方法です。引数なしでDefaultIfEmpty()を使用する場合、デフォルト値は デフォルト値テーブル でわかるように、出力のタイプに定義された値になります。

結果のSELECTはそれほどエレガントではありませんが、許容範囲内です。

それが役に立てば幸い。

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

リストに要素がある場合(空でない場合)、MyCounterフィールドの最大値を取得します。それ以外の場合は0を返します。

9
beastieboy

問題は、クエリに結果がない場合に何をしたいのかと思います。これが例外的な場合は、クエリを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
7
tvanfosson

別の可能性は、生の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がクエリを内部SELECTにwhere句を適用することに匹敵するものに最適化するかどうかです。私は今それを調べています...

SQL Serverでの実行プランの解釈に精通していませんが、WHERE句が外側のSELECTにある場合、そのステップをもたらす実際の行の数は、一致する行のみに対してテーブル内のすべての行になりますWHERE句が内部SELECTにある場合。ただし、すべての行を考慮すると、1%のコストのみが次のステップにシフトされ、いずれにせよ1行のみがSQL Serverから返されるように見えるため、物事の大まかなスキームにそれほど大きな違いはないかもしれません。

6
Rex Miller

遅くなりましたが、私は同じ懸念を持っていました...

元の投稿からコードを言い換えると、定義されたセット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();
6
Dom Ribaut

なぜもっと直接的なものではないのですか:

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

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を返します。

2
Nix
decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;
1
jong su.

注目に値する興味深い違いの1つは、FirstOrDefaultとTake(1)が(とにかくLINQPadに従って)同じSQLを生成するのに対し、FirstOrDefaultは一致する行がなく、Take(1)が返されるときに値(デフォルト)を返すことです。結果なし...少なくともLINQPadでは。

1
Rex Miller

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);
0
Stephen Kennedy

同様の問題がありました。Max()を使用してユニットテストに合格しましたが、ライブデータベースに対して実行すると失敗しました。

私の解決策は、実行中のロジックからクエリを分離することであり、1つのクエリに結合することではありませんでした。
Linq-objects(Linq-objects Max()はnullで動作します)および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);

効率が悪い?恐らく。

次回アプリが落ちない限り、気にしますか?いや。

0
Seb

Entity FrameworkおよびLinq to SQLの場合、IQueryable<T>.Max(...)メソッドに渡されるExpressionを変更する拡張メソッドを定義することでこれを実現できます。

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のデフォルト値を返します

0
Ashot Muradian