最大またはデフォルト?


176

行を返さない可能性がある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が返されます)。レガシーアプリでカウンター値にギャップが生じる可能性があるため(可能な場合)、子行の生のカウントに依存したくないことに注意してください。質問を一般的すぎるようにしようとする私の悪い。

回答:


206

以来DefaultIfEmptySQLにLINQで実装されていない、私はそれが返されるエラーで検索を行なったし、見つかった魅力的な記事の集計関数に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();

1
VBを修正するには、Selectは "Select CType(y.MyCounter、Integer?)"になります。目的のためにNothingを0に変換するために元のチェックを行う必要がありますが、例外なく結果を取得するのが好きです。
gfrizzle 2008

2
DefaultIfEmptyの2つのオーバーロードの1つがLINQ to SQLでサポートされています-パラメーターをとらないオーバーロード。
DamienG 2008

LINQ to SQLで両方の形式のDefaultIfEmptyをテストしたばかりなので、この情報は古くなっている可能性があります
Neil

3
@ニール:答えてください。DefaultIfEmptyは私のために動作しません:私が欲しいMaxDateTimeMax(x => (DateTime?)x.TimeStamp)まだ唯一の方法..
duedl0r '20

1
DefaultIfEmptyは現在SQLにLINQに実装されているが、この答えは、SQL文戻ることを「SELECT MyCounter」にDefaultIfEmpty結果を使用するなど、IMOより良いままですべての値の行が加算され、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についてはよく知りませんが、上記の構文を使用すると、「SQLとして実行するためにノード 'OptionalValue'をフォーマットできませんでした」と表示されます。また、デフォルト値(ゼロ)を指定してみましたが、それも好ましくありません。
gfrizzle 2008

ああ。DefaultIfEmptyはLINQ to SQLではサポートされていないようです。最初に.ToListを使用してリストにキャストすることで回避できますが、これはパフォーマンスに大きな影響を与えます。
Jacob Proffitt

3
ありがとう、これはまさに私が探していたものです。拡張メソッドの使用:var colCount = RowsEnumerable.Select(row => row.Cols.Count).DefaultIfEmpty().Max()
Jani

35

あなたが求めていることを考えてください!

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

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

さて、実際にやりたいことは何ですか?


良い点-私はそれらの詳細で私の質問をすぐに更新します。選択するレコードがない場合は、0が必要であることを知っていれば十分です。これは、最終的なソリューションに確実に影響します。
gfrizzle 2008

17
私は頻繁にユニコーンをネバーランドに飛ばそうとします、そして、私の努力は無意味で未定義であるというあなたの提案に腹を立てます。
Chris Shouts、

2
この議論は正しいとは思いません。それは明らかにlinq-to-sqlであり、sql Maxではゼロ行を超える行はnullとして定義されています。
duedl0r 2012年

4
Linqは、クエリがオブジェクトに対してメモリ内で実行される場合でも、データベースで行に対して実行される場合でも、通常は同じ結果を生成する必要があります。LinqクエリはLinqクエリであり、使用中のアダプターに関係なく忠実に実行する必要があります。
yfeldblum 2012年

1
Linqの結果はメモリで実行してもsqlで実行しても同じであることに理論的に同意しますが、実際に少し深く掘り下げると、常にそうであるとは限らないことがわかります。Linq式は、複雑な式変換を使用してSQLに変換されます。単純な1対1の翻訳ではありません。1つの違いはnullの場合です。C#では「null == null」がtrueです。SQLでは、「null == null」の一致は外部結合に含まれますが、内部結合には含まれません。ただし、内部結合はほとんどの場合必要なものなので、デフォルトです。これにより、動作に違いが生じる可能性があります。
Curtis Yallop 14

25

常に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();

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

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


3
これは2つのクエリを実行しませんか?
andreapier 2013

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();

1つ目はNOT NULL列をクエリするときに許可され、2つ目はそれを使用してNULLABLE列をクエリする方法です。引数なしでDefaultIfEmpty()を使用すると、デフォルト値は、出力のタイプに定義された値になります。デフォルト値の表

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

それが役に立てば幸い。


7

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

私の場合、行が返されないことが頻繁に発生します(従来のシステムでは、患者は以前の資格を持っている場合と持っていない場合があります)。これがより例外的なケースである場合、私はおそらくこのルートに行くでしょう(そして、私はまだ、あまり良く見えないかもしれません)。
gfrizzle 2008

6

もう1つの可能性は、生の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がクエリを最適化するかどうかです。私は今それを調べています...

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


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

注目に値すると思われる興味深い違いの1つは、FirstOrDefaultとTake(1)が同じSQLを生成する一方で(LINQPadに従って)、一致する行がなく、Take(1)が返す場合、FirstOrDefaultが値(デフォルト)を返すことです。結果なし...少なくとも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が返されます。


順序付けは、大きなデータセットで深刻なパフォーマンスの低下を引き起こす可能性があります。最大値を見つけるのは非常に非効率的な方法です。
Peter Bruins、


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()を使用してパスしましたが、ライブデータベースに対して実行すると失敗しました。

私の解決策は、実行中のロジックからクエリを分離することであり、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);

効率が悪い?恐らく。

次回アプリが落ちない限り、気にしてもいいですか?いいえ。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.