Как подсчитать строки в 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();

Но это крайне неэффективно. Есть способ попроще?


РЕДАКТИРОВАТЬ: Спасибо всем. Я переместил БД из частного прикрепленного, чтобы я мог запустить профилирование; это помогает, но вызывает недоумение, которого я не ожидал.

И мои реальные данные немного глубже, я буду использовать Грузовики проведения Поддонов из дел по пунктам - и я не хочу, чтобы грузовик , чтобы покинуть страну , если не существует , по крайней мере один Пункт в нем.

Мои попытки показаны ниже. Я не понимаю, что 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, отношения «грузовик-поддон» и «поддон-ящик» являются отношениями «многие-ко-многим», хотя я не думаю, что это имеет значение. Мои настоящие объекты нематериальны, и их сложнее описать, поэтому я изменил имена. ]

NVRAM
источник
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()? Это нужно? Разве без него он не работал бы нормально?
Jo Smo
@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
49

Я думаю ты хочешь что-то вроде

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

(отредактировано, чтобы отразить комментарии)

Кевин
источник
1
Нет, ему нужно количество сущностей в MyTable, на которые ссылается одна сущность с ID = 1 в MyContainer
Крейг
3
Между прочим, если t.ID является PK, то счет в приведенном выше коде всегда будет 1. :)
Крейг Стунц
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
убедитесь, что вы включили "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();
}
Quickhorn
источник
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, вам придется заключить это в сохраненную процедуру или использовать прямой вызов «Выполнить SQL-запрос».

Марк

marc_s
источник
1
Это не будет полное сканирование таблицы из-за ссылки FK в WHERE. Будут сканироваться только детали мастера. Проблема с производительностью, с которой он столкнулся, заключалась в загрузке данных blob, а не в количестве записей. Предполагая, что обычно не бывает десятков тысяч подробных записей на основную запись, я бы не стал «оптимизировать» что-то, что на самом деле не является медленным.
Craig Stuntz
Хорошо, да, в этом случае вы выберете только подмножество - это должно быть хорошо. Что касается данных blob - у меня создалось впечатление, что вы можете установить «отложенную загрузку» для любого столбца в любой из ваших таблиц 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