Jak zliczać wiersze w ramach EntityFramework bez ładowania zawartości?

109

Próbuję określić, jak policzyć pasujące wiersze w tabeli przy użyciu EntityFramework.

Problem polega na tym, że każdy wiersz może zawierać wiele megabajtów danych (w polu binarnym). Oczywiście SQL wyglądałby mniej więcej tak:

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

Mogłem załadować wszystkie wiersze, a następnie znaleźć Count z:

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

Ale to jest rażąco nieefektywne. Czy jest prostszy sposób?


EDYCJA: Dziękuję wszystkim. Przeniosłem bazę danych z dołączonego prywatnego, więc mogę uruchomić profilowanie; to pomaga, ale powoduje zamieszanie, którego się nie spodziewałem.

I moje prawdziwe dane jest nieco głębiej, użyję Trucks przewożących Palety o sprawach z pozycji - i nie chcą Truck opuścić chyba że istnieje co najmniej jedna pozycja w nim.

Moje próby są pokazane poniżej. Część, której nie rozumiem, to to, że CASE_2 nigdy nie uzyskuje dostępu do serwera 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 wynikający z CASE_1 jest przesyłany potokiem przez sp_executesql , ale:

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

[ Naprawdę nie mam ciężarówek, kierowców, palet, skrzyń ani przedmiotów; jak widać z SQL, relacje Ciężarówka-Paleta i Paleta-Skrzynia to wiele do wielu - chociaż nie sądzę, żeby to miało znaczenie. Moje prawdziwe przedmioty są niematerialne i trudniejsze do opisania, więc zmieniłem nazwy. ]

NVRAM
źródło
1
jak rozwiązałeś problem z ładowaniem palet?
Sherlock

Odpowiedzi:

123

Składnia zapytania:

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

Składnia metody:

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

Obie generują to samo zapytanie SQL.

Craig Stuntz
źródło
Dlaczego SelectMany()? Czy to potrzebne? Czy bez niego nie działałoby to dobrze?
Jo Smo
@JoSmo, nie, to zupełnie inne zapytanie.
Craig Stuntz
Dziękuję za wyjaśnienie mi tego. Chciałem się tylko upewnić. :)
Jo Smo
1
Czy możesz mi powiedzieć, dlaczego jest inaczej w przypadku SelectMany? Nie rozumiem. Robię to bez SelectMany, ale robi się bardzo wolno, ponieważ mam ponad 20 milionów płyt. Wypróbowałem odpowiedź od Yang Zhanga i działa świetnie, chciałem tylko wiedzieć, co robi SelectMany.
mikesoft
1
@AustinFelipe Bez wywołania SelectMany zapytanie zwróciłoby liczbę wierszy w MyContainer o identyfikatorze równym „1”. Wywołanie SelectMany zwraca wszystkie wiersze w MyTable, które należą do poprzedniego wyniku zapytania (czyli wyniku MyContainer.Where(o => o.ID == '1'))
sbecker
49

Myślę, że chcesz czegoś takiego

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

(zredagowane w celu odzwierciedlenia komentarzy)

Kevin
źródło
1
Nie, potrzebuje liczby jednostek w MyTable, do których odwołuje się jedna jednostka o ID = 1 w MyContainer
Craig Stuntz
3
Nawiasem mówiąc, jeśli t.ID jest PK, to liczenie w powyższym kodzie zawsze będzie wynosić 1. :)
Craig Stuntz
2
@Craig, masz rację, powinienem był użyć t.ForeignTable.ID. Zaktualizowano.
Kevin
1
Cóż, to jest krótkie i proste. Mój wybór to: var count = context.MyTable.Count(t => t.MyContainer.ID == '1'); nie długie i brzydkie: var count = (from o in context.MyContainer where o.ID == '1' from t in o.MyTable select t).Count(); Ale to zależy od stylu kodowania ...
CL
upewnij się, że dodałeś „using System.Linq”, bo to nie zadziała
CountMurphy
17

Jak rozumiem, wybrana odpowiedź nadal ładuje wszystkie powiązane testy. Według tego bloga msdn jest lepszy sposób.

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

konkretnie

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
źródło
4
Nie ma potrzeby składania dodatkowych Find(1)wniosków. Po prostu stwórz jednostkę i dołącz do kontekstu:var princess = new PrincessEntity{ Id = 1 }; context.Princesses.Attach(princess);
tenbit
13

To jest mój kod:

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

Upewnij się, że zmienna jest zdefiniowana jako IQueryable, a następnie gdy użyjesz metody Count (), EF wykona coś podobnego

select count(*) from ...

W przeciwnym razie, jeśli rekordy są zdefiniowane jako IEnumerable, wygenerowany plik sql będzie sprawdzał całą tabelę i policzy zwracane wiersze.

Yang Zhang
źródło
10

Cóż, nawet SELECT COUNT(*) FROM Table będzie dość nieefektywne, szczególnie w przypadku dużych tabel, ponieważ SQL Server naprawdę nie może zrobić nic poza pełnym skanowaniem tabeli (skanowanie indeksu klastrowego).

Czasami wystarczy znać przybliżoną liczbę wierszy z bazy danych, a w takim przypadku wystarczy taka instrukcja:

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

Spowoduje to sprawdzenie dynamicznego widoku zarządzania i wyodrębnienie z niego liczby wierszy i rozmiaru tabeli dla określonej tabeli. Czyni to poprzez zsumowanie wpisów dla stosu (id_indeksu = 0) lub indeksu klastrowego (id_indeksu = 1).

Jest szybki, łatwy w użyciu, ale nie gwarantuje 100% dokładności ani aktualności. Jednak w wielu przypadkach jest to „wystarczająco dobre” (i znacznie mniej obciąża serwer).

Może to też zadziała w twoim przypadku? Oczywiście, aby użyć go w EF, musiałbyś zawrzeć to w przechowywanym procencie lub użyć prostego wywołania „Execute SQL query”.

Marc

marc_s
źródło
1
Nie będzie to pełny skan tabeli ze względu na odniesienie FK w GDZIE. Zostaną zeskanowane tylko dane mistrza. Problem z wydajnością wynikał z ładowania danych obiektu blob, a nie liczby rekordów. Zakładając, że zazwyczaj nie ma dziesiątek tysięcy + szczegółowych rekordów na płytę główną, nie „optymalizowałbym” czegoś, co w rzeczywistości nie jest wolne.
Craig Stuntz
OK, tak, w takim przypadku wybierzesz tylko podzbiór - to powinno wystarczyć. Jeśli chodzi o dane blobów - odniosłem wrażenie, że można ustawić "odroczone ładowanie" w dowolnej kolumnie w dowolnej z tabel EF, aby uniknąć ich ładowania, więc może to pomóc.
marc_s
Czy istnieje sposób używania tego SQL z EntityFramework? W każdym razie w tym przypadku potrzebowałem tylko wiedzieć, że są pasujące wiersze, ale celowo zadałem pytanie bardziej ogólnie.
NVRAM
4

Użyj metody ExecuteStoreQuery kontekstu jednostki. Pozwala to uniknąć pobierania całego zestawu wyników i deserializacji do obiektów w celu wykonania prostej liczby wierszy.

   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
źródło
6
Jeśli napiszesz, int count = context.MyTable.Count(m => m.MyContainerID == '1')wygenerowany kod SQL będzie dokładnie przypominał to, co robisz, ale kod jest znacznie ładniejszy. Żadne jednostki nie są ładowane do pamięci jako takie. Jeśli chcesz, wypróbuj go w LINQPad - pokaże ci SQL używany pod okładkami.
Drew Noakes
In-line SQL. . nie moja ulubiona rzecz.
Duanne
3

Myślę, że to powinno działać ...

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
źródło
W tym kierunku również poszedłem na początku, ale rozumiem, że jeśli nie dodasz go ręcznie, m będzie miał właściwość MyContainer, ale nie będzie MyContainerId. Dlatego to, co chcesz zbadać, to m.MyContainer.ID.
Kevin
Jeśli MyContainer jest rodzicem, a MyTable są dziećmi w związku, to musieliście ustanowić tę relację za pomocą jakiegoś obcego klucza, nie jestem pewien, jak inaczej można by wiedzieć, które jednostki MyTable są powiązane z bytem MyContainer ... Ale może ja
przyjął