Wie zähle ich Zeilen in EntityFramework, ohne Inhalte zu laden?

109

Ich versuche , um zu bestimmen , wie man zählt die passenden Zeilen auf einem Tisch die EntityFramework verwenden.

Das Problem ist, dass jede Zeile möglicherweise viele Megabyte Daten enthält (in einem Binärfeld). Natürlich wäre das SQL ungefähr so:

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

Ich könnte alle Zeilen laden und dann die Anzahl finden mit:

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

Das ist aber grob ineffizient. Gibt es einen einfacheren Weg?


EDIT: Danke an alle. Ich habe die Datenbank aus einem privaten Anhang verschoben, damit ich die Profilerstellung ausführen kann. Das hilft, verursacht aber Verwirrungen, die ich nicht erwartet hatte.

Und meine realen Daten sind etwas tiefer, ich werde Lastwagen verwenden, die Paletten mit Kisten mit Gegenständen tragen - und ich möchte nicht, dass der Lastwagen verlässt, es sei denn, es ist mindestens ein Gegenstand darin.

Meine Versuche sind unten gezeigt. Der Teil, den ich nicht bekomme, ist, dass CASE_2 niemals auf den DB-Server (MSSQL) zugreift.

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

Und das aus CASE_1 resultierende SQL wird durch sp_executesql geleitet , aber:

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

[ Ich habe nicht wirklich Lastwagen, Fahrer, Paletten, Koffer oder Gegenstände; Wie Sie aus der SQL ersehen können, sind die Beziehungen zwischen LKW und Palette sowie zwischen Palette und Fall viele zu viele - obwohl ich nicht denke, dass dies wichtig ist. Meine realen Objekte sind immateriell und schwerer zu beschreiben, deshalb habe ich die Namen geändert. ]]

NVRAM
quelle
1
Wie haben Sie das Palettenladeproblem gelöst?
Sherlock

Antworten:

123

Abfragesyntax:

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

Methodensyntax:

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

Beide generieren dieselbe SQL-Abfrage.

Craig Stuntz
quelle
Warum das SelectMany()? Wird es gebraucht? Würde es ohne es nicht richtig funktionieren?
Jo Smo
@JoSmo, nein, das ist eine ganz andere Frage.
Craig Stuntz
Danke, dass du das für mich geklärt hast. Ich wollte nur sicher sein. :)
Jo Smo
1
Können Sie mir sagen, warum es beim SelectMany anders ist? Ich verstehe nicht. Ich mache es ohne SelectMany, aber es wird sehr langsam, weil ich über 20 Millionen Datensätze habe. Ich habe die Antwort von Yang Zhang ausprobiert und arbeite großartig. Ich wollte nur wissen, was SelectMany macht.
Mikeoft
1
@AustinFelipe Ohne den Aufruf von SelectMany würde die Abfrage die Anzahl der Zeilen in MyContainer mit der ID '1' zurückgeben. Der SelectMany-Aufruf gibt alle Zeilen in MyTable zurück, die zum vorherigen Ergebnis der Abfrage gehören (dh das Ergebnis von MyContainer.Where(o => o.ID == '1'))
sbecker
48

Ich denke du willst so etwas

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

(bearbeitet, um Kommentare wiederzugeben)

Kevin
quelle
1
Nein, er benötigt die Anzahl der Entitäten in MyTable, auf die von der einen Entität mit ID = 1 in MyContainer verwiesen wird
Craig Stuntz
3
Übrigens, wenn t.ID eine PK ist, dann wird im obigen Code immer 1 gezählt. :)
Craig Stuntz
2
@Craig, du hast recht, ich hätte t.ForeignTable.ID verwenden sollen. Aktualisiert.
Kevin
1
Nun, das ist kurz und einfach. Meine Wahl ist: var count = context.MyTable.Count(t => t.MyContainer.ID == '1'); nicht lang und hässlich: var count = (from o in context.MyContainer where o.ID == '1' from t in o.MyTable select t).Count(); Aber es hängt vom Codierungsstil ab ...
CL
Stellen Sie sicher, dass Sie "using System.Linq" angeben, da dies
sonst
16

Soweit ich weiß, lädt die ausgewählte Antwort immer noch alle zugehörigen Tests. Laut diesem MSDN-Blog gibt es einen besseren Weg.

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

Speziell

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();
}
Quickhorn
quelle
3
Es ist keine zusätzliche Find(1)Anfrage erforderlich . Erstellen Sie einfach die Entität und hängen Sie sie an den Kontext an:var princess = new PrincessEntity{ Id = 1 }; context.Princesses.Attach(princess);
Tenbits
13

Das ist mein Code:

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

Stellen Sie sicher, dass die Variable als IQueryable definiert ist. Wenn Sie dann die Count () -Methode verwenden, führt EF so etwas aus

select count(*) from ...

Wenn die Datensätze als IEnumerable definiert sind, fragt die generierte SQL ansonsten die gesamte zurückgegebene Tabelle ab und zählt die zurückgegebenen Zeilen.

Yang Zhang
quelle
10

Nun, selbst das SELECT COUNT(*) FROM Tablewird ziemlich ineffizient sein, insbesondere bei großen Tabellen, da SQL Server wirklich nur einen vollständigen Tabellenscan (Clustered Index Scan) durchführen kann.

Manchmal ist es gut genug, eine ungefähre Anzahl von Zeilen aus der Datenbank zu kennen, und in einem solchen Fall kann eine solche Aussage ausreichen:

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

Dadurch wird die dynamische Verwaltungsansicht überprüft und die Anzahl der Zeilen und die Tabellengröße anhand einer bestimmten Tabelle extrahiert. Dazu werden die Einträge für den Heap (index_id = 0) oder den Clustered-Index (index_id = 1) summiert.

Es ist schnell, einfach zu bedienen, aber es ist nicht garantiert 100% genau oder aktuell. In vielen Fällen ist dies jedoch "gut genug" (und belastet den Server erheblich weniger).

Vielleicht würde das auch bei Ihnen funktionieren? Um es in EF zu verwenden, müssten Sie dies natürlich in einem gespeicherten Prozess zusammenfassen oder einen direkten Aufruf "SQL-Abfrage ausführen" verwenden.

Marc

marc_s
quelle
1
Aufgrund der FK-Referenz im WHERE wird kein vollständiger Tabellenscan durchgeführt. Es werden nur Details des Masters gescannt. Das Leistungsproblem, das er hatte, war das Laden von Blob-Daten, nicht die Anzahl der Datensätze. Vorausgesetzt, es gibt normalerweise nicht mehr als zehntausende Detailaufzeichnungen pro Stammsatz, würde ich etwas, das nicht wirklich langsam ist, nicht "optimieren".
Craig Stuntz
OK, ja, in diesem Fall wählen Sie nur eine Teilmenge aus - das sollte in Ordnung sein. Was die Blob-Daten betrifft - ich hatte den Eindruck, dass Sie für jede Spalte in einer Ihrer EF-Tabellen ein "verzögertes Laden" festlegen können, um ein Laden zu vermeiden. Dies könnte also hilfreich sein.
marc_s
Gibt es eine Möglichkeit, diese SQL mit EntityFramework zu verwenden? In diesem Fall musste ich nur wissen, dass es übereinstimmende Zeilen gibt, aber ich habe die Frage absichtlich allgemeiner gestellt.
NVRAM
4

Verwenden Sie die ExecuteStoreQuery- Methode des Entitätskontexts. Dadurch wird vermieden, dass die gesamte Ergebnismenge heruntergeladen und in Objekte deserialisiert wird, um eine einfache Zeilenzählung durchzuführen.

   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>();
    }
goosemanjack
quelle
6
Wenn Sie schreiben, int count = context.MyTable.Count(m => m.MyContainerID == '1')ähnelt das generierte SQL genau dem, was Sie tun, aber der Code ist viel besser. Es werden keine Entitäten als solche in den Speicher geladen. Probieren Sie es in LINQPad aus, wenn Sie möchten - es zeigt Ihnen die SQL, die unter der Decke verwendet wird.
Drew Noakes
Inline-SQL. . nicht meine Lieblingssache.
Duanne
3

Ich denke das sollte funktionieren ...

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();
Bytebender
quelle
Dies ist auch die Richtung, in die ich zuerst gegangen bin, aber ich verstehe, dass m, wenn Sie es nicht manuell hinzugefügt haben, eine MyContainer-Eigenschaft, aber keine MyContainerId hat. Daher möchten Sie m.MyContainer.ID untersuchen.
Kevin
Wenn MyContainer das übergeordnete Element und MyTable die untergeordneten Elemente in der Beziehung sind, mussten Sie diese Beziehung mit einem Fremdschlüssel herstellen. Ich bin mir nicht sicher, woher Sie sonst wissen würden, welche MyTable-Entitäten mit einer MyContainer-Entität verknüpft sind ... Aber vielleicht ich machte eine Annahme über die Struktur ...
Bytebender