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. ]


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.


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)


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

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.


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


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

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

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ął
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.