Como COUNT linhas dentro de EntityFramework sem carregar conteúdo?

109

Estou tentando determinar como contar as linhas correspondentes em uma tabela usando o EntityFramework.

O problema é que cada linha pode ter muitos megabytes de dados (em um campo Binário). Claro que o SQL seria algo assim:

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

Eu poderia carregar todas as linhas e , em seguida, encontrar o conde com:

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

Mas isso é extremamente ineficiente. Existe uma maneira mais simples?


EDIT: Obrigado a todos. Mudei o banco de dados de um anexo privado para poder executar a criação de perfil; isso ajuda, mas causa confusões que eu não esperava.

E meus dados reais são um pouco mais profundos, usarei Caminhões carregando paletes de caixas de itens - e não quero que o caminhão saia a menos que haja pelo menos um item nele.

Minhas tentativas são mostradas abaixo. A parte que não entendo é que CASE_2 nunca acessa o servidor de banco de dados (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

E o SQL resultante de CASE_1 é canalizado por meio de sp_executesql , mas:

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

[ Eu realmente não tenho caminhões, motoristas, paletes, caixas ou itens; como você pode ver no SQL, os relacionamentos Truck-Pallet e Pallet-Case são muitos para muitos - embora eu não ache que isso importe. Meus objetos reais são intangíveis e mais difíceis de descrever, então mudei os nomes. ]

NVRAM
fonte
1
como você resolveu o problema de carregamento de paletes?
Sherlock

Respostas:

123

Sintaxe de consulta:

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

Sintaxe do método:

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

Ambos geram a mesma consulta SQL.

Craig Stuntz
fonte
Por que SelectMany()? É necessário? Não funcionaria bem sem ele?
Jo Smo
@JoSmo, não, essa é uma consulta totalmente diferente.
Craig Stuntz
Obrigado por esclarecer isso para mim. Só queria ter certeza. :)
Jo Smo
1
Você pode me dizer por que é diferente com o SelectMany? Eu não entendo. Eu faço isso sem SelectMany, mas fica muito lento porque tenho mais de 20 milhões de registros. Tentei a resposta de Yang Zhang e funciona muito bem, só queria saber o que o SelectMany faz.
mikesoft de
1
@AustinFelipe Sem a chamada para SelectMany, a consulta retornaria o número de linhas em MyContainer com o ID igual a '1'. A chamada SelectMany retorna todas as linhas em MyTable que pertencem ao resultado anterior da consulta (significando o resultado de MyContainer.Where(o => o.ID == '1'))
sbecker
48

Eu acho que você quer algo como

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

(editado para refletir os comentários)

Kevin
fonte
1
Não, ele precisa da contagem das entidades em MyTable referenciadas por uma entidade com ID = 1 em MyContainer
Craig Stuntz
3
A propósito, se t.ID for um PK, a contagem no código acima será sempre 1. :)
Craig Stuntz
2
@Craig, você está certo, eu deveria ter usado t.ForeignTable.ID. Atualizada.
Kevin de
1
Bem, isso é curto e simples. Minha escolha é: var count = context.MyTable.Count(t => t.MyContainer.ID == '1'); não longa e feia: var count = (from o in context.MyContainer where o.ID == '1' from t in o.MyTable select t).Count(); Mas depende do estilo de codificação ...
CL
certifique-se de incluir "using System.Linq", ou isso não funcionará
CountMurphy
16

Pelo que entendi, a resposta selecionada ainda carrega todos os testes relacionados. De acordo com este blog do msdn, existe uma maneira melhor.

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

Especificamente

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
fonte
3
Não há necessidade de fazer Find(1)pedido extra . Basta criar a entidade e anexar ao contexto:var princess = new PrincessEntity{ Id = 1 }; context.Princesses.Attach(princess);
tenbits
13

Este é o meu código:

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

Certifique-se de que a variável está definida como IQueryable, então quando você usa o método Count (), EF irá executar algo como

select count(*) from ...

Caso contrário, se os registros forem definidos como IEnumerable, o sql gerado consultará a tabela inteira e contará as linhas retornadas.

Yang Zhang
fonte
10

Bem, mesmo o SELECT COUNT(*) FROM Tableserá bastante ineficiente, especialmente em tabelas grandes, uma vez que o SQL Server realmente não pode fazer nada além de fazer uma verificação completa da tabela (verificação de índice de cluster).

Às vezes, é bom o suficiente saber um número aproximado de linhas do banco de dados e, nesse caso, uma declaração como esta pode ser suficiente:

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

Isso inspecionará a visualização de gerenciamento dinâmico e extrairá o número de linhas e o tamanho da tabela dela, dada uma tabela específica. Ele faz isso somando as entradas para o heap (index_id = 0) ou o índice clusterizado (index_id = 1).

É rápido e fácil de usar, mas não é garantido que seja 100% preciso ou atualizado. Mas, em muitos casos, isso é "bom o suficiente" (e sobrecarrega muito menos o servidor).

Talvez isso funcione para você também? Claro, para usá-lo no EF, você teria que embrulhar isso em um procedimento armazenado ou usar uma chamada direta "Execute SQL query".

Marc

marc_s
fonte
1
Não será uma varredura completa da tabela devido à referência FK no WHERE. Apenas os detalhes do mestre serão verificados. O problema de desempenho que ele estava enfrentando era o carregamento de dados de blob, não a contagem de registros. Presumindo que não haja tipicamente dezenas de milhares de registros de detalhes por registro mestre, eu não "otimizaria" algo que não seja realmente lento.
Craig Stuntz
OK, sim, nesse caso, você só selecionará um subconjunto - isso deve estar bem. Quanto aos dados de blob - tive a impressão de que você poderia definir um "carregamento adiado" em qualquer coluna de qualquer uma de suas tabelas EF para evitar carregá-lo, então isso pode ajudar.
marc_s
Existe uma maneira de usar esse SQL com o EntityFramework? De qualquer forma, neste caso, eu só precisava saber que havia linhas correspondentes, mas intencionalmente fiz a pergunta de forma mais geral.
NVRAM de
4

Use o método ExecuteStoreQuery do contexto da entidade. Isso evita o download de todo o conjunto de resultados e a desserialização em objetos para fazer uma contagem de linhas simples.

   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>();
    }
ganso
fonte
6
Se você escrever int count = context.MyTable.Count(m => m.MyContainerID == '1'), o SQL gerado se parecerá exatamente com o que você está fazendo, mas o código é muito melhor. Nenhuma entidade é carregada na memória como tal. Experimente no LINQPad se desejar - ele mostrará o SQL usado nos bastidores.
Drew Noakes em
SQL em linha. . não é minha coisa favorita.
Duanne,
3

Eu acho que isso deve funcionar...

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
fonte
Esta é a direção que eu segui no início também, mas é meu entendimento que, a menos que você o tenha adicionado manualmente, m terá uma propriedade MyContainer, mas nenhum MyContainerId. Portanto, o que você deseja examinar é m.MyContainer.ID.
Kevin
Se MyContainer é o pai e MyTable são os filhos no relacionamento, você teria que estabelecer esse relacionamento com alguma chave estrangeira, não tenho certeza de como você saberia quais entidades MyTable estão associadas a uma entidade MyContainer ... Mas talvez eu fez uma suposição sobre a estrutura ...
bytebender