web-dev-qa-db-fra.com

Les performances des requêtes Entity Framework diffèrent extrêmement avec l'exécution SQL brute

J'ai une question sur les performances d'exécution des requêtes Entity Framework.

Schéma :

J'ai une structure de table comme celle-ci:

CREATE TABLE [dbo].[DataLogger]
(
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [ProjectID] [bigint] NULL,
    CONSTRAINT [PrimaryKey1] PRIMARY KEY CLUSTERED ( [ID] ASC )
)

CREATE TABLE [dbo].[DCDistributionBox]
(
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [DataLoggerID] [bigint] NOT NULL,
    CONSTRAINT [PrimaryKey2] PRIMARY KEY CLUSTERED ( [ID] ASC )
)

ALTER TABLE [dbo].[DCDistributionBox]
    ADD CONSTRAINT [FK_DCDistributionBox_DataLogger] 
    FOREIGN KEY([DataLoggerID]) REFERENCES [dbo].[DataLogger] ([ID])

CREATE TABLE [dbo].[DCString] 
(
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [DCDistributionBoxID] [bigint] NOT NULL,
    [CurrentMPP] [decimal](18, 2) NULL,
    CONSTRAINT [PrimaryKey3] PRIMARY KEY CLUSTERED ( [ID] ASC )
)

ALTER TABLE [dbo].[DCString]
    ADD CONSTRAINT [FK_DCString_DCDistributionBox] 
    FOREIGN KEY([DCDistributionBoxID]) REFERENCES [dbo].[DCDistributionBox] ([ID])

CREATE TABLE [dbo].[StringData]
(
    [DCStringID] [bigint] NOT NULL,
    [TimeStamp] [datetime] NOT NULL,
    [DCCurrent] [decimal](18, 2) NULL,
    CONSTRAINT [PrimaryKey4] PRIMARY KEY CLUSTERED ( [TimeStamp] DESC, [DCStringID] ASC)
)

CREATE NONCLUSTERED INDEX [TimeStamp_DCCurrent-NonClusteredIndex] 
ON [dbo].[StringData] ([DCStringID] ASC, [TimeStamp] ASC)
INCLUDE ([DCCurrent])

Il existe également des index standard sur les clés étrangères (je ne veux pas tous les énumérer pour des raisons d'espace).

La table [StringData] A les statistiques de stockage suivantes:

  • Espace de données: 26901,86 Mo
  • Nombre de lignes: 131 827 749
  • Partitionné: vrai
  • Nombre de partitions: 62

Utilisation :

Je veux maintenant regrouper les données dans la table [StringData] Et faire une agrégation.

J'ai créé une requête Entity Framework (des informations détaillées sur la requête peuvent être trouvées ici ):

var compareData = model.StringDatas
    .AsNoTracking()
    .Where(p => p.DCString.DCDistributionBox.DataLogger.ProjectID == projectID && p.TimeStamp >= fromDate && p.TimeStamp < tillDate)
    .Select(d => new
    {
        TimeStamp = d.TimeStamp,
        DCCurrentMpp = d.DCCurrent / d.DCString.CurrentMPP
    })
    .GroupBy(d => DbFunctions.AddMinutes(DateTime.MinValue, DbFunctions.DiffMinutes(DateTime.MinValue, d.TimeStamp) / minuteInterval * minuteInterval))
    .Select(d => new
    {
        TimeStamp = d.Key,
        DCCurrentMppMin = d.Min(v => v.DCCurrentMpp),
        DCCurrentMppMax = d.Max(v => v.DCCurrentMpp),
        DCCurrentMppAvg = d.Average(v => v.DCCurrentMpp),
        DCCurrentMppStDev = DbFunctions.StandardDeviationP(d.Select(v => v.DCCurrentMpp))
    })
    .ToList();

Le délai d'exécution est exceptionnellement long!?

  • Résultat d'exécution: 92 lignes
  • Temps d'exécution: ~ 16000 ms

Tentatives :

J'ai maintenant jeté un œil à la requête SQL générée par Entity Framework et ressemble à ceci:

DECLARE @p__linq__4 DATETIME = 0;
DECLARE @p__linq__3 DATETIME = 0;
DECLARE @p__linq__5 INT = 15;
DECLARE @p__linq__6 INT = 15;
DECLARE @p__linq__0 BIGINT = 20827;
DECLARE @p__linq__1 DATETIME = '06.02.2016 00:00:00';
DECLARE @p__linq__2 DATETIME = '07.02.2016 00:00:00';

SELECT 
1 AS [C1], 
[GroupBy1].[K1] AS [C2], 
[GroupBy1].[A1] AS [C3], 
[GroupBy1].[A2] AS [C4], 
[GroupBy1].[A3] AS [C5], 
[GroupBy1].[A4] AS [C6]
FROM ( SELECT 
    [Project1].[K1] AS [K1], 
    MIN([Project1].[A1]) AS [A1], 
    MAX([Project1].[A2]) AS [A2], 
    AVG([Project1].[A3]) AS [A3], 
    STDEVP([Project1].[A4]) AS [A4]
    FROM ( SELECT 
        DATEADD (minute, ((DATEDIFF (minute, @p__linq__4, [Project1].[TimeStamp])) / @p__linq__5) * @p__linq__6, @p__linq__3) AS [K1], 
        [Project1].[C1] AS [A1], 
        [Project1].[C1] AS [A2], 
        [Project1].[C1] AS [A3], 
        [Project1].[C1] AS [A4]
        FROM ( SELECT 
            [Extent1].[TimeStamp] AS [TimeStamp], 
            [Extent1].[DCCurrent] / [Extent2].[CurrentMPP] AS [C1]
            FROM    [dbo].[StringData] AS [Extent1]
            INNER JOIN [dbo].[DCString] AS [Extent2] ON [Extent1].[DCStringID] = [Extent2].[ID]
            INNER JOIN [dbo].[DCDistributionBox] AS [Extent3] ON [Extent2].[DCDistributionBoxID] = [Extent3].[ID]
            INNER JOIN [dbo].[DataLogger] AS [Extent4] ON [Extent3].[DataLoggerID] = [Extent4].[ID]
            WHERE (([Extent4].[ProjectID] = @p__linq__0) OR (([Extent4].[ProjectID] IS NULL) AND (@p__linq__0 IS NULL))) AND ([Extent1].[TimeStamp] >= @p__linq__1) AND ([Extent1].[TimeStamp] < @p__linq__2)
        )  AS [Project1]
    )  AS [Project1]
    GROUP BY [K1]
)  AS [GroupBy1]

J'ai copié cette requête SQL dans SSMS sur la même machine, connectée avec la même chaîne de connexion que l'Entity Framework.

Le résultat est une performance très améliorée:

  • Résultat d'exécution: 92 lignes
  • Temps d'exécution: 517 ms

Je fais également un test de fonctionnement en boucle et le résultat est étrange. Le test ressemble à ceci

for (int i = 0; i < 50; i++)
{
    DateTime begin = DateTime.UtcNow;

    [...query...]

    TimeSpan excecutionTimeSpan = DateTime.UtcNow - begin;
    Debug.WriteLine("{0}th run: {1}", i, excecutionTimeSpan.ToString());
}

Le résultat est très différent et semble aléatoire (?):

0th run: 00:00:11.0618580
1th run: 00:00:11.3339467
2th run: 00:00:10.0000676
3th run: 00:00:10.1508140
4th run: 00:00:09.2041939
5th run: 00:00:07.6710321
6th run: 00:00:10.3386312
7th run: 00:00:17.3422765
8th run: 00:00:13.8620557
9th run: 00:00:14.9041528
10th run: 00:00:12.7772906
11th run: 00:00:17.0170235
12th run: 00:00:14.7773750

Question :

Pourquoi l'exécution des requêtes Entity Framework est-elle si lente? Le nombre de lignes résultant est vraiment faible et la requête SQL brute affiche des performances très rapides.

Mise à jour 1 :

Je prends soin que ce ne soit pas un délai de création de MetaContext ou de modèle. Certaines autres requêtes sont exécutées sur la même instance de modèle juste avant avec de bonnes performances.

Mise à jour 2 (liée à la réponse de @ x0007me):

Merci pour cet indice, mais cela peut être éliminé en modifiant les paramètres du modèle comme ceci:

modelContext.Configuration.UseDatabaseNullSemantics = true;

Le SQL généré par EF est maintenant:

SELECT 
1 AS [C1], 
[GroupBy1].[K1] AS [C2], 
[GroupBy1].[A1] AS [C3], 
[GroupBy1].[A2] AS [C4], 
[GroupBy1].[A3] AS [C5], 
[GroupBy1].[A4] AS [C6]
FROM ( SELECT 
    [Project1].[K1] AS [K1], 
    MIN([Project1].[A1]) AS [A1], 
    MAX([Project1].[A2]) AS [A2], 
    AVG([Project1].[A3]) AS [A3], 
    STDEVP([Project1].[A4]) AS [A4]
    FROM ( SELECT 
        DATEADD (minute, ((DATEDIFF (minute, @p__linq__4, [Project1].[TimeStamp])) / @p__linq__5) * @p__linq__6, @p__linq__3) AS [K1], 
        [Project1].[C1] AS [A1], 
        [Project1].[C1] AS [A2], 
        [Project1].[C1] AS [A3], 
        [Project1].[C1] AS [A4]
        FROM ( SELECT 
            [Extent1].[TimeStamp] AS [TimeStamp], 
            [Extent1].[DCCurrent] / [Extent2].[CurrentMPP] AS [C1]
            FROM    [dbo].[StringData] AS [Extent1]
            INNER JOIN [dbo].[DCString] AS [Extent2] ON [Extent1].[DCStringID] = [Extent2].[ID]
            INNER JOIN [dbo].[DCDistributionBox] AS [Extent3] ON [Extent2].[DCDistributionBoxID] = [Extent3].[ID]
            INNER JOIN [dbo].[DataLogger] AS [Extent4] ON [Extent3].[DataLoggerID] = [Extent4].[ID]
            WHERE ([Extent4].[ProjectID] = @p__linq__0) AND ([Extent1].[TimeStamp] >= @p__linq__1) AND ([Extent1].[TimeStamp] < @p__linq__2)
        )  AS [Project1]
    )  AS [Project1]
    GROUP BY [K1]
)  AS [GroupBy1]

Vous pouvez donc voir que le problème que vous avez décrit est maintenant résolu, mais le temps d'exécution ne change pas.

De plus, comme vous pouvez le voir dans le schéma et le temps d'exécution brut, j'ai utilisé une structure optimisée avec un indexeur hautement optimisé.

Mise à jour 3 (liée à la réponse de @Vladimir Baranov):

Je ne vois pas pourquoi cela peut être lié à la mise en cache du plan de requête. Parce que dans le MSDN est clairement décrit que l'EF6 utilise la mise en cache du plan de requête.

Une simple preuve de test que l'énorme différence de temps d'exécution n'est pas liée à la mise en cache du plan de requête (code phseudo):

using(var modelContext = new ModelContext())
{
    modelContext.Query(); //1th run activates caching

    modelContext.Query(); //2th used cached plan
}

Par conséquent, les deux requêtes s'exécutent avec le même temps d'exécution.

Mise à jour 4 (liée à la réponse de @bubi):

J'ai essayé d'exécuter la requête générée par l'EF comme vous l'avez décrite:

int result = model.Database.ExecuteSqlCommand(@"SELECT 
    1 AS [C1], 
    [GroupBy1].[K1] AS [C2], 
    [GroupBy1].[A1] AS [C3], 
    [GroupBy1].[A2] AS [C4], 
    [GroupBy1].[A3] AS [C5], 
    [GroupBy1].[A4] AS [C6]
    FROM ( SELECT 
        [Project1].[K1] AS [K1], 
        MIN([Project1].[A1]) AS [A1], 
        MAX([Project1].[A2]) AS [A2], 
        AVG([Project1].[A3]) AS [A3], 
        STDEVP([Project1].[A4]) AS [A4]
        FROM ( SELECT 
            DATEADD (minute, ((DATEDIFF (minute, 0, [Project1].[TimeStamp])) / @p__linq__5) * @p__linq__6, 0) AS [K1], 
            [Project1].[C1] AS [A1], 
            [Project1].[C1] AS [A2], 
            [Project1].[C1] AS [A3], 
            [Project1].[C1] AS [A4]
            FROM ( SELECT 
                [Extent1].[TimeStamp] AS [TimeStamp], 
                [Extent1].[DCCurrent] / [Extent2].[CurrentMPP] AS [C1]
                FROM    [dbo].[StringData] AS [Extent1]
                INNER JOIN [dbo].[DCString] AS [Extent2] ON [Extent1].[DCStringID] = [Extent2].[ID]
                INNER JOIN [dbo].[DCDistributionBox] AS [Extent3] ON [Extent2].[DCDistributionBoxID] = [Extent3].[ID]
                INNER JOIN [dbo].[DataLogger] AS [Extent4] ON [Extent3].[DataLoggerID] = [Extent4].[ID]
                WHERE ([Extent4].[ProjectID] = @p__linq__0) AND ([Extent1].[TimeStamp] >= @p__linq__1) AND ([Extent1].[TimeStamp] < @p__linq__2)
            )  AS [Project1]
        )  AS [Project1]
        GROUP BY [K1]
    )  AS [GroupBy1]",
    new SqlParameter("p__linq__0", 20827),
    new SqlParameter("p__linq__1", fromDate),
    new SqlParameter("p__linq__2", tillDate),
    new SqlParameter("p__linq__5", 15),
    new SqlParameter("p__linq__6", 15));
  • Résultat de l'exécution: 92
  • Temps d'exécution: ~ 16000 ms

Cela a pris exactement aussi longtemps que la requête EF normale!?

Mise à jour 5 (liée à la réponse de @vittore):

Je crée une arborescence d'appels tracée, peut-être que cela aide:

call tree trace

Mise à jour 6 (liée à la réponse de @usr):

J'ai créé deux showplan XML via SQL Server Profiler.

Exécution rapide (SSMS) .SQLPlan

exécution lente (EF) .SQLPlan

Mise à jour 7 (liée aux commentaires de @VladimirBaranov):

Je lance maintenant un cas de test supplémentaire lié à vos commentaires.

D'abord, j'élimine le temps en prenant des opérations d'ordre en utilisant une nouvelle colonne calculée et un INDEXER correspondant. Cela réduit le retard de performance lié à DATEADD(MINUTE, DATEDIFF(MINUTE, 0, [TimeStamp] ) / 15* 15, 0). Détaillez comment et pourquoi vous pouvez trouver ici .

Le résultat ressemble à ceci:

Requête Pure EntityFramework:

for (int i = 0; i < 3; i++)
{
    DateTime begin = DateTime.UtcNow;
    var result = model.StringDatas
        .AsNoTracking()
        .Where(p => p.DCString.DCDistributionBox.DataLogger.ProjectID == projectID && p.TimeStamp15Minutes >= fromDate && p.TimeStamp15Minutes < tillDate)
        .Select(d => new
        {
            TimeStamp = d.TimeStamp15Minutes,
            DCCurrentMpp = d.DCCurrent / d.DCString.CurrentMPP
        })
        .GroupBy(d => d.TimeStamp)
        .Select(d => new
        {
            TimeStamp = d.Key,
            DCCurrentMppMin = d.Min(v => v.DCCurrentMpp),
            DCCurrentMppMax = d.Max(v => v.DCCurrentMpp),
            DCCurrentMppAvg = d.Average(v => v.DCCurrentMpp),
            DCCurrentMppStDev = DbFunctions.StandardDeviationP(d.Select(v => v.DCCurrentMpp))
        })
        .ToList();

        TimeSpan excecutionTimeSpan = DateTime.UtcNow - begin;
        Debug.WriteLine("{0}th run pure EF: {1}", i, excecutionTimeSpan.ToString());
}

0e exécution EF pur: 00: 00: 12.6460624

1ère exécution EF pur: 00: 00: 11.0258393

2ème exécution EF pur: 00: 00: 08.4171044

J'ai maintenant utilisé le SQL généré par EF comme requête SQL:

for (int i = 0; i < 3; i++)
{
    DateTime begin = DateTime.UtcNow;
    int result = model.Database.ExecuteSqlCommand(@"SELECT 
        1 AS [C1], 
        [GroupBy1].[K1] AS [TimeStamp15Minutes], 
        [GroupBy1].[A1] AS [C2], 
        [GroupBy1].[A2] AS [C3], 
        [GroupBy1].[A3] AS [C4], 
        [GroupBy1].[A4] AS [C5]
        FROM ( SELECT 
            [Project1].[TimeStamp15Minutes] AS [K1], 
            MIN([Project1].[C1]) AS [A1], 
            MAX([Project1].[C1]) AS [A2], 
            AVG([Project1].[C1]) AS [A3], 
            STDEVP([Project1].[C1]) AS [A4]
            FROM ( SELECT 
                [Extent1].[TimeStamp15Minutes] AS [TimeStamp15Minutes], 
                [Extent1].[DCCurrent] / [Extent2].[CurrentMPP] AS [C1]
                FROM    [dbo].[StringData] AS [Extent1]
                INNER JOIN [dbo].[DCString] AS [Extent2] ON [Extent1].[DCStringID] = [Extent2].[ID]
                INNER JOIN [dbo].[DCDistributionBox] AS [Extent3] ON [Extent2].[DCDistributionBoxID] = [Extent3].[ID]
                INNER JOIN [dbo].[DataLogger] AS [Extent4] ON [Extent3].[DataLoggerID] = [Extent4].[ID]
                WHERE ([Extent4].[ProjectID] = @p__linq__0) AND ([Extent1].[TimeStamp15Minutes] >= @p__linq__1) AND ([Extent1].[TimeStamp15Minutes] < @p__linq__2)
            )  AS [Project1]
            GROUP BY [Project1].[TimeStamp15Minutes]
        )  AS [GroupBy1];",
    new SqlParameter("p__linq__0", 20827),
    new SqlParameter("p__linq__1", fromDate),
    new SqlParameter("p__linq__2", tillDate));

    TimeSpan excecutionTimeSpan = DateTime.UtcNow - begin;
    Debug.WriteLine("{0}th run: {1}", i, excecutionTimeSpan.ToString());
}

0ème manche: 00: 00: 00.8381200

1ère manche: 00: 00: 00.6920736

2ème course: 00: 00: 00.7081006

et avec OPTION(RECOMPILE):

for (int i = 0; i < 3; i++)
{
    DateTime begin = DateTime.UtcNow;
    int result = model.Database.ExecuteSqlCommand(@"SELECT 
        1 AS [C1], 
        [GroupBy1].[K1] AS [TimeStamp15Minutes], 
        [GroupBy1].[A1] AS [C2], 
        [GroupBy1].[A2] AS [C3], 
        [GroupBy1].[A3] AS [C4], 
        [GroupBy1].[A4] AS [C5]
        FROM ( SELECT 
            [Project1].[TimeStamp15Minutes] AS [K1], 
            MIN([Project1].[C1]) AS [A1], 
            MAX([Project1].[C1]) AS [A2], 
            AVG([Project1].[C1]) AS [A3], 
            STDEVP([Project1].[C1]) AS [A4]
            FROM ( SELECT 
                [Extent1].[TimeStamp15Minutes] AS [TimeStamp15Minutes], 
                [Extent1].[DCCurrent] / [Extent2].[CurrentMPP] AS [C1]
                FROM    [dbo].[StringData] AS [Extent1]
                INNER JOIN [dbo].[DCString] AS [Extent2] ON [Extent1].[DCStringID] = [Extent2].[ID]
                INNER JOIN [dbo].[DCDistributionBox] AS [Extent3] ON [Extent2].[DCDistributionBoxID] = [Extent3].[ID]
                INNER JOIN [dbo].[DataLogger] AS [Extent4] ON [Extent3].[DataLoggerID] = [Extent4].[ID]
                WHERE ([Extent4].[ProjectID] = @p__linq__0) AND ([Extent1].[TimeStamp15Minutes] >= @p__linq__1) AND ([Extent1].[TimeStamp15Minutes] < @p__linq__2)
            )  AS [Project1]
            GROUP BY [Project1].[TimeStamp15Minutes]
        )  AS [GroupBy1]
        OPTION(RECOMPILE);",
    new SqlParameter("p__linq__0", 20827),
    new SqlParameter("p__linq__1", fromDate),
    new SqlParameter("p__linq__2", tillDate));

    TimeSpan excecutionTimeSpan = DateTime.UtcNow - begin;
    Debug.WriteLine("{0}th run: {1}", i, excecutionTimeSpan.ToString());
}

0ème manche avec RECOMPILE: 00: 00: 00.8260932

1ère manche avec RECOMPILE: 00: 00: 00.9139730

2ème manche avec RECOMPILE: 00: 00: 01.0680665

Même requête SQL exécutée dans SSMS (sans RECOMPILE):

00: 00: 01.105

Même requête SQL exécutée dans SSMS (avec RECOMPILE):

00: 00: 00.902

J'espère que ce sont toutes des valeurs dont vous aviez besoin.

23
Steffen Mangold

Dans cette réponse, je me concentre sur l'observation originale: la requête générée par EF est lente, mais lorsque la même requête est exécutée dans SSMS, elle est rapide.

Une explication possible de ce comportement est Reniflage de paramètre .

SQL Server utilise un processus appelé reniflement de paramètres lorsqu'il exécute des procédures stockées qui ont des paramètres. Lorsque la procédure est compilée ou recompilée, la valeur transmise au paramètre est évaluée et utilisée pour créer un plan d'exécution. Cette valeur est ensuite stockée avec le plan d'exécution dans le cache du plan. Lors des exécutions suivantes, cette même valeur - et ce même plan - est utilisée.

Ainsi, EF génère une requête qui a peu de paramètres. La première fois que vous exécutez cette requête, le serveur crée un plan d'exécution pour cette requête en utilisant les valeurs des paramètres qui étaient en vigueur lors de la première exécution. Ce plan est généralement assez bon. Mais, plus tard, vous exécutez la même requête EF en utilisant d'autres valeurs pour les paramètres. Il est possible que pour de nouvelles valeurs de paramètres, le plan généré précédemment ne soit pas optimal et la requête devienne lente. Le serveur continue d'utiliser le plan précédent, car il s'agit toujours de la même requête, seules les valeurs des paramètres sont différentes.

Si à ce moment vous prenez le texte de la requête et essayez de l'exécuter directement dans SSMS, le serveur créera un nouveau plan d'exécution, car techniquement ce n'est pas la même requête qui est émise par l'application EF. Une seule différence de caractère suffit, toute modification des paramètres de session suffit également pour que le serveur traite la requête comme une nouvelle. Par conséquent, le serveur a deux plans pour la même requête en apparence dans son cache. Le premier plan "lent" est lent pour les nouvelles valeurs de paramètres, car il a été initialement construit pour différentes valeurs de paramètres. Le deuxième plan "rapide" est construit pour les valeurs actuelles des paramètres, il est donc rapide.

L'article lent dans l'application, rapide dans SSMS par Erland Sommarskog explique cela et d'autres domaines connexes de manière beaucoup plus détaillée.

Il existe plusieurs façons de supprimer les plans mis en cache et de forcer le serveur à les régénérer. Changer la table ou changer les index de la table devrait le faire - cela devrait rejeter tous les plans qui sont liés à cette table, à la fois "lent" et "rapide". Ensuite, vous exécutez la requête dans l'application EF avec de nouvelles valeurs de paramètres et obtenez un nouveau plan "rapide". Vous exécutez la requête dans SSMS et obtenez un deuxième plan "rapide" avec de nouvelles valeurs de paramètres. Le serveur génère toujours deux plans, mais les deux plans sont rapides maintenant.

Une autre variante consiste à ajouter OPTION(RECOMPILE) à la requête. Avec cette option, le serveur ne stockerait pas le plan généré dans son cache. Ainsi, chaque fois que la requête s'exécute, le serveur utilise des valeurs de paramètres réelles pour générer le plan qui (selon lui) serait optimal pour les valeurs de paramètres données. L'inconvénient est un surcoût supplémentaire de la génération du plan.

Remarquez que le serveur pourrait toujours choisir un "mauvais" plan avec cette option en raison de statistiques obsolètes, par exemple. Mais, au moins, le reniflage de paramètres ne serait pas un problème.


Ceux qui se demandent comment ajouter un indice OPTION (RECOMPILE) à la requête générée par EF jettent un œil à cette réponse:

https://stackoverflow.com/a/26762756/4116017

14
Vladimir Baranov

Je sais que je suis un peu en retard ici, mais depuis que j'ai participé à la construction de la requête en question, je me sens obligé de prendre des mesures.

Le problème général que je vois avec les requêtes Linq to Entities est que la manière typique dont nous les construisons introduit des paramètres inutiles, ce qui peut affecter le plan de requête de base de données mis en cache (ainsi appelé Problème de reniflement des paramètres du serveur SQL ).

Jetons un œil à votre groupe de requêtes par expression

d => DbFunctions.AddMinutes(DateTime.MinValue, DbFunctions.DiffMinutes(DateTime.MinValue, d.TimeStamp) / minuteInterval * minuteInterval)

Puisque minuteInterval est une variable (c'est-à-dire non constante), elle introduit un paramètre. Pareil pour DateTime.MinValue (notez que les types primitifs exposent des choses similaires comme constant s, mais pour DateTime, decimal etc. ils sont champs statiques en lecture seule = ce qui fait une grande différence sur la façon dont ils sont traités à l'intérieur des expressions).

Mais quelle que soit la façon dont il est représenté dans le système CLR, DateTime.MinValue est logiquement une constante. Qu'en est-il de minuteInterval, cela dépend de votre utilisation.

Ma tentative de résoudre le problème serait d'éliminer tous les paramètres liés à cette expression. Comme nous ne pouvons pas faire cela avec une expression générée par le compilateur, nous devons la construire manuellement en utilisant System.Linq.Expressions. Ce dernier n'est pas intuitif, mais heureusement, nous pouvons utiliser une approche hybride.

Tout d'abord, nous avons besoin d'une méthode d'assistance qui nous permet de remplacer les paramètres d'expression:

public static class ExpressionUtils
{
    public static Expression ReplaceParemeter(this Expression expression, ParameterExpression source, Expression target)
    {
        return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
    }

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node == Source ? Target : base.VisitParameter(node);
        }
    }
}

Maintenant, nous avons tout ce dont nous avons besoin. Laissez encapsuler la logique dans une méthode personnalisée:

public static class QueryableUtils
{
    public static IQueryable<IGrouping<DateTime, T>> GroupBy<T>(this IQueryable<T> source, Expression<Func<T, DateTime>> dateSelector, int minuteInterval)
    {
        Expression<Func<DateTime, DateTime, int, DateTime>> expr = (date, baseDate, interval) =>
            DbFunctions.AddMinutes(baseDate, DbFunctions.DiffMinutes(baseDate, date) / interval).Value;
        var selector = Expression.Lambda<Func<T, DateTime>>(
            expr.Body
            .ReplaceParemeter(expr.Parameters[0], dateSelector.Body)
            .ReplaceParemeter(expr.Parameters[1], Expression.Constant(DateTime.MinValue))
            .ReplaceParemeter(expr.Parameters[2], Expression.Constant(minuteInterval))
            , dateSelector.Parameters[0]
        );
        return source.GroupBy(selector);
    }
}

Enfin, remplacez

.GroupBy(d => DbFunctions.AddMinutes(DateTime.MinValue, DbFunctions.DiffMinutes(DateTime.MinValue, d.TimeStamp) / minuteInterval * minuteInterval))

avec

.GroupBy(d => d.TimeStamp, minuteInterval * minuteInterval)

et la requête SQL générée serait la suivante (pour minuteInterval = 15):

SELECT 
    1 AS [C1], 
    [GroupBy1].[K1] AS [C2], 
    [GroupBy1].[A1] AS [C3], 
    [GroupBy1].[A2] AS [C4], 
    [GroupBy1].[A3] AS [C5], 
    [GroupBy1].[A4] AS [C6]
    FROM ( SELECT 
        [Project1].[K1] AS [K1], 
        MIN([Project1].[A1]) AS [A1], 
        MAX([Project1].[A2]) AS [A2], 
        AVG([Project1].[A3]) AS [A3], 
        STDEVP([Project1].[A4]) AS [A4]
        FROM ( SELECT 
            DATEADD (minute, (DATEDIFF (minute, convert(datetime2, '0001-01-01 00:00:00.0000000', 121), [Project1].[TimeStamp])) / 225, convert(datetime2, '0001-01-01 00:00:00.0000000', 121)) AS [K1], 
            [Project1].[C1] AS [A1], 
            [Project1].[C1] AS [A2], 
            [Project1].[C1] AS [A3], 
            [Project1].[C1] AS [A4]
            FROM ( SELECT 
                [Extent1].[TimeStamp] AS [TimeStamp], 
                [Extent1].[DCCurrent] / [Extent2].[CurrentMPP] AS [C1]
                FROM    [dbo].[StringDatas] AS [Extent1]
                INNER JOIN [dbo].[DCStrings] AS [Extent2] ON [Extent1].[DCStringID] = [Extent2].[ID]
                INNER JOIN [dbo].[DCDistributionBoxes] AS [Extent3] ON [Extent2].[DCDistributionBoxID] = [Extent3].[ID]
                INNER JOIN [dbo].[DataLoggers] AS [Extent4] ON [Extent3].[DataLoggerID] = [Extent4].[ID]
                WHERE ([Extent4].[ProjectID] = @p__linq__0) AND ([Extent1].[TimeStamp] >= @p__linq__1) AND ([Extent1].[TimeStamp] < @p__linq__2)
            )  AS [Project1]
        )  AS [Project1]
        GROUP BY [K1]
    )  AS [GroupBy1]

Comme vous pouvez le constater, nous avons réussi à éliminer certains des paramètres de requête. Cela vous aidera-t-il? Eh bien, comme pour tout réglage de requête de base de données, cela peut ou non. Vous devez essayer de voir.

2
Ivan Stoev

Le moteur de base de données détermine le plan de chaque requête en fonction de la façon dont elle est appelée. Dans le cas de votre requête EF Linq, le plan est préparé de manière à ce que chaque paramètre d'entrée soit traité comme un inconnu (puisque vous n'avez aucune idée de ce qui arrive). Dans votre requête réelle, vous avez tous vos paramètres dans le cadre de la requête, elle s'exécutera donc sous un plan différent de celui d'un paramètre. L'une des pièces affectées que je vois immédiatement est

... (@ p__linq__0 IS NULL) ..

Ceci est FAUX puisque p_linq_0 = 20827 et n'est PAS NUL, donc votre première moitié du WHERE est FAUX pour commencer et n'a plus besoin d'être examinée. En cas de requêtes LINQ, la base de données n'a aucune idée de ce qui arrive, donc tout évalue tout de même.

Vous devrez voir si vous pouvez utiliser des indices ou d'autres techniques pour accélérer cette exécution.

1
Milan

Lorsque EF exécute la requête, il l'enveloppe et l'exécute avec sp_executesql, ce qui signifie que le plan d'exécution sera mis en cache dans le cache du plan d'exécution de la procédure stockée. En raison de différences (reniflage de paramètres, etc.) dans la façon dont la version SQL brute vs la version SP ont leurs plans d'exécution construits, les deux peuvent différer.

Lors de l'exécution de la version EF (sp wrapped), SQL Server utilise très probablement un plan d'exécution plus générique qui couvre une plage d'horodatages plus large que les valeurs que vous transmettez réellement.

Cela dit, pour réduire les chances que SQL Server essaie quelque chose de "drôle" avec des jointures de hachage, etc., les premières choses que je ferais sont les suivantes:

1) Indexez les colonnes utilisées dans la clause where et dans les jointures

create index ix_DataLogger_ProjectID on DataLogger (ProjectID);
create index ix_DCDistributionBox_DataLoggerID on DCDistributionBox (DataLoggerID);
create index ix_DCString_DCDistributionBoxID on DCString (DCDistributionBoxID);

2) Effectuez des jointures explicites dans la requête Linq pour éliminer la partie ou ProductID est nulle

1
KristoferA