Maks atau Default?

176

Apa cara terbaik untuk mendapatkan nilai Max dari kueri LINQ yang mungkin tidak menghasilkan baris? Jika saya lakukan

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

Saya mendapatkan kesalahan saat kueri tidak mengembalikan baris. Saya bisa melakukannya

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

tapi itu terasa agak tumpul untuk permintaan sesederhana itu. Apakah saya kehilangan cara yang lebih baik untuk melakukannya?

UPDATE: Inilah kisah belakangnya: Saya mencoba mengambil konter kelayakan berikutnya dari tabel anak (sistem warisan, jangan mulai saya ...). Baris kelayakan pertama untuk setiap pasien selalu 1, yang kedua adalah 2, dll. (Jelas ini bukan kunci utama tabel anak). Jadi, saya memilih nilai penghitung maks yang ada untuk seorang pasien, dan kemudian menambahkan 1 padanya untuk membuat baris baru. Ketika tidak ada nilai anak yang ada, saya perlu permintaan untuk mengembalikan 0 (jadi menambahkan 1 akan memberi saya nilai penghitung 1). Perhatikan bahwa saya tidak ingin bergantung pada jumlah mentah baris anak, jika aplikasi warisan memperkenalkan celah dalam nilai penghitung (mungkin). Buruk saya karena mencoba membuat pertanyaan terlalu umum.

gfrizzle
sumber

Jawaban:

206

Karena DefaultIfEmptytidak diimplementasikan dalam LINQ ke SQL, saya melakukan pencarian pada kesalahan itu kembali dan menemukan artikel menarik yang berhubungan dengan set nol dalam fungsi agregat. Untuk meringkas apa yang saya temukan, Anda dapat mengatasi batasan ini dengan melemparkan ke nullable dalam pilih Anda. VB saya agak berkarat, tapi saya kira akan seperti ini:

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

Atau dalam C #:

var x = (from y in context.MyTable
         where y.MyField == value
         select (int?)y.MyCounter).Max();
Jacob Proffitt
sumber
1
Untuk memperbaiki VB, Select akan menjadi "Select CType (y.MyCounter, Integer?)". Saya harus melakukan pemeriksaan asli untuk mengonversi Nothing ke 0 untuk keperluan saya, tetapi saya suka mendapatkan hasilnya tanpa kecuali.
gfrizzle
2
Salah satu dari dua kelebihan DefaultIfEmpty didukung dalam LINQ ke SQL - salah satu yang tidak mengambil parameter.
DamienG
Mungkin informasi ini sudah ketinggalan zaman, karena saya baru saja berhasil menguji kedua bentuk DefaultIfEmpty di LINQ ke SQL
Neil
3
@Neil: tolong jawab. DefaultIfEmpty tidak berfungsi untuk saya: Saya ingin Maxa DateTime. Max(x => (DateTime?)x.TimeStamp)masih satu-satunya cara ..
duedl0r
1
Meskipun DefaultIfEmpty sekarang diimplementasikan dalam LINQ ke SQL, jawaban ini tetap IMO yang lebih baik, karena menggunakan DefaultIfEmpty menghasilkan pernyataan SQL 'SELECT MyCounter' yang mengembalikan baris untuk setiap nilai yang dijumlahkan , sedangkan jawaban ini menghasilkan MAX (MyCounter) yang mengembalikan sebuah tunggal, jumlah baris. (Diuji dalam EntityFrameworkCore 2.1.3.)
Carl Sharman
107

Saya baru saja mengalami masalah yang sama, tetapi saya menggunakan metode ekstensi LINQ pada daftar daripada sintaks kueri. Casting untuk trik Nullable juga berfungsi di sana:

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

Kedengarannya seperti kasing untuk DefaultIfEmpty(kode yang belum diuji mengikuti):

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).DefaultIfEmpty.Max
Jacob Proffitt
sumber
Saya tidak terbiasa dengan DefaultIfEmpty, tetapi saya mendapatkan "Tidak dapat memformat simpul 'Nilai Opsional' untuk dieksekusi sebagai SQL" saat menggunakan sintaks di atas. Saya juga mencoba memberikan nilai default (nol), tetapi juga tidak suka.
gfrizzle
Ah. Sepertinya DefaultIfEmpty tidak didukung di LINQ ke SQL. Anda dapat menyiasatinya dengan melakukan casting ke daftar terlebih dahulu dengan .ToList tetapi itu adalah pencapaian kinerja yang signifikan.
Jacob Proffitt
3
Terima kasih, ini persis apa yang saya cari. Menggunakan metode ekstensi:var colCount = RowsEnumerable.Select(row => row.Cols.Count).DefaultIfEmpty().Max()
Jani
35

Pikirkan tentang apa yang Anda tanyakan!

Maks {{1, 2, 3, -1, -2, -3} jelas 3. Maks {{}} jelas 2. Tapi apa maks dari himpunan kosong {}? Jelas itu adalah pertanyaan yang tidak berarti. Maks dari set kosong sama sekali tidak didefinisikan. Mencoba mendapatkan jawaban adalah kesalahan matematis. Maks dari setiap set itu sendiri harus menjadi elemen dalam set itu. Set kosong tidak memiliki elemen, sehingga mengklaim bahwa beberapa angka tertentu adalah maks set itu tanpa berada di set itu adalah kontradiksi matematika.

Sama seperti perilaku yang benar bagi komputer untuk melempar pengecualian ketika programmer memintanya untuk membaginya dengan nol, demikian juga perilaku yang benar bagi komputer untuk melempar pengecualian ketika programmer meminta untuk mengambil maks set kosong. Pembagian dengan nol, mengambil maks set yang kosong, menggoyangkan spacklerorke, dan mengendarai unicorn terbang ke Neverland semuanya tidak ada artinya, tidak mungkin, tidak terdefinisi.

Sekarang, apa yang sebenarnya ingin Anda lakukan?

yfeldblum
sumber
Poin bagus - Saya akan segera memperbarui pertanyaan saya dengan perincian itu. Cukuplah untuk mengatakan bahwa saya tahu saya ingin 0 ketika tidak ada catatan untuk dipilih, yang pasti berdampak pada solusi akhirnya.
gfrizzle
17
Saya sering mencoba menerbangkan unicorn saya ke Neverland, dan saya tersinggung saran Anda bahwa upaya saya tidak berarti dan tidak terdefinisi.
Chris Shouts
2
Menurut saya argumentasi ini tidak benar. Ini jelas linq-to-sql, dan dalam sql Max lebih dari nol baris didefinisikan sebagai nol, bukan?
duedl0r
4
Linq umumnya harus menghasilkan hasil yang identik apakah kueri dieksekusi dalam memori pada objek atau apakah kueri dieksekusi di database pada baris. Kueri Linq adalah kueri Linq, dan harus dijalankan dengan setia terlepas dari adaptor mana yang digunakan.
yfeldblum
1
Sementara saya setuju secara teori bahwa hasil Linq harus identik apakah dijalankan dalam memori atau dalam sql, ketika Anda benar-benar menggali sedikit lebih dalam, Anda menemukan mengapa ini tidak selalu bisa begitu. Ekspresi Linq diterjemahkan ke dalam sql menggunakan terjemahan ekspresi kompleks. Ini bukan terjemahan satu-ke-satu yang sederhana. Satu perbedaan adalah kasus nol. Dalam C # "null == null" benar. Dalam SQL, "null == null" cocok disertakan untuk gabungan luar tetapi tidak untuk gabungan dalam. Namun, inner joins hampir selalu seperti yang Anda inginkan sehingga merupakan default. Ini menyebabkan kemungkinan perbedaan perilaku.
Curtis Yallop
25

Anda selalu bisa menambahkan Double.MinValueke urutan. Ini akan memastikan bahwa setidaknya ada satu elemen dan Maxakan mengembalikannya hanya jika itu adalah minimum. Untuk menentukan opsi mana yang lebih efisien ( Concat, FirstOrDefaultatau Take(1)), Anda harus melakukan pembandingan yang memadai.

double x = context.MyTable
    .Where(y => y.MyField == value)
    .Select(y => y.MyCounter)
    .Concat(new double[]{Double.MinValue})
    .Max();
David Schmitt
sumber
10
int max = list.Any() ? list.Max(i => i.MyCounter) : 0;

Jika daftar memiliki elemen apa pun (mis. Tidak kosong), itu akan mengambil maks bidang MyCounter, yang lain akan mengembalikan 0.

beastieboy
sumber
3
Tidakkah ini menjalankan 2 kueri?
andreapier
10

Karena .Net 3.5 Anda dapat menggunakan DefaultIfEmpty () yang meneruskan nilai default sebagai argumen. Sesuatu seperti salah satu cara berikut:

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

Yang pertama diizinkan ketika Anda query kolom NOT NULL dan yang kedua adalah cara yang digunakan untuk query kolom NULLABLE. Jika Anda menggunakan DefaultIfEmpty () tanpa argumen, nilai default akan ditentukan untuk tipe output Anda, seperti yang Anda lihat di Tabel Nilai Default .

SELECT yang dihasilkan tidak akan begitu elegan tetapi dapat diterima.

Semoga ini bisa membantu.

Fernando Brustolin
sumber
7

Saya pikir masalahnya adalah apa yang Anda inginkan terjadi ketika kueri tidak memiliki hasil. Jika ini merupakan kasus luar biasa maka saya akan membungkus kueri dalam blok coba / tangkap dan menangani pengecualian yang dihasilkan oleh kueri standar. Jika tidak ada permintaan untuk mengembalikan hasil, maka Anda harus mencari tahu apa yang Anda inginkan hasilnya dalam kasus itu. Mungkin jawaban @ David (atau yang serupa akan bekerja). Artinya, jika MAX akan selalu positif, maka mungkin cukup untuk memasukkan nilai "buruk" yang dikenal ke dalam daftar yang hanya akan dipilih jika tidak ada hasil. Secara umum, saya akan mengharapkan permintaan yang mengambil maksimum untuk memiliki beberapa data untuk bekerja dan saya akan pergi rute mencoba / menangkap karena jika tidak Anda selalu dipaksa untuk memeriksa apakah nilai yang Anda peroleh sudah benar atau tidak. SAYA'

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
sumber
Dalam kasus saya, kembali tanpa baris terjadi lebih sering daripada tidak (sistem legacy, pasien mungkin atau mungkin tidak memiliki kelayakan sebelumnya, bla bla bla). Jika ini adalah kasus yang lebih luar biasa, saya mungkin akan pergi rute ini (dan saya mungkin masih, tidak melihat jauh lebih baik).
gfrizzle
6

Kemungkinan lain adalah pengelompokan, mirip dengan cara Anda mendekatinya dalam SQL mentah:

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

Satu-satunya hal (pengujian lagi di LINQPad) beralih ke rasa VB LINQ memberikan kesalahan sintaks pada klausa pengelompokan. Saya yakin padanan konseptualnya cukup mudah ditemukan, saya hanya tidak tahu bagaimana cara mencerminkannya dalam VB.

SQL yang dihasilkan akan menjadi sesuatu seperti:

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 bersarang tampak menjengkelkan, seperti eksekusi kueri akan mengambil semua baris kemudian pilih yang cocok dari set yang diambil ... pertanyaannya adalah apakah SQL Server mengoptimalkan kueri menjadi sesuatu yang sebanding dengan menerapkan klausa mana ke SELECT bagian dalam. Saya sedang melihat itu sekarang ...

Saya tidak berpengalaman dalam menafsirkan rencana eksekusi di SQL Server, tetapi sepertinya ketika klausa WHERE ada di luar SELECT, jumlah baris aktual yang menghasilkan langkah itu adalah semua baris dalam tabel, dibandingkan hanya baris yang cocok. ketika klausa WHERE ada di SELECT bagian dalam. Yang mengatakan, sepertinya hanya 1% biaya dialihkan ke langkah berikut ketika semua baris dipertimbangkan, dan bagaimanapun hanya satu baris yang kembali dari SQL Server jadi mungkin itu bukan perbedaan besar dalam skema besar hal .

Rex Miller
sumber
6

terlambat, tapi saya memiliki masalah yang sama ...

Mengulang kode Anda dari posting asli, Anda ingin maks set S didefinisikan oleh

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

Mempertimbangkan komentar terakhir Anda

Cukuplah untuk mengatakan bahwa saya tahu saya ingin 0 ketika tidak ada catatan untuk dipilih, yang pasti berdampak pada solusi akhirnya

Saya dapat menguraikan kembali masalah Anda sebagai: Anda ingin maks {0 + S}. Dan sepertinya solusi yang diusulkan dengan concat secara semantik adalah yang benar :-)

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

Kenapa Bukan sesuatu yang lebih langsung seperti:

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

Satu perbedaan menarik yang tampaknya perlu diperhatikan adalah bahwa sementara FirstOrDefault dan Take (1) menghasilkan SQL yang sama (menurut LINQPad,), FirstOrDefault mengembalikan nilai - default - saat tidak ada baris yang cocok dan Take (1) kembali tidak ada hasil ... setidaknya di LINQPad.

Rex Miller
sumber
1

Hanya untuk membuat semua orang di luar sana tahu bahwa menggunakan Linq untuk Entitas metode di atas tidak akan berfungsi ...

Jika Anda mencoba melakukan sesuatu seperti

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

Ini akan menghasilkan pengecualian:

System.NotSupportedException: Node ekspresi LINQ ketik 'NewArrayInit' tidak didukung di LINQ ke Entitas ..

Saya sarankan hanya melakukan

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

Dan FirstOrDefaultakan mengembalikan 0 jika daftar Anda kosong.

Nix
sumber
Memesan dapat menyebabkan penurunan kinerja yang serius dengan kumpulan data besar. Ini adalah cara yang sangat tidak efisien untuk menemukan nilai maksimal.
Peter Bruins
1
decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;
jong su.
sumber
1

Saya telah mengetuk MaxOrDefaultmetode ekstensi. Tidak banyak, tetapi kehadirannya di Intellisense adalah pengingat yang berguna bahwa Maxpada urutan kosong akan menyebabkan pengecualian. Selain itu, metode ini memungkinkan standar untuk ditentukan jika diperlukan.

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

Untuk Entity Framework dan Linq to SQL kita dapat mencapai ini dengan mendefinisikan metode ekstensi yang memodifikasi metode yang Expressionditeruskan ke 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);
    }
}

Pemakaian:

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

Query yang dihasilkan identik, ini berfungsi seperti panggilan normal ke IQueryable<T>.Max(...)metode, tetapi jika tidak ada catatan, ia mengembalikan nilai standar tipe T alih-alih melempar pengecualian.

Ashot Muradian
sumber
-1

Saya baru saja mengalami masalah yang sama, tes unit saya lulus menggunakan Max () tetapi gagal ketika dijalankan terhadap database hidup.

Solusi saya adalah memisahkan kueri dari logika yang dilakukan, bukan bergabung dalam satu kueri.
Saya membutuhkan solusi untuk bekerja dalam unit test menggunakan Linq-objects (di Linq-objects Max () bekerja dengan nulls) dan Linq-sql ketika mengeksekusi di lingkungan langsung.

(Saya mengejek Select () di tes saya)

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

Kurang efisien? Mungkin.

Apakah saya peduli, selama aplikasi saya tidak jatuh di waktu berikutnya? Nggak.

Seb
sumber