สูงสุดหรือค่าเริ่มต้น


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

แต่นั่นทำให้รู้สึกป้านเล็กน้อยสำหรับคำของ่าย ๆ ฉันพลาดวิธีที่ดีกว่าในการทำสิ่งนี้หรือไม่?

UPDATE: นี่คือเรื่องราวย้อนหลัง: ฉันกำลังพยายามดึงตัวนับสิทธิ์ต่อไปจากตารางลูก (ระบบเดิมอย่าเริ่มฉัน ... ) แถวแรกที่มีสิทธิ์สำหรับผู้ป่วยแต่ละรายคือ 1 เสมอที่สองคือ 2 เป็นต้น (เห็นได้ชัดว่านี่ไม่ใช่คีย์หลักของตารางเด็ก) ดังนั้นฉันเลือกค่าตัวนับที่มีอยู่สูงสุดสำหรับผู้ป่วยแล้วเพิ่ม 1 ตัวเพื่อสร้างแถวใหม่ เมื่อไม่มีค่าย่อยที่มีอยู่ฉันต้องการเคียวรีเพื่อส่งคืน 0 (ดังนั้นการเพิ่ม 1 จะให้ค่าตัวนับเป็น 1) โปรดทราบว่าฉันไม่ต้องการที่จะพึ่งพาจำนวนแถวของแถวรองในกรณีที่แอปรุ่นเก่าแนะนำช่องว่างในค่าตัวนับ (เป็นไปได้) ฉันไม่ดีที่พยายามทำคำถามที่กว้างเกินไป

คำตอบ:


206

เนื่องจากDefaultIfEmptyไม่ได้ใช้งานใน LINQ กับ SQL ฉันจึงค้นหาข้อผิดพลาดที่ส่งคืนและพบบทความที่น่าสนใจที่เกี่ยวข้องกับชุดค่า Null ในฟังก์ชันรวม เพื่อสรุปสิ่งที่ฉันพบคุณสามารถหลีกเลี่ยงข้อ จำกัด นี้ได้ด้วยการส่งให้เป็นโมฆะภายในการเลือกของคุณ 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 CType (y.MyCounter, Integer?)" ฉันต้องทำการตรวจสอบต้นฉบับเพื่อแปลงไม่มีอะไรเป็น 0 เพื่อจุดประสงค์ของฉัน แต่ฉันชอบรับผลลัพธ์โดยไม่มีข้อยกเว้น
gfrizzle

2
หนึ่งในสอง overloads ของ DefaultIfEmpty ได้รับการสนับสนุนใน LINQ ไปยัง SQL - หนึ่งที่ไม่ใช้พารามิเตอร์
DamienG

อาจเป็นไปได้ว่าข้อมูลนี้ล้าสมัยเพราะฉันเพิ่งทดสอบรูปแบบ DefaultIfEmpty ใน LINQ ถึง SQL ทั้งสองรูปแบบเรียบร้อยแล้ว
Neil

3
@ Neil: โปรดตอบ DefaultIfEmpty ไม่ทำงานสำหรับฉัน: ฉันต้องการของMax ยังคงเป็นวิธีเดียว ..DateTimeMax(x => (DateTime?)x.TimeStamp)
duedl0r

1
แม้ว่า DefaultIfEmpty จะถูกนำไปใช้ใน LINQ ไปยัง SQL คำตอบนี้จะยังคงดีกว่า IMO เช่นเดียวกับการใช้ DefaultIfEmpty ในคำสั่ง SQL 'SELECT MyCounter' ที่ส่งคืนแถวสำหรับทุกค่าที่ถูกรวมในขณะที่คำตอบนี้ส่งผลให้ MAX แถวเดียวที่รวมแล้ว (ทดสอบใน EntityFrameworkCore 2.1.3.)
Carl Sharman

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 แต่นั่นเป็นผลงานยอดเยี่ยม
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

17
ฉันพยายามบินยูนิคอร์นไปที่เนเวอร์แลนด์บ่อยครั้งและฉันก็ทำผิดต่อข้อเสนอแนะของคุณว่าความพยายามของฉันนั้นไร้ความหมายและไม่ได้กำหนด
Chris Shouts

2
ฉันไม่คิดว่าการโต้แย้งนี้ถูกต้อง มันชัดเจน linq-to-sql และใน sql Max มากกว่าศูนย์แถวถูกกำหนดเป็นโมฆะไม่?
duedl0r

4
โดยทั่วไปแล้ว Linq ควรสร้างผลลัพธ์ที่เหมือนกันไม่ว่าจะดำเนินการค้นหาในหน่วยความจำบนวัตถุหรือไม่ว่าจะดำเนินการแบบสอบถามที่ฐานข้อมูลในแถว คำสั่ง Linq เป็นคำสั่ง Linq และควรดำเนินการอย่างซื่อสัตย์ไม่ว่าอะแดปเตอร์ใดที่ใช้งานอยู่
yfeldblum

1
ในขณะที่ฉันเห็นด้วยในทางทฤษฎีแล้วผลลัพธ์ของ Linq ควรเหมือนกันไม่ว่าจะถูกประมวลผลในหน่วยความจำหรือใน sql แต่เมื่อคุณขุดลึกลงไปเล็กน้อยคุณจะค้นพบว่าทำไมสิ่งนี้ถึงไม่สามารถทำได้ การแสดงออกของ Linq ถูกแปลเป็น sql โดยใช้การแปลที่ซับซ้อน มันไม่ใช่การแปลแบบตัวต่อตัว ข้อแตกต่างอย่างหนึ่งคือกรณีของค่าว่าง ใน C # "null == null" เป็นจริง ใน SQL การจับคู่ "null == null" จะรวมอยู่ในการรวมภายนอก แต่ไม่ใช่สำหรับการรวมภายใน อย่างไรก็ตามการรวมภายในเป็นสิ่งที่คุณต้องการเกือบจะเป็นค่าเริ่มต้น สิ่งนี้ทำให้เกิดความแตกต่างในพฤติกรรม
เคอร์ติส Yallop

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

ฉันคิดว่าปัญหาคือสิ่งที่คุณต้องการให้เกิดขึ้นเมื่อแบบสอบถามไม่มีผลลัพธ์ หากเป็นกรณีพิเศษฉันจะตัดข้อความค้นหาในบล็อก try / catch และจัดการข้อยกเว้นที่แบบสอบถามมาตรฐานสร้างขึ้น หากไม่เป็นเช่นนั้นหากแบบสอบถามไม่ส่งคืนผลลัพธ์คุณจะต้องทราบว่าคุณต้องการให้ผลลัพธ์อยู่ในกรณีใด อาจเป็นได้ว่าคำตอบของ @ 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

ในกรณีของฉันไม่มีการส่งคืนแถวเกิดขึ้นบ่อยกว่าระบบเดิมผู้ป่วยอาจมีหรือไม่มีคุณสมบัติก่อนหน้านี้ blah blah blah หากนี่เป็นกรณีที่พิเศษกว่านี้ฉันอาจจะไปเส้นทางนี้ (และฉันอาจยังไม่เห็นดีขึ้นมาก)
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

เลือกซ้อนกันดู icky เช่นการดำเนินการแบบสอบถามจะดึงแถวทั้งหมดแล้วเลือกหนึ่งที่ตรงกันจากชุดที่ดึงมา ... คำถามคือว่า SQL Server ปรับให้เหมาะสมแบบสอบถามเป็นสิ่งที่เทียบเคียงได้กับการใช้คำสั่งที่จะเลือกส่วนภายใน ฉันกำลังมองหาที่ตอนนี้ ...

ฉันไม่เชี่ยวชาญในการตีความแผนการดำเนินการใน SQL Server แต่ดูเหมือนว่าเมื่อ WHERE clause อยู่ที่ SELECT ภายนอกจำนวนแถวที่แท้จริงที่เกิดขึ้นในขั้นตอนนั้นคือแถวทั้งหมดในตารางเทียบกับแถวที่ตรงกันเท่านั้น เมื่อ WHERE clause อยู่ใน SELECT ภายใน ที่กล่าวว่าดูเหมือนว่าต้นทุนเพียง 1% เท่านั้นที่ถูกย้ายไปยังขั้นตอนต่อไปนี้เมื่อพิจารณาทุกแถวและอาจมีเพียงหนึ่งแถวเท่านั้นที่กลับมาจาก SQL Server ดังนั้นอาจไม่ใช่ความแตกต่างที่ยิ่งใหญ่ในรูปแบบของสิ่งต่าง ๆ .


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


1

ข้อแตกต่างที่น่าสนใจอย่างหนึ่งที่น่าสังเกตคือในขณะที่ FirstOrDefault และ Take (1) สร้าง SQL เดียวกัน (ตาม LINQPad อย่างไรก็ตาม) FirstOrDefault จะส่งกลับค่า - ค่าเริ่มต้น - เมื่อไม่มีแถวที่ตรงกันและรับคืน (1) ไม่มีผลลัพธ์ ... อย่างน้อยใน LINQPad


1

เพียงแค่ให้ทุกคนรู้ว่ากำลังใช้ Linq กับ Entities วิธีการด้านบนจะไม่ทำงาน ...

ถ้าคุณลองทำอะไรซักอย่าง

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 หากรายการของคุณว่างเปล่า


การสั่งซื้ออาจทำให้ประสิทธิภาพลดลงอย่างมากด้วยชุดข้อมูลขนาดใหญ่ มันเป็นวิธีที่ไม่มีประสิทธิภาพในการหาค่าสูงสุด
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 ไปยัง 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 แทนการโยน exption


-1

ฉันเพิ่งมีปัญหาที่คล้ายกันการทดสอบหน่วยของฉันผ่านการใช้ Max () แต่ล้มเหลวเมื่อทำงานกับฐานข้อมูลสด

โซลูชันของฉันคือการแยกแบบสอบถามจากตรรกะที่กำลังดำเนินการไม่ได้เข้าร่วมในแบบสอบถามเดียว
ฉันต้องการโซลูชันเพื่อทำงานในการทดสอบหน่วยโดยใช้ Linq-objects (ใน Linq-objects Max () ทำงานร่วมกับ nulls) และ Linq-sql เมื่อทำงานในสภาพแวดล้อมจริง

(ฉันจำลองการเลือก () ในการทดสอบของฉัน)

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

มีประสิทธิภาพน้อยลงหรือไม่ อาจ.

ฉันจะสนใจตราบใดที่แอพของฉันไม่ล้มลงในครั้งต่อไป Nope

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.