วิธีนับแถวภายใน EntityFramework โดยไม่ต้องโหลดเนื้อหา


109

ฉันกำลังพยายามหาวิธีนับแถวที่ตรงกันบนตารางโดยใช้ EntityFramework

ปัญหาคือแต่ละแถวอาจมีข้อมูลหลายเมกะไบต์ (ในเขตข้อมูลไบนารี) แน่นอนว่า SQL จะเป็นแบบนี้:

SELECT COUNT(*) FROM [MyTable] WHERE [fkID] = '1';

ฉันสามารถโหลดทุกแถวจากนั้นค้นหา Count ด้วย:

var owner = context.MyContainer.Where(t => t.ID == '1');
owner.MyTable.Load();
var count = owner.MyTable.Count();

แต่นั่นก็ไร้ประสิทธิภาพอย่างสิ้นเชิง มีวิธีที่ง่ายกว่านี้ไหม


แก้ไข: ขอบคุณทุกคน ฉันได้ย้ายฐานข้อมูลจากไฟล์แนบส่วนตัวดังนั้นฉันจึงสามารถเรียกใช้การสร้างโปรไฟล์ได้ สิ่งนี้ช่วยได้ แต่ทำให้เกิดความสับสนที่ฉันไม่คาดคิด

และข้อมูลที่เป็นจริงของฉันเป็นบิตลึกผมจะใช้รถบรรทุกแบกพาเลทของกรณีของรายการ - และฉันไม่ต้องการให้รถบรรทุกที่จะออกจนกว่าจะมีอย่างน้อยหนึ่งรายการในนั้น

ความพยายามของฉันแสดงไว้ด้านล่าง ส่วนที่ฉันไม่ได้รับคือ CASE_2 ไม่เคยเข้าถึงเซิร์ฟเวอร์ DB (MSSQL)

var truck = context.Truck.FirstOrDefault(t => (t.ID == truckID));
if (truck == null)
    return "Invalid Truck ID: " + truckID;
var dlist = from t in ve.Truck
    where t.ID == truckID
    select t.Driver;
if (dlist.Count() == 0)
    return "No Driver for this Truck";

var plist = from t in ve.Truck where t.ID == truckID
    from r in t.Pallet select r;
if (plist.Count() == 0)
    return "No Pallets are in this Truck";
#if CASE_1
/// This works fine (using 'plist'):
var list1 = from r in plist
    from c in r.Case
    from i in c.Item
    select i;
if (list1.Count() == 0)
    return "No Items are in the Truck";
#endif

#if CASE_2
/// This never executes any SQL on the server.
var list2 = from r in truck.Pallet
        from c in r.Case
        from i in c.Item
        select i;
bool ok = (list.Count() > 0);
if (!ok)
    return "No Items are in the Truck";
#endif

#if CASE_3
/// Forced loading also works, as stated in the OP...
bool ok = false;
foreach (var pallet in truck.Pallet) {
    pallet.Case.Load();
    foreach (var kase in pallet.Case) {
        kase.Item.Load();
        var item = kase.Item.FirstOrDefault();
        if (item != null) {
            ok = true;
            break;
        }
    }
    if (ok) break;
}
if (!ok)
    return "No Items are in the Truck";
#endif

และ SQL ที่เป็นผลมาจาก CASE_1 ถูกส่งผ่านsp_executesqlแต่:

SELECT [Project1].[C1] AS [C1]
FROM   ( SELECT cast(1 as bit) AS X ) AS [SingleRowTable1]
LEFT OUTER JOIN  (SELECT 
    [GroupBy1].[A1] AS [C1]
    FROM ( SELECT 
        COUNT(cast(1 as bit)) AS [A1]
        FROM   [dbo].[PalletTruckMap] AS [Extent1]
        INNER JOIN [dbo].[PalletCaseMap] AS [Extent2] ON [Extent1].[PalletID] = [Extent2].[PalletID]
        INNER JOIN [dbo].[Item] AS [Extent3] ON [Extent2].[CaseID] = [Extent3].[CaseID]
        WHERE [Extent1].[TruckID] = '....'
    )  AS [GroupBy1] ) AS [Project1] ON 1 = 1

[ ฉันไม่มีรถบรรทุกไดรเวอร์พาเลทเคสหรือไอเท็มจริงๆ อย่างที่คุณเห็นจาก SQL ความสัมพันธ์ของ Truck-Pallet และ Pallet-Case นั้นเป็นแบบหลายต่อหลายคน - แม้ว่าฉันจะไม่คิดว่ามันสำคัญ วัตถุที่แท้จริงของฉันเป็นสิ่งที่จับต้องไม่ได้และยากที่จะอธิบายฉันจึงเปลี่ยนชื่อ ]


1
คุณแก้ปัญหาการโหลดพาเลทอย่างไร
Sherlock

คำตอบ:


123

ไวยากรณ์การสืบค้น:

var count = (from o in context.MyContainer
             where o.ID == '1'
             from t in o.MyTable
             select t).Count();

ไวยากรณ์ของวิธีการ:

var count = context.MyContainer
            .Where(o => o.ID == '1')
            .SelectMany(o => o.MyTable)
            .Count()

ทั้งสองสร้างแบบสอบถาม SQL เดียวกัน


ทำไมSelectMany()? จำเป็นไหม? มันจะไม่ทำงานได้ดีถ้าไม่มีมัน?
Jo Smo

@JoSmo ไม่นั่นเป็นคำถามที่แตกต่างกันโดยสิ้นเชิง
Craig Stuntz

ขอบคุณที่เคลียร์เรื่องนี้ให้ฉัน เพียงแค่ต้องการที่จะให้แน่ใจว่า. :)
Jo Smo

1
คุณช่วยบอกฉันได้ไหมว่าเหตุใด SelectMany จึงแตกต่างกัน ฉันไม่เข้าใจ. ฉันทำโดยไม่ต้อง SelectMany แต่มันช้ามากเพราะฉันมีบันทึกมากกว่า 20 ล้านรายการ ฉันลองคำตอบจาก Yang Zhang และใช้งานได้ดีเพียงแค่อยากรู้ว่า SelectMany ทำอะไร
mikesoft

1
@AustinFelipe หากไม่มีการเรียก SelectMany แบบสอบถามจะส่งคืนจำนวนแถวใน MyContainer ด้วย ID เท่ากับ '1' การเรียก SelectMany ส่งคืนแถวทั้งหมดใน MyTable ที่เป็นของผลลัพธ์ก่อนหน้าของแบบสอบถาม (หมายถึงผลลัพธ์ของMyContainer.Where(o => o.ID == '1'))
sbecker

49

ฉันคิดว่าคุณต้องการอะไรเช่น

var count = context.MyTable.Count(t => t.MyContainer.ID == '1');

(แก้ไขเพื่อแสดงความคิดเห็น)


1
ไม่เขาต้องการจำนวนเอนทิตีใน MyTable ที่อ้างอิงโดยเอนทิตีหนึ่งที่มี ID = 1 ใน MyContainer
Craig Stuntz

3
อนึ่งหาก t.ID เป็น PK การนับในรหัสด้านบนจะเป็น 1 เสมอ :)
Craig Stuntz

2
@ Craig คุณพูดถูกฉันควรใช้ t.ForeignTable.ID อัปเดตแล้ว
Kevin

1
นี่คือสั้นและเรียบง่าย ทางเลือกของฉันคือ: var count = context.MyTable.Count(t => t.MyContainer.ID == '1'); ไม่ยาวและน่าเกลียด: var count = (from o in context.MyContainer where o.ID == '1' from t in o.MyTable select t).Count(); แต่ขึ้นอยู่กับรูปแบบการเข้ารหัส ...
CL

ตรวจสอบให้แน่ใจว่าคุณใส่ "using System.Linq" ไว้ไม่เช่นนั้นจะไม่ได้ผล
CountMurphy

17

ตามที่ฉันเข้าใจคำตอบที่เลือกยังคงโหลดการทดสอบที่เกี่ยวข้องทั้งหมด ตามบล็อก msdn นี้มีวิธีที่ดีกว่า

http://blogs.msdn.com/b/adonet/archive/2011/01/31/using-dbcontext-in-ef-feature-ctp5-part-6-loading-related-entities.aspx

โดยเฉพาะ

using (var context = new UnicornsContext())

    var princess = context.Princesses.Find(1);

    // Count how many unicorns the princess owns 
    var unicornHaul = context.Entry(princess)
                      .Collection(p => p.Unicorns)
                      .Query()
                      .Count();
}

4
ไม่จำเป็นต้องทำการFind(1)ร้องขอเพิ่มเติม เพียงสร้างเอนทิตีและแนบกับบริบท:var princess = new PrincessEntity{ Id = 1 }; context.Princesses.Attach(princess);
tenbits

13

นี่คือรหัสของฉัน:

IQueryable<AuctionRecord> records = db.AuctionRecord;
var count = records.Count();

ตรวจสอบให้แน่ใจว่าตัวแปรถูกกำหนดให้เป็น IQueryable จากนั้นเมื่อคุณใช้วิธี Count () EF จะดำเนินการบางอย่างเช่น

select count(*) from ...

มิฉะนั้นหากกำหนดระเบียนเป็น IEnumerable sql ที่สร้างขึ้นจะค้นหาทั้งตารางและนับแถวที่ส่งคืน


10

แม้ว่าSELECT COUNT(*) FROM Tableจะค่อนข้างไม่มีประสิทธิภาพโดยเฉพาะในตารางขนาดใหญ่เนื่องจาก SQL Server ไม่สามารถทำอะไรได้นอกจากทำการสแกนแบบเต็มตาราง (การสแกนดัชนีคลัสเตอร์)

ในบางครั้งการทราบจำนวนแถวโดยประมาณจากฐานข้อมูลก็เป็นการดีและในกรณีเช่นนี้คำสั่งเช่นนี้อาจเพียงพอ:

SELECT 
    SUM(used_page_count) * 8 AS SizeKB,
    SUM(row_count) AS [RowCount], 
    OBJECT_NAME(OBJECT_ID) AS TableName
FROM 
    sys.dm_db_partition_stats
WHERE 
    OBJECT_ID = OBJECT_ID('YourTableNameHere')
    AND (index_id = 0 OR index_id = 1)
GROUP BY 
    OBJECT_ID

สิ่งนี้จะตรวจสอบมุมมองการจัดการแบบไดนามิกและดึงจำนวนแถวและขนาดตารางออกจากตารางโดยระบุตารางที่เจาะจง ทำได้โดยการสรุปรายการสำหรับฮีป (index_id = 0) หรือดัชนีคลัสเตอร์ (index_id = 1)

รวดเร็วใช้งานง่าย แต่ไม่รับประกันว่าจะถูกต้อง 100% หรือเป็นข้อมูลล่าสุด แต่ในหลาย ๆ กรณีนี่คือ "ดีพอ" (และทำให้เซิร์ฟเวอร์มีภาระน้อยลงมาก)

บางทีนั่นอาจจะเหมาะกับคุณด้วย? แน่นอนในการใช้งานใน EF คุณจะต้องรวมสิ่งนี้ไว้ใน proc ที่จัดเก็บไว้หรือใช้การเรียก "Execute SQL query"

มาร์ค


1
จะไม่เป็นการสแกนแบบเต็มตารางเนื่องจากการอ้างอิง FK ใน WHERE จะสแกนเฉพาะรายละเอียดของต้นแบบเท่านั้น ปัญหาด้านประสิทธิภาพที่เขาพบมาจากการโหลดข้อมูลหยดไม่ใช่จำนวนบันทึก โดยปกติแล้วการสันนิษฐานว่าโดยทั่วไปจะมีบันทึกรายละเอียดไม่เกินสิบรายการต่อบันทึกหลักฉันจะไม่ "เพิ่มประสิทธิภาพ" สิ่งที่ไม่ช้า
Craig Stuntz

ตกลงใช่ในกรณีนั้นคุณจะเลือกเฉพาะส่วนย่อยซึ่งก็น่าจะใช้ได้ สำหรับข้อมูลหยด - ฉันอยู่ภายใต้การแสดงผลที่คุณสามารถตั้งค่า "การโหลดที่รอการตัดบัญชี" ในคอลัมน์ใดก็ได้ในตาราง EF ของคุณเพื่อหลีกเลี่ยงการโหลดซึ่งอาจช่วยได้
marc_s

มีวิธีใช้ SQL นี้กับ EntityFramework หรือไม่? อย่างไรก็ตามในกรณีนี้ฉันแค่ต้องการรู้ว่ามีแถวที่ตรงกัน แต่ฉันตั้งใจถามคำถามโดยทั่วไปมากกว่า
NVRAM

4

ใช้เมธอดExecuteStoreQueryของบริบทเอนทิตี วิธีนี้จะหลีกเลี่ยงการดาวน์โหลดชุดผลลัพธ์ทั้งหมดและการแยกส่วนย่อยลงในออบเจ็กต์เพื่อนับแถวง่ายๆ

   int count;

    using (var db = new MyDatabase()){
      string sql = "SELECT COUNT(*) FROM MyTable where FkId = {0}";

      object[] myParams = {1};
      var cntQuery = db.ExecuteStoreQuery<int>(sql, myParams);

      count = cntQuery.First<int>();
    }

6
ถ้าคุณเขียนint count = context.MyTable.Count(m => m.MyContainerID == '1')SQL ที่สร้างขึ้นจะคล้ายกับสิ่งที่คุณกำลังทำอยู่ แต่โค้ดนั้นดีกว่ามาก ไม่มีเอนทิตีถูกโหลดลงในหน่วยความจำเช่นนี้ ลองใช้ใน LINQPad ถ้าคุณต้องการ - จะแสดง SQL ที่ใช้ภายใต้ฝาครอบ
Drew Noakes

SQL ในบรรทัด . ไม่ใช่สิ่งที่ฉันชอบ
Duanne

3

คิดว่าน่าจะใช้ได้นะ ...

var query = from m in context.MyTable
            where m.MyContainerId == '1' // or what ever the foreign key name is...
            select m;

var count = query.Count();

นี่คือทิศทางที่ฉันไปในตอนแรกเช่นกัน แต่ฉันเข้าใจว่าถ้าคุณไม่ได้เพิ่มด้วยตนเอง m จะมีคุณสมบัติ MyContainer แต่ไม่มี MyContainerId ดังนั้นสิ่งที่คุณต้องการตรวจสอบคือ m.MyContainer.ID
Kevin

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