4
votes

La requête Entity Framework Core 3.0 provoque «SqlException: 'Execution Timeout Expired'» et tempdb devient saturé. Fonctionne avec EF Core 2.2.6

J'exécute une requête assez simple dans Microsoft Entity Framework Core 3.0 qui ressemble à ceci:

SELECT [x.Interests].[Id], [x.Interests].[Name], [x.Interests].[ProfileId]
FROM [Interests] AS [x.Interests]
INNER JOIN (
    SELECT TOP(1) [x0].[Id]
    FROM [Profiles] AS [x0]
    WHERE [x0].[SiteId] = '123'
    ORDER BY [x0].[Id]
) AS [t] ON [x.Interests].[ProfileId] = [t].[Id]
ORDER BY [t].[Id]

Cela a bien fonctionné avec EF Core 2.2.6, mais lors de la mise à niveau vers EF Core 3.0, cette requête s'exécute instantanément pour 721 profils, mais pour au moins un profil, la requête expire:

Microsoft.Data.SqlClient.SqlException: 'Délai d'exécution expiré.
Le délai d'attente s'est écoulé avant la fin de l'opération ou le serveur ne répond pas. '

J'ai ensuite enregistré la requête réelle envoyée au serveur de base de données:

https://stackoverflow.com/a/58348159/3850405

SELECT [t].[Id], [t].[Age], [t].[City], [t].[Country], [t].[County], [t].[DeactivatedAccount], [t].[Gender], [t].[HasPictures], [t].[LastLogin], [t].[MemberSince], [t].[PresentationUpdated], [t].[ProfileName], [t].[ProfilePictureUrl], [t].[ProfileText], [t].[SiteId], [t].[VisitorsCount], [i].[Id], [i].[Name], [i].[ProfileId], [p0].[Id], [p0].[Description], [p0].[Name], [p0].[ProfileId], [n].[Id], [n].[Name], [n].[NetworkId], [n].[ProfileId], [p1].[Id], [p1].[Name], [p1].[ProfileId]
FROM (
    SELECT TOP(2) [p].[Id], [p].[Age], [p].[City], [p].[Country], [p].[County], [p].[DeactivatedAccount], [p].[Gender], [p].[HasPictures], [p].[LastLogin], [p].[MemberSince], [p].[PresentationUpdated], [p].[ProfileName], [p].[ProfilePictureUrl], [p].[ProfileText], [p].[SiteId], [p].[VisitorsCount]
    FROM [Profiles] AS [p]
    WHERE ([p].[SiteId] = '123') AND '123' IS NOT NULL
) AS [t]
LEFT JOIN [Interests] AS [i] ON [t].[Id] = [i].[ProfileId]
LEFT JOIN [Pets] AS [p0] ON [t].[Id] = [p0].[ProfileId]
LEFT JOIN [Networks] AS [n] ON [t].[Id] = [n].[ProfileId]
LEFT JOIN [PersonalityTraits] AS [p1] ON [t].[Id] = [p1].[ProfileId]
ORDER BY [t].[Id], [i].[Id], [p0].[Id], [n].[Id], [p1].[Id]

J'ai ensuite essayé d'exécuter le SQL réel dans SSMS et me suis retrouvé avec l'erreur suivante:

Msg 1105, niveau 17, état 2, ligne 1
Impossible d'allouer de l'espace pour l'objet «stockage d'exécution temporaire dbo.SORT: 140737692565504» dans la base de données «tempdb» car le groupe de fichiers «PRIMARY» est plein. Créez de l'espace disque en supprimant les fichiers inutiles, en supprimant des objets dans le groupe de fichiers, en ajoutant des fichiers supplémentaires au groupe de fichiers ou en activant la croissance automatique pour les fichiers existants dans le groupe de fichiers.

Mon tempdb avait maintenant complètement rempli le disque de la base de données. J'ai essayé 10 autres identifiants et la même requête s'exécute instantanément.

J'ai essayé de réduire à nouveau le tempdb avec la commande DBCC SHRINKDATABASE(tempdb, 10); et cela a bien fonctionné. Cependant, lorsque j'ai essayé d'exécuter à nouveau les requêtes, la même chose s'est produite. Si je saute les tables, tout se passe bien. Quel pourrait être le problème ici et comment le résoudre? S'agit-il d'un bogue connu dans EF Core 3.0? En regardant la requête dans EF Core 2.2.6, elle effectue des sélections individuelles comme celle-ci pour toutes les tables:

var dbProfile = db.Profiles.Where(x => x.SiteId == Int32.Parse(id))
    .Include(x => x.Interests)
    .Include(x => x.Pets)
    .Include(x => x.Networks)
    .Include(x => x.PersonalityTraits)
    .SingleOrDefault();


1 commentaires

Sauf si la quantité de données est énorme, cette requête devrait s'exécuter sans problème. tempdb explosion de tempdb indique généralement que les tables ne sont pas indexées correctement. Avez-vous des index sur tous les champs de clé étrangère et SiteId ? En outre, l'exécution de statistiques peut faire une énorme différence.


3 Réponses :


4
votes

Il s'agit d'un changement de rupture documenté dans EF Core 3: le chargement hâtif des entités associées se produit désormais dans une seule requête

Le nouveau comportement est similaire à la génération de requêtes dans EF6, où plusieurs inclusions peuvent créer des requêtes très volumineuses et coûteuses. Ces requêtes peuvent également échouer en raison de délais d'expiration, des coûts de génération du plan de requête ou de l'épuisement des ressources d'exécution des requêtes.

Ainsi, tout comme dans EF6, vous devez vous abstenir d'inclure plusieurs chemins d'inclusion d'entités non liés, car ils créent des requêtes très coûteuses.

Au lieu de cela, vous pouvez utiliser le chargement différé, ou charger explicitement des parties du graphique d'entité dans des requêtes distinctes et laisser le suivi des modifications corriger les propriétés de navigation.

EF 5 a ajouté une option pour désactiver la génération d'une seule grande requête appelée Split Queries .


0 commentaires

0
votes

Ajout à la réponse @ DavidBrowne-Microsoft.

Supposons que nous ayons les entités et les navigations suivantes dans le modèle

// Customer Include Address
SELECT [c].[Id], [c].[CustomerName], [a].[Id], [a].[City], [a].[CustomerId]
FROM [Customers] AS [c]
LEFT JOIN [Address] AS [a] ON [c].[Id] = [a].[CustomerId]
WHERE ([c].[CustomerName] = N'John') AND [c].[CustomerName] IS NOT NULL

// Order Include Order discount
SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate], [o0].[Id], [o0].[Discount], [o0].[OrderId]
FROM [Customers] AS [c]
INNER JOIN [Order] AS [o] ON [c].[Id] = [o].[CustomerId]
LEFT JOIN [OrderDiscount] AS [o0] ON [o].[Id] = [o0].[OrderId]
WHERE ([c].[CustomerName] = N'John') AND [c].[CustomerName] IS NOT NULL

// OrderDetails
SELECT [o0].[Id], [o0].[OrderId], [o0].[ProductName]
FROM [Customers] AS [c]
INNER JOIN [Order] AS [o] ON [c].[Id] = [o].[CustomerId]
INNER JOIN [OrderDetail] AS [o0] ON [o].[Id] = [o0].[OrderId]
WHERE ([c].[CustomerName] = N'John') AND [c].[CustomerName] IS NOT NULL

Vous devrez réécrire uniquement pour les navigations de collection, les navigations de référence peuvent faire partie de la même requête.

var baseQuery = db.Customers.Include(c => c.Address).Where(c => c.CustomerName == "John");
var result = baseQuery.ToList(); // Or async method, If doing FirstOrDefault, add Take(1) to base query
baseQuery.Include(c => c.Orders).ThenInclude(o => o.OrderDiscount).SelectMany(c => c.Orders).Load();
baseQuery.SelectMany(c => c.Orders).SelectMany(o => o.OrderDetails).Load();

Cela générera 3 requêtes sur le serveur. Ce serait un SQL légèrement plus optimisé que ce que EF Core 2.2 a généré. Et StateManager réparera les navigations. Cela évite également de dupliquer les enregistrements provenant du serveur vers le client.

SQL généré:

Customer
Customer.Address (reference nav)
Customer.Orders (collection nav)
Order.OrderDetails (collection nav)
Order.OrderDiscount (reference nav)

Edit: Si vous n'avez pas de navigation CLR à utiliser dans SelectMany, vous pouvez utiliser EF.Property pour référencer la navigation de collection.

Source: https://github.com/aspnet/EntityFrameworkCore/issues/18022#issuecomment-537219137


0 commentaires

1
votes

Vous devez changer votre code comme celui-ci, cela générera plus de requêtes et évitera le temps mort

db.Profiles.AsTracking().Where(........

rappelez-vous que la requête doit être exécutée en mode Tracking. s'il ne charge pas les fils, vous pouvez ajouter asTracking comme ceci:

var dbProfile = db.Profiles.SingleOrDefault(x => x.SiteId == Int32.Parse(id));
dbProfile.Include(x => x.Interests).Load();
dbProfile.Include(x => x.Pets).Load();
dbProfile.Include(x => x.Networks).Load();
dbProfile.Include(x => x.PersonalityTraits).Load();


0 commentaires