<p>J’ai une question concernant les performances d’exécution des requêtes Entity Framework.</p>
<p><strong>Schéma</strong> :</p>
<p>J’ai une structure de table comme ceci :</p>
<pre><code class="lang-auto">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])
</code></pre>
<p>Des index standard sur les clés étrangères existent également (je ne veux pas tous les lister pour des raisons d’espace).</p>
<p>La table <code>[StringData]</code> a les statistiques de stockage suivantes :</p>
<ul>
<li>
<p>Espace de données : 26 901,86 Mo</p>
</li>
<li>
<p>Nombre de lignes : 131 827 749</p>
</li>
<li>
<p>Partitionnée : oui</p>
</li>
<li>
<p>Nombre de partitions : 62</p>
</li>
</ul>
<p><strong>Utilisation</strong> :</p>
<p>Je souhaite maintenant regrouper les données de la table <code>[StringData]</code> et effectuer des agrégations.</p>
<p>J’ai créé une requête Entity Framework (les détails de la requête se trouvent <a href="https://stackoverflow.com/questions/35436442" rel="noopener nofollow ugc">ici</a>) :</p>
<pre><code class="lang-auto">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();
</code></pre>
<p>Le temps d’exécution est exceptionnellement long !?</p>
<ul>
<li>
<p>Résultat d’exécution : 92 lignes</p>
</li>
<li>
<p>Temps d’exécution : 16000ms</p>
</li>
</ul>
<p><strong>Tentatives</strong> :</p>
<p>J’ai ensuite examiné la requête SQL générée par Entity Framework qui ressemble à ceci :</p>
<pre><code class="lang-auto">DECLARE @plinq4 DATETIME = 0;
DECLARE @plinq3 DATETIME = 0;
DECLARE @plinq5 INT = 15;
DECLARE @plinq6 INT = 15;
DECLARE @plinq0 BIGINT = 20827;
DECLARE @plinq1 DATETIME = '06.02.2016 00:00:00';
DECLARE @plinq2 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, @plinq4, [Project1].[TimeStamp])) / @plinq5) * @plinq6, @plinq3) 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] = @plinq0) OR (([Extent4].[ProjectID] IS NULL) AND (@plinq0 IS NULL))) AND ([Extent1].[TimeStamp] >= @plinq1) AND ([Extent1].[TimeStamp] < @plinq2)
) AS [Project1]
) AS [Project1]
GROUP BY [K1]
) AS [GroupBy1]
</code></pre>
<p>J’ai copié cette requête SQL dans SSMS sur la même machine, connecté avec la même chaîne de connexion qu’Entity Framework.</p>
<p>Le résultat est une performance très nettement améliorée :</p>
<ul>
<li>
<p>Résultat d’exécution : 92 lignes</p>
</li>
<li>
<p>Temps d’exécution : 517ms</p>
</li>
</ul>
<p>J’ai également effectué un test en boucle et le résultat est étrange. Le test ressemble à ceci :</p>
<pre><code class="lang-auto">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());
}
</code></pre>
<p>Le résultat est très différent et semble aléatoire (?) :</p>
<pre><code class="lang-auto">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
</code></pre>
<p><strong>Question</strong> :</p>
<p>Pourquoi l’exécution des requêtes Entity Framework est-elle si lente ? Le nombre de lignes résultantes est vraiment faible et la requête SQL brute montre des performances très rapides.</p>
<p><strong>Mise à jour 1</strong> :</p>
<p>Je m’assure que ce n’est pas un délai lié à la création du MetaContext ou du modèle. Quelques autres requêtes sont exécutées sur la même instance de modèle juste avant avec de bonnes performances.</p>
<p><strong>Mise à jour 2</strong> (en rapport avec la réponse de <span class="mention">@x0007me</span>) :</p>
<p>Merci pour l’indication mais cela peut être éliminé en changeant les paramètres du modèle comme ceci :</p>
<pre><code class="lang-auto">modelContext.Configuration.UseDatabaseNullSemantics = true;
</code></pre>
<p>Le SQL généré par EF est maintenant :</p>
<pre><code class="lang-auto">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, @plinq4, [Project1].[TimeStamp])) / @plinq5) * @plinq6, @plinq3) 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] = @plinq0) AND ([Extent1].[TimeStamp] >= @plinq1) AND ([Extent1].[TimeStamp] < @plinq2)
) AS [Project1]
) AS [Project1]
GROUP BY [K1]
) AS [GroupBy1]
</code></pre>
<p>Comme vous pouvez le voir, le problème que vous avez décrit est maintenant résolu, mais le temps d’exécution ne change pas.</p>
<p>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 des index hautement optimisés.</p>
<p><strong>Mise à jour 3</strong> (en rapport avec la réponse de <span class="mention">@Vladimir</span> Baranov) :</p>
<p>Je ne vois pas en quoi cela peut être lié à la mise en cache du plan de requête. Parce que dans la MSDN, il est clairement décrit qu’EF6 utilise la mise en cache du plan de requête.</p>
<p>Un simple test prouve que l’énorme différence de temps d’exécution n’est pas liée à la mise en cache du plan de requête (pseudo-code) :</p>
<pre><code class="lang-auto">using(var modelContext = new ModelContext())
{
modelContext.Query(); //1th run activates caching
modelContext.Query(); //2th used cached plan
}
</code></pre>
<p>En résultat, les deux requêtes s’exécutent avec le même temps.</p>
<p><strong>Mise à jour 4</strong> (en rapport avec la réponse de <span class="mention">@bubi</span>) :</p>
<p>J’ai essayé d’exécuter la requête générée par EF comme vous l’avez décrit :</p>
<pre><code class="lang-auto">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])) / @plinq5) * @plinq6, 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] = @plinq0) AND ([Extent1].[TimeStamp] >= @plinq1) AND ([Extent1].[TimeStamp] < @plinq2)
) AS [Project1]
) AS [Project1]
GROUP BY [K1]
) AS [GroupBy1]",
new SqlParameter("plinq0", 20827),
new SqlParameter("plinq1", fromDate),
new SqlParameter("plinq2", tillDate),
new SqlParameter("plinq5", 15),
new SqlParameter("plinq6", 15));
</code></pre>
<ul>
<li>
<p>Résultat d’exécution : 92</p>
</li>
<li>
<p>Temps d’exécution : 16000ms</p>
</li>
</ul>
<p>Cela a pris exactement aussi longtemps que la requête EF normale !?</p>
<p><strong>Mise à jour 5</strong> (en rapport avec la réponse de <span class="mention">@vittore</span>) :</p>
<p>J’ai créé un arbre d’appels tracé, peut-être que cela aidera :</p>
<p><strong>Mise à jour 6</strong> (en rapport avec la réponse de <span class="mention">@usr</span>) :</p>
<p>J’ai créé deux XML de plan d’exécution via SQL Server Profiler.</p>
<p><a href="http://pastebin.com/6We2u6Dg" rel="noopener nofollow ugc">Exécution rapide (SSMS).SQLPlan</a></p>
<p><a href="http://pastebin.com/B2mty2R9" rel="noopener nofollow ugc">Exécution lente (EF).SQLPlan</a></p>
<p><strong>Mise à jour 7</strong> (en rapport avec les commentaires de <span class="mention">@VladimirBaranov</span>) :</p>
<p>J’ai maintenant effectué d’autres cas de test en rapport avec vos commentaires.</p>
<p>D’abord j’ai éliminé les opérations d’ordonnancement coûteuses en utilisant une nouvelle colonne calculée et un INDEX correspondant. Cela réduit le décalage de performance lié à <code>DATEADD(MINUTE, DATEDIFF(MINUTE, 0, [TimeStamp] ) / 15* 15, 0)</code>. Les détails sur comment et pourquoi se trouvent <a href="https://stackoverflow.com/questions/35436442" rel="noopener nofollow ugc">ici</a>.</p>
<p>Le résultat ressemble à ceci :</p>
<p>Requête Entity Framework pure :</p>
<pre><code class="lang-auto">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());
}
</code></pre>
<p>0th run pure EF: <strong>00:00:12.6460624</strong></p>
<p>1th run pure EF: <strong>00:00:11.0258393</strong></p>
<p>2th run pure EF: <strong>00:00:08.4171044</strong></p>
<p>J’ai ensuite utilisé le SQL généré par EF comme requête SQL :</p>
<pre><code class="lang-auto">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] = @plinq0) AND ([Extent1].[TimeStamp15Minutes] >= @plinq1) AND ([Extent1].[TimeStamp15Minutes] < @plinq2)
) AS [Project1]
GROUP BY [Project1].[TimeStamp15Minutes]
) AS [GroupBy1];",
new SqlParameter("plinq0", 20827),
new SqlParameter("plinq1", fromDate),
new SqlParameter("plinq2", tillDate));
TimeSpan excecutionTimeSpan = DateTime.UtcNow - begin;
Debug.WriteLine("{0}th run: {1}", i, excecutionTimeSpan.ToString());
}
</code></pre>
<p>0th run: <strong>00:00:00.8381200</strong></p>
<p>1th run: <strong>00:00:00.6920736</strong></p>
<p>2th run: <strong>00:00:00.7081006</strong></p>
<p>et avec <code>OPTION(RECOMPILE)</code> :</p>
<pre><code class="lang-auto">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] = @plinq0) AND ([Extent1].[TimeStamp15Minutes] >= @plinq1) AND ([Extent1].[TimeStamp15Minutes] < @plinq2)
) AS [Project1]
GROUP BY [Project1].[TimeStamp15Minutes]
) AS [GroupBy1]
OPTION(RECOMPILE);",
new SqlParameter("plinq0", 20827),
new SqlParameter("plinq1", fromDate),
new SqlParameter("plinq2", tillDate));
TimeSpan excecutionTimeSpan = DateTime.UtcNow - begin;
Debug.WriteLine("{0}th run: {1}", i, excecutionTimeSpan.ToString());
}
</code></pre>
<p>0th run avec RECOMPILE: <strong>00:00:00.8260932</strong></p>
<p>1th run avec RECOMPILE: <strong>00:00:00.9139730</strong></p>
<p>2th run avec RECOMPILE: <strong>00:00:01.0680665</strong></p>
<p>Même requête SQL exécutée dans SSMS (sans RECOMPILE) :</p>
<p><strong>00:00:01.105</strong></p>
<p>Même requête SQL exécutée dans SSMS (avec RECOMPILE) :</p>
<p><strong>00:00:00.902</strong></p>
<p>J’espère que ce sont toutes les valeurs dont vous aviez besoin.</p>
<hr>
<p><em>Source : <a href="https://stackoverflow.com/questions/35436442" rel="noopener nofollow ugc">Stack Overflow</a></em></p>