Comment compter les lignes dans EntityFramework sans charger le contenu?

109

J'essaie de déterminer comment compter les lignes correspondantes sur une table en utilisant EntityFramework.

Le problème est que chaque ligne peut contenir plusieurs mégaoctets de données (dans un champ binaire). Bien sûr, le SQL serait quelque chose comme ceci:

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

Je pourrais charger toutes les lignes, puis trouver le compte avec:

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

Mais c'est tout à fait inefficace. Existe-t-il un moyen plus simple?


EDIT: Merci à tous. J'ai déplacé la base de données d'un attaché privé afin que je puisse exécuter le profilage; cela aide mais provoque des confusions auxquelles je ne m'attendais pas.

Et mes vraies données sont un peu plus profondes, j'utiliserai des camions transportant des palettes de caisses d' articles - et je ne veux pas que le camion parte à moins qu'il n'y ait au moins un article dedans.

Mes tentatives sont illustrées ci-dessous. La partie que je n'obtiens pas est que CASE_2 n'accède jamais au serveur de base de données (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

Et le SQL résultant de CASE_1 est acheminé via sp_executesql , mais:

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

[ Je n'ai pas vraiment de camions, de chauffeurs, de palettes, de caisses ou d'articles; comme vous pouvez le voir à partir du SQL, les relations camion-palette et palette-caisse sont plusieurs-à-plusieurs - bien que je ne pense pas que cela compte. Mes objets réels sont intangibles et plus difficiles à décrire, alors j'ai changé les noms. ]

NVRAM
la source
1
comment avez-vous résolu le problème de chargement des palettes?
Sherlock

Réponses:

123

Syntaxe de la requête:

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

Syntaxe de la méthode:

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

Les deux génèrent la même requête SQL.

Craig Stuntz
la source
Pourquoi le SelectMany()? Est-ce nécessaire? Cela ne fonctionnerait-il pas correctement sans cela?
Jo Smo du
@JoSmo, non, c'est une requête totalement différente.
Craig Stuntz
Merci d'avoir clarifié cela pour moi. Je voulais juste être sûr. :)
Jo Smo
1
Pouvez-vous me dire pourquoi est-ce différent avec le SelectMany? Je ne comprends pas. Je le fais sans SelectMany mais ça devient vraiment lent car j'ai plus de 20 millions de disques. J'ai essayé la réponse de Yang Zhang et fonctionne très bien, je voulais juste savoir ce que fait SelectMany.
mikesoft
1
@AustinFelipe Sans l'appel à SelectMany, la requête renverrait le nombre de lignes dans MyContainer avec l'ID égal à «1». L'appel SelectMany renvoie toutes les lignes de MyTable qui appartiennent au résultat précédent de la requête (c'est-à-dire le résultat de MyContainer.Where(o => o.ID == '1'))
sbecker
48

Je pense que tu veux quelque chose comme

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

(modifié pour refléter les commentaires)

Kevin
la source
1
Non, il a besoin du nombre d'entités dans MyTable référencées par la seule entité avec ID = 1 dans MyContainer
Craig Stuntz
3
Incidemment, si le t.ID est un PK, le compte dans le code ci-dessus sera toujours 1. :)
Craig Stuntz
2
@Craig, vous avez raison, j'aurais dû utiliser t.ForeignTable.ID. Actualisé.
Kevin
1
Eh bien, c'est court et simple. Mon choix est: var count = context.MyTable.Count(t => t.MyContainer.ID == '1'); pas long et moche: var count = (from o in context.MyContainer where o.ID == '1' from t in o.MyTable select t).Count(); mais cela dépend du style de codage ...
CL
assurez-vous d'inclure "using System.Linq", sinon cela ne fonctionnera pas
CountMurphy
17

Si je comprends bien, la réponse sélectionnée charge toujours tous les tests associés. Selon ce blog msdn, il existe un meilleur moyen.

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

Plus précisément

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
la source
4
Il n'est pas nécessaire de faire une Find(1)demande supplémentaire . Créez simplement l'entité et attachez-la au contexte:var princess = new PrincessEntity{ Id = 1 }; context.Princesses.Attach(princess);
tenbits
13

Voici mon code:

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

Assurez-vous que la variable est définie comme IQueryable, puis lorsque vous utilisez la méthode Count (), EF exécutera quelque chose comme

select count(*) from ...

Sinon, si les enregistrements sont définis comme IEnumerable, le sql généré interrogera la table entière et comptera les lignes renvoyées.

Yang Zhang
la source
10

Eh bien, même le SELECT COUNT(*) FROM Tablesera assez inefficace, en particulier sur les grandes tables, car SQL Server ne peut vraiment rien faire d'autre que faire une analyse complète de la table (analyse d'index cluster).

Parfois, il est assez bon de connaître un nombre approximatif de lignes de la base de données, et dans un tel cas, une instruction comme celle-ci peut suffire:

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

Cela inspectera la vue de gestion dynamique et en extraira le nombre de lignes et la taille de la table, étant donné une table spécifique. Il le fait en additionnant les entrées pour le tas (index_id = 0) ou l'index cluster (index_id = 1).

C'est rapide, c'est facile à utiliser, mais il n'est pas garanti d'être précis ou à jour à 100%. Mais dans de nombreux cas, cela est "assez bon" (et met beaucoup moins de charge sur le serveur).

Peut-être que cela fonctionnerait pour vous aussi? Bien sûr, pour l'utiliser dans EF, vous devez encapsuler cela dans un processus stocké ou utiliser un simple appel "Execute SQL query".

Marc

marc_s
la source
1
Ce ne sera pas une analyse complète de la table en raison de la référence FK dans le WHERE. Seuls les détails du maître seront analysés. Le problème de performances qu'il rencontrait provenait du chargement des données blob, pas du nombre d'enregistrements. En supposant qu'il n'y a généralement pas des dizaines de milliers d'enregistrements détaillés par enregistrement maître, je n'optimiserais pas quelque chose qui n'est pas vraiment lent.
Craig Stuntz
OK, oui, dans ce cas, vous ne sélectionnerez qu'un sous-ensemble - cela devrait être bien. En ce qui concerne les données blob, j'avais l'impression que vous pouviez définir un "chargement différé" sur n'importe quelle colonne de l'une de vos tables EF pour éviter de le charger, ce qui pourrait aider.
marc_s
Existe-t-il un moyen d'utiliser ce SQL avec EntityFramework? Quoi qu'il en soit, dans ce cas, je n'avais besoin que de savoir qu'il y avait des lignes correspondantes, mais j'ai intentionnellement posé la question plus généralement.
NVRAM
4

Utilisez la méthode ExecuteStoreQuery du contexte d'entité. Cela évite de télécharger tout le jeu de résultats et de désérialiser en objets pour effectuer un simple comptage de lignes.

   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
la source
6
Si vous écrivez, int count = context.MyTable.Count(m => m.MyContainerID == '1')le SQL généré ressemblera exactement à ce que vous faites, mais le code est beaucoup plus agréable. Aucune entité n'est chargée en mémoire en tant que telle. Essayez-le dans LINQPad si vous le souhaitez - il vous montrera le SQL utilisé sous les couvertures.
Drew Noakes
SQL en ligne. . pas ma chose préférée.
Duanne
3

Je pense que cela devrait fonctionner ...

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
la source
C'est la direction que j'ai prise au début aussi, mais je crois comprendre qu'à moins que vous ne l'avez ajouté manuellement, m aura une propriété MyContainer mais pas MyContainerId. Par conséquent, ce que vous voulez examiner est m.MyContainer.ID.
Kevin
Si MyContainer est le parent et MyTable sont les enfants dans la relation, vous deviez établir cette relation avec une clé étrangère, je ne sais pas comment vous sauriez quelles entités MyTable sont associées à une entité MyContainer ... Mais peut-être que je fait une hypothèse sur la structure ...
bytebender