Як COUNT рядків у EntityFramework без завантаження вмісту?


109

Я намагаюся визначити, як рахувати відповідні рядки на таблиці за допомогою EntityFramework.

Проблема полягає в тому, що кожен рядок може мати багато мегабайт даних (у двійковому полі). Звичайно, SQL був би приблизно таким:

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

Я можу завантажити всі рядки, а потім знайти графа за допомогою:

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

Але це вкрай неефективно. Чи є простіший спосіб?


EDIT: Спасибі, всі. Я перемістив БД із приватного доданого файлу, щоб я міг запускати профілювання; це допомагає, але викликає сум'яття, яких я не очікував.

І мої реальні дані трохи глибше, я буду використовувати Вантажівки проведення Піддонів з справ по пунктам - і я не хочу, щоб вантажівка , щоб покинути країну , якщо не існує , по крайней мере один Пункт в ньому.

Мої спроби показані нижче. Частина, яку я не отримую, - це те, що CASE_2 ніколи не отримує доступ до сервера БД (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, відносини вантажівки-піддонів та піддонів-піддонів багато-на-багато, хоча я не думаю, що це має значення. Мої реальні об’єкти нематеріальні та важче описати, тому я змінив назви. ]


1
як ви вирішили проблему з завантаженням піддону?
Шерлок

Відповіді:


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()? Це потрібно? Не вийшло б без нього правильно?
Джо Смол

@JoSmo, ні, це зовсім інший запит.
Craig Stuntz

Дякую, що очистили це за мене. Просто хотілося бути впевненим. :)
Jo Smo

1
Чи можете ви сказати мені, чому з SelectMany все по-іншому? Я не розумію. Я роблю це без SelectMany, але він стає дуже повільним, оскільки у мене є понад 20 мільйонів записів. Я спробував відповідь Ян Чжан і працює чудово, просто хотів дізнатися, що робить SelectMany.
mikesoft

1
@AustinFelipe Без виклику SelectMany запит повертає кількість рядків у MyContainer з ідентифікатором, рівним '1'. Виклик SelectMany повертає всі рядки в MyTable, які належать до попереднього результату запиту (мається на увазі результат MyContainer.Where(o => o.ID == '1'))
sbecker

48

Я думаю, ти хочеш чогось подібного

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

(відредаговано для відображення коментарів)


1
Ні, йому потрібна кількість сутностей у MyTable, на яку посилається одна сутність з ідентифікатором = 1 в MyContainer
Крейг Стунц

3
До речі, якщо t.ID - ПК, то кількість коду в коді вище завжди буде 1. :)
Craig Stuntz

2
@Craig, ти маєш рацію, я повинен був використовувати t.ForeignTable.ID. Оновлено.
Кевін

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

переконайтеся, що ви включили "за допомогою System.Linq", або цю роботу не
будете

16

Як я розумію, обрана відповідь все ще завантажує всі відповідні тести. Відповідно до цього блогу 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();
}

3
Не потрібно робити додаткові 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, вам доведеться загортати це в збережений додаток або використовувати прямий дзвінок "Виконати запит SQL".

Марк


1
Це не буде повним скануванням таблиці через посилання FK в WHERE. Будуть скановані лише деталі майстра. Проблеми щодо продуктивності, які у нього виникали, були з завантаженням даних про краплі, а не з числа записів. Припускаючи, що зазвичай не десятки тисяч + записів деталей на основний запис, я б не "оптимізував" те, що насправді не повільно.
Крейг Штунц

Гаразд, так, у такому випадку ви виберете лише підмножину - це повинно бути добре. Що стосується даних крапки - я мав враження, що ви можете встановити "відкладене завантаження" на будь-який стовпець будь-якої з ваших таблиць 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, який використовується під обкладинками.
Дрю Ноакс

Вбудований 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.
Кевін

Якщо MyContainer є батьком, а MyTable є дітьми у стосунках, то вам довелося встановити цей зв'язок з яким-небудь іноземним ключем, я не впевнений, як інакше ви могли б знати, які об’єкти MyTable, де асоційовано з сутністю MyContainer ... Але, можливо, я зробив припущення про структуру ...
bytebender
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.