Tối đa hay mặc định?


176

Cách tốt nhất để nhận giá trị Max từ truy vấn LINQ có thể không trả về hàng nào? Nếu tôi chỉ làm

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

Tôi gặp lỗi khi truy vấn không có hàng. tôi có thể làm

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

nhưng điều đó cảm thấy một chút khó khăn cho một yêu cầu đơn giản như vậy. Tôi có thiếu một cách tốt hơn để làm điều đó?

CẬP NHẬT: Đây là câu chuyện phía sau: Tôi đang cố gắng truy xuất bộ đếm đủ điều kiện tiếp theo từ một bảng con (hệ thống cũ, đừng để tôi bắt đầu ...). Hàng đủ điều kiện đầu tiên cho mỗi bệnh nhân luôn là 1, thứ hai là 2, v.v. (rõ ràng đây không phải là khóa chính của bảng con). Vì vậy, tôi đang chọn giá trị truy cập tối đa hiện có cho một bệnh nhân và sau đó thêm 1 vào giá trị đó để tạo một hàng mới. Khi không có giá trị con hiện có, tôi cần truy vấn trả về 0 (vì vậy việc thêm 1 sẽ cho tôi giá trị truy cập là 1). Lưu ý rằng tôi không muốn dựa vào số lượng hàng con thô, trong trường hợp ứng dụng cũ đưa ra các khoảng trống trong các giá trị truy cập (có thể). Xấu của tôi để cố gắng làm cho câu hỏi quá chung chung.

Câu trả lời:


206

DefaultIfEmptykhông được triển khai trong LINQ to SQL, tôi đã tìm kiếm lỗi mà nó đã trả về và tìm thấy một bài viết hấp dẫn liên quan đến các tập hợp null trong các hàm tổng hợp. Để tóm tắt những gì tôi tìm thấy, bạn có thể vượt qua giới hạn này bằng cách chuyển thành không thể trong phạm vi lựa chọn của bạn. VB của tôi hơi rỉ sét, nhưng tôi nghĩ nó sẽ giống như thế này:

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

Hoặc trong C #:

var x = (from y in context.MyTable
         where y.MyField == value
         select (int?)y.MyCounter).Max();

1
Để sửa VB, Chọn sẽ là "Chọn CType (y.MyCorer, Integer?)". Tôi phải thực hiện kiểm tra ban đầu để chuyển đổi Không thành 0 cho mục đích của mình, nhưng tôi thích nhận được kết quả mà không có ngoại lệ.
gfriheads

2
Một trong hai tình trạng quá tải của Default IfEmpty được hỗ trợ trong LINQ to SQL - một trong những điều không tham số.
DamienG

Có thể thông tin này đã lỗi thời, vì tôi vừa thử nghiệm thành công cả hai dạng Default IfEmpty trong LINQ to SQL
Neil

3
@Neil: vui lòng trả lời. Mặc định IfEmpty không hoạt động với tôi: Tôi muốn Maxa DateTime. Max(x => (DateTime?)x.TimeStamp)vẫn là cách duy nhất ..
duedl0r

1
Mặc dù Default IfEmpty hiện được triển khai trong LINQ to SQL, nhưng câu trả lời này vẫn tốt hơn IMO, vì sử dụng Default IfEmpty dẫn đến một câu lệnh SQL 'CHỌN MyCorer' trả về một hàng cho mọi giá trị được tính tổng , trong khi câu trả lời này dẫn đến MAX (MyCorer) trả về đơn, tổng hàng. (Đã thử nghiệm trong EntityFrameworkCore 2.1.3.)
Carl Sharman

107

Tôi chỉ gặp một vấn đề tương tự, nhưng tôi đã sử dụng các phương thức mở rộng LINQ trong danh sách thay vì cú pháp truy vấn. Việc sử dụng thủ thuật Nullable cũng hoạt động ở đó:

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

48

Âm thanh giống như một trường hợp cho DefaultIfEmpty(mã chưa được kiểm tra sau):

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

Tôi không quen thuộc với Default IfEmpty, nhưng tôi nhận được "Không thể định dạng nút 'OptionsValue' để thực thi dưới dạng SQL" khi sử dụng cú pháp ở trên. Tôi cũng đã thử cung cấp một giá trị mặc định (không), nhưng nó cũng không giống như vậy.
gfriheads

Ah. Có vẻ như Default IfEmpty không được hỗ trợ trong LINQ to SQL. Bạn có thể khắc phục điều đó bằng cách chuyển sang danh sách trước bằng .ToList nhưng đó là một thành tích đáng kể.
Jacob Proffitt

3
Cảm ơn, đây chính xác là những gì tôi đang tìm kiếm. Sử dụng các phương thức mở rộng:var colCount = RowsEnumerable.Select(row => row.Cols.Count).DefaultIfEmpty().Max()
Jani

35

Hãy suy nghĩ về những gì bạn đang hỏi!

Tối đa của {1, 2, 3, -1, -2, -3} rõ ràng là 3. Tối đa của {2} rõ ràng là 2. Nhưng tối đa của tập hợp trống {} là gì? Rõ ràng đó là một câu hỏi vô nghĩa. Tối đa của tập hợp trống chỉ đơn giản là không được xác định. Cố gắng để có được một câu trả lời là một lỗi toán học. Tối đa của bất kỳ tập hợp nào phải là một thành phần trong tập hợp đó. Tập hợp trống không có phần tử, do đó, tuyên bố rằng một số cụ thể là tối đa của tập hợp đó mà không có trong tập hợp đó là một mâu thuẫn toán học.

Giống như hành vi đúng để máy tính ném ngoại lệ khi lập trình viên yêu cầu nó chia cho 0, do đó, đó là hành vi đúng để máy tính ném ngoại lệ khi lập trình viên yêu cầu lấy tối đa của tập rỗng. Chia cho số 0, lấy tối đa của tập hợp trống, vẫy chiếc spacklerorke và cưỡi con kỳ lân bay đến Neverland đều vô nghĩa, không thể, không xác định.

Bây giờ, bạn thực sự muốn làm gì?


Điểm hay - Tôi sẽ cập nhật câu hỏi của tôi ngay với những chi tiết đó. Đủ để nói rằng tôi biết tôi muốn 0 khi không có hồ sơ để chọn, điều này chắc chắn có tác động đến giải pháp cuối cùng.
gfriheads

17
Tôi thường cố bay con kỳ lân của mình đến Neverland và tôi xúc phạm đề nghị của bạn rằng những nỗ lực của tôi là vô nghĩa và không xác định.
Chris hét lên

2
Tôi không nghĩ lập luận này là đúng. Đó là linq-to-sql rõ ràng và trong sql Max trên 0 hàng được định nghĩa là null, không?
duedl0r

4
Linq thường tạo ra kết quả giống hệt nhau cho dù truy vấn được thực thi trong bộ nhớ trên các đối tượng hay liệu truy vấn được thực hiện tại cơ sở dữ liệu trên các hàng. Các truy vấn Linq là các truy vấn Linq và nên được thực hiện một cách trung thực bất kể bộ điều hợp nào đang được sử dụng.
yfeldblum

1
Mặc dù tôi đồng ý về mặt lý thuyết rằng các kết quả Linq nên giống hệt nhau cho dù được thực hiện trong bộ nhớ hoặc trong sql, khi bạn thực sự đào sâu hơn một chút, bạn khám phá lý do tại sao điều này không thể luôn luôn như vậy. Biểu thức Linq được dịch sang sql bằng cách sử dụng dịch biểu thức phức tạp. Nó không phải là một bản dịch một-một đơn giản. Một sự khác biệt là trường hợp null. Trong C # "null == null" là đúng. Trong SQL, các kết quả khớp "null == null" được bao gồm cho các phép nối ngoài nhưng không bao gồm các phép nối bên trong. Tuy nhiên, tham gia bên trong hầu như luôn luôn là những gì bạn muốn vì vậy chúng là mặc định. Điều này gây ra sự khác biệt có thể trong hành vi.
Curtis Yallop

25

Bạn luôn có thể thêm Double.MinValuevào chuỗi. Điều này sẽ đảm bảo rằng có ít nhất một yếu tố và Maxsẽ chỉ trả lại nếu nó thực sự là mức tối thiểu. Để xác định lựa chọn là hiệu quả hơn ( Concat, FirstOrDefaulthay Take(1)), bạn nên thực hiện đầy đủ điểm chuẩn.

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;

Nếu danh sách có bất kỳ phần tử nào (nghĩa là không trống), nó sẽ lấy tối đa của trường MyCorer, phần khác sẽ trả về 0.


3
Điều này sẽ không chạy 2 truy vấn?
andreapier

10

Kể từ .Net 3.5, bạn có thể sử dụng Default IfEmpty () truyền giá trị mặc định làm đối số. Một cái gì đó giống như một trong những cách sau:

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

Cái đầu tiên được cho phép khi bạn truy vấn cột KHÔNG NULL và cái thứ hai là cách người ta sử dụng nó để truy vấn cột NULLABLE. Nếu bạn sử dụng Default IfEmpty () mà không có đối số, giá trị mặc định sẽ được xác định theo loại đầu ra của bạn, như bạn có thể thấy trong Bảng Giá trị mặc định .

Kết quả CHỌN sẽ không thanh lịch nhưng có thể chấp nhận được.

Hy vọng nó giúp.


7

Tôi nghĩ vấn đề là bạn muốn điều gì xảy ra khi truy vấn không có kết quả. Nếu đây là trường hợp ngoại lệ thì tôi sẽ bọc truy vấn trong khối try / Catch và xử lý ngoại lệ mà truy vấn tiêu chuẩn tạo ra. Nếu không có kết quả nào để truy vấn không có kết quả, thì bạn cần phải tìm ra kết quả bạn muốn trong trường hợp đó. Nó có thể là câu trả lời của @ David (hoặc một cái gì đó tương tự sẽ hoạt động). Nghĩa là, nếu MAX sẽ luôn dương, thì có thể đủ để chèn một giá trị "xấu" đã biết vào danh sách sẽ chỉ được chọn nếu không có kết quả. Nói chung, tôi mong đợi một truy vấn đang truy xuất tối đa để có một số dữ liệu hoạt động và tôi sẽ đi theo lộ trình thử / bắt vì nếu không bạn luôn bị buộc phải kiểm tra xem giá trị bạn nhận được có chính xác hay không. TÔI'

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

Trong trường hợp của tôi, việc trả lại không có hàng xảy ra thường xuyên hơn không (hệ thống kế thừa, bệnh nhân có thể có hoặc không có đủ điều kiện trước đó, blah blah blah). Nếu đây là một trường hợp đặc biệt hơn, có lẽ tôi sẽ đi theo con đường này (và tôi vẫn có thể, không thấy tốt hơn nhiều).
gfriheads

6

Một khả năng khác sẽ được nhóm lại, tương tự như cách bạn có thể tiếp cận nó trong SQL thô:

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

Điều duy nhất là (thử nghiệm lại trong LINQPad) chuyển sang hương vị VB LINQ đưa ra lỗi cú pháp trên mệnh đề nhóm. Tôi chắc rằng tương đương khái niệm là đủ dễ tìm, tôi chỉ không biết làm thế nào để phản ánh nó trong VB.

SQL được tạo sẽ là một cái gì đó dọc theo dòng:

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

CHỌN lồng nhau trông có vẻ kỳ cục, giống như thực thi truy vấn sẽ truy xuất tất cả các hàng sau đó chọn một hàng trùng khớp từ tập đã truy xuất ... câu hỏi đặt ra là SQL Server có tối ưu hóa truy vấn thành một cái gì đó có thể so sánh với việc áp dụng mệnh đề where cho SELECT bên trong hay không. Bây giờ tôi đang xem xét điều đó ...

Tôi không thành thạo trong việc diễn giải các kế hoạch thực hiện trong SQL Server, nhưng có vẻ như khi mệnh đề WHERE nằm ở CHỌN bên ngoài, số lượng hàng thực tế dẫn đến bước đó là tất cả các hàng trong bảng, so với chỉ các hàng khớp khi mệnh đề WHERE nằm trên CHỌN bên trong. Điều đó nói rằng, có vẻ như chỉ có 1% chi phí được chuyển sang bước tiếp theo khi tất cả các hàng được xem xét và theo cách nào đó chỉ có một hàng trở lại từ SQL Server, vì vậy có lẽ nó không phải là một sự khác biệt lớn trong sơ đồ lớn của mọi thứ .


6

đến muộn, nhưng tôi có cùng mối quan tâm ...

Đọc lại mã của bạn từ bài đăng gốc, bạn muốn tối đa của tập S được xác định bởi

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

Có tính đến bình luận cuối cùng của bạn

Đủ để nói rằng tôi biết tôi muốn 0 khi không có bản ghi nào để chọn, điều này chắc chắn có tác động đến giải pháp cuối cùng

Tôi có thể viết lại vấn đề của bạn là: Bạn muốn tối đa {0 + S}. Và có vẻ như giải pháp được đề xuất với concat về mặt ngữ nghĩa là đúng :-)

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

3

Tại sao không phải là một cái gì đó trực tiếp hơn như:

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

1

Một điểm khác biệt thú vị có vẻ đáng chú ý là trong khi FirstOrDefault và Take (1) tạo cùng một SQL (theo LINQPad, dù sao), FirstOrDefault trả về một giá trị - mặc định - khi không có hàng nào khớp và trả về (1) không có kết quả ... ít nhất là trong LINQPad.


1

Chỉ cần cho mọi người biết rằng đang sử dụng Linq cho Thực thể, các phương pháp trên sẽ không hoạt động ...

Nếu bạn cố gắng làm một cái gì đó như

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

Nó sẽ ném một ngoại lệ:

System.NotSupportedException: Loại nút biểu thức LINQ 'NewArrayInit' không được hỗ trợ trong LINQ to Entities ..

Tôi sẽ đề nghị chỉ làm

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

FirstOrDefaultsẽ trả về 0 nếu danh sách của bạn trống.


Đặt hàng có thể dẫn đến suy giảm hiệu suất nghiêm trọng với các bộ dữ liệu lớn. Đó là một cách rất không hiệu quả để tìm một giá trị tối đa.
Peter Bruins

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

1

Tôi đã gõ một MaxOrDefaultphương pháp mở rộng. Không có gì nhiều nhưng sự hiện diện của nó trong Intellisense là một lời nhắc hữu ích rằng Maxtrên một chuỗi trống sẽ gây ra ngoại lệ. Ngoài ra, phương thức cho phép mặc định được chỉ định nếu được yêu cầu.

    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

Đối với Entity Framework và Linq to SQL, chúng ta có thể đạt được điều này bằng cách định nghĩa một phương thức mở rộng sửa đổi một phương thức Expressionđược truyền cho 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);
    }
}

Sử dụng:

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

Truy vấn được tạo giống hệt nhau, nó hoạt động giống như một cuộc gọi đến IQueryable<T>.Max(...)phương thức bình thường , nhưng nếu không có bản ghi, nó trả về giá trị mặc định của loại T thay vì ném ra một ngoại lệ


-1

Tôi chỉ gặp một vấn đề tương tự, các bài kiểm tra đơn vị của tôi đã vượt qua bằng Max () nhưng không thành công khi chạy với cơ sở dữ liệu trực tiếp.

Giải pháp của tôi là tách truy vấn khỏi logic đang được thực hiện, không tham gia chúng trong một truy vấn.
Tôi cần một giải pháp để làm việc trong các bài kiểm tra đơn vị bằng cách sử dụng các đối tượng Linq (trong Linq-object Max () hoạt động với null) và Linq-sql khi thực hiện trong môi trường sống.

(Tôi chế giễu Chọn () trong các thử nghiệm của mình)

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

Kém hiệu quả? Có lẽ.

Tôi có quan tâm, miễn là lần sau ứng dụng của tôi không bị đổ? Không.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.