introduction
Dans cet article, nous avons discutĂ© de certaines techniques d'optimisation des requĂȘtes LINQ .Voici quelques autres approches d'optimisation de code liĂ©es aux requĂȘtes LINQ .Il est connu que LINQ (Language-Integrated Query) est un langage simple et pratique pour interroger une source de donnĂ©es.Et LINQ to SQL est une technologie d'accĂšs aux donnĂ©es dans un SGBD. Il s'agit d'un outil puissant pour travailler avec des donnĂ©es, oĂč les requĂȘtes sont construites via un langage dĂ©claratif, qui sera ensuite converti en requĂȘtes SQL par la plateforme et envoyĂ© au serveur de base de donnĂ©es dĂ©jĂ pour exĂ©cution. Dans notre cas, par SGBD, nous entendons MS SQL Server .Cependant, les requĂȘtes LINQ ne sont pas converties en requĂȘtes SQL Ă©crites de maniĂšre optimale qu'un DBA expĂ©rimentĂ© pourrait Ă©crire avec toutes les nuances de l'optimisation des requĂȘtes SQL :- connexions optimales ( JOIN ) et filtrage des rĂ©sultats ( WHERE )
- de nombreuses nuances dans l'utilisation des composés et des conditions de groupe
- de nombreuses variantes pour remplacer les conditions IN par EXISTS et NOT IN , <> par EXISTS
- mise en cache intermédiaire des résultats via des tables temporaires, CTE, variables de table
- en utilisant une clause ( OPTION ) avec des instructions et des conseils de table WITH (...)
- l'utilisation de vues indexées, comme moyen de se débarrasser des lectures redondantes des données dans les échantillons
Les principaux goulots d'Ă©tranglement des performances des requĂȘtes SQL rĂ©sultantes lors de la compilation des requĂȘtes LINQ sont les suivants:- consolidation de l'ensemble du mĂ©canisme de sĂ©lection des donnĂ©es en une seule demande
- duplication de blocs de code identiques, ce qui conduit finalement à de multiples lectures supplémentaires de données
- groupes de conditions à plusieurs composants (logiques "et" et "ou") - ET et OU , se combinant dans des conditions difficiles, conduit au fait que l'optimiseur, ayant des index non clusterisés appropriés, par les champs nécessaires, commence finalement à balayer par l'index de cluster ( INDEX SCAN ) par groupe de conditions
- imbrication profonde des sous - requĂȘtes rend trĂšs problĂ©matique pour analyser les instructions SQL et d' analyser les plans de requĂȘtes des dĂ©veloppeurs et administrateurs de bases
Méthodes d'optimisation
Passons maintenant directement aux méthodes d'optimisation.1) Indexation supplémentaire
Il est prĂ©fĂ©rable de considĂ©rer les filtres sur les tables d'Ă©chantillonnage principales, car trĂšs souvent, la requĂȘte entiĂšre est construite autour d'une ou deux tables principales (applications-personnes-opĂ©rations) et avec un ensemble standard de conditions (IsClosed, Canceled, Enabled, Status). Il est important que les Ă©chantillons identifiĂ©s crĂ©ent les index correspondants.Cette solution est logique lorsque le choix dans ces champs limite considĂ©rablement l'ensemble renvoyĂ© Ă la requĂȘte.Par exemple, nous avons 500 000 applications. Cependant, il n'y a que 2 000 entrĂ©es actives. Ensuite, un index correctement sĂ©lectionnĂ© nous sauvera d' INDEX SCAN sur une grande table et nous permettra de sĂ©lectionner rapidement les donnĂ©es via un index non clusterisĂ©.Le manque d'index peut Ă©galement ĂȘtre dĂ©tectĂ© via des invites pour l'analyse des plans de requĂȘte ou la collecte de statistiques pour les vues systĂšmeMS SQL Server :- sys.dm_db_missing_index_groups
- sys.dm_db_missing_index_group_stats
- sys.dm_db_missing_index_details
Toutes les donnĂ©es de vue contiennent des informations sur les index manquants, Ă l'exception des index spatiaux.Cependant, les index et la mise en cache sont souvent des mĂ©thodes pour gĂ©rer les effets des requĂȘtes LINQ et SQL mal Ă©crites .Comme le montre la dure pratique de la vie pour l'entreprise, il est souvent important de mettre en Ćuvre les fonctionnalitĂ©s de l'entreprise Ă une certaine date. Et par consĂ©quent, les requĂȘtes souvent lourdes sont mises en arriĂšre-plan avec la mise en cache.Cela est en partie justifiĂ©, car l'utilisateur n'a pas toujours besoin des derniĂšres donnĂ©es et un niveau acceptable de rĂ©ponse d'interface utilisateur se produit.Cette approche vous permet de rĂ©soudre les besoins de l'entreprise, mais rĂ©duit finalement l'efficacitĂ© du systĂšme d'information, retardant simplement la rĂ©solution des problĂšmes.Il convient Ă©galement de se rappeler que dans le processus de recherche nĂ©cessaire pour ajouter de nouveaux index, les propositions d' optimisation MS SQL peuvent ĂȘtre incorrectes, y compris dans les conditions suivantes:- si des index avec un ensemble similaire de champs existent dĂ©jĂ
- si les champs de la table ne peuvent pas ĂȘtre indexĂ©s en raison de restrictions d'indexation (plus d'informations Ă ce sujet sont dĂ©crites ici ).
2) Fusion des attributs en un nouvel attribut
Parfois, certains champs de la mĂȘme table par laquelle un groupe de conditions se produit peuvent ĂȘtre remplacĂ©s par l'introduction d'un nouveau champ.Cela est particuliĂšrement vrai pour les champs d'Ă©tat, qui, par type, sont gĂ©nĂ©ralement au niveau du bit ou de l'entier.Exemple:IsClosed = 0 AND Canceled = 0 AND Enabled = 0 est remplacĂ© par Status = 1 .Vous saisissez ici l'attribut entier Status, qui est fourni en remplissant ces statuts dans le tableau. L'Ă©tape suivante consiste Ă indexer ce nouvel attribut.Il s'agit d'une solution fondamentale au problĂšme de performances, car nous demandons des donnĂ©es sans calculs inutiles.3) MatĂ©rialisation de la soumission
Malheureusement, les requĂȘtes LINQ ne peuvent pas utiliser directement des tables temporaires, des CTE et des variables de table.Cependant, il existe un autre moyen d'optimiser ce cas: il s'agit des vues indexĂ©es.Un groupe de conditions (de l'exemple ci-dessus) IsClosed = 0 AND Canceled = 0 AND Enabled = 0 (ou un ensemble d'autres conditions similaires) devient une bonne option pour les utiliser dans une vue indexĂ©e, en mettant en cache une petite tranche de donnĂ©es Ă partir d'un grand ensemble.Mais il existe un certain nombre de limitations lors de la matĂ©rialisation d'une vue:- en utilisant des sous-requĂȘtes, les clauses EXISTS doivent ĂȘtre remplacĂ©es Ă l'aide de JOIN
- Impossible d' utiliser les clauses UNION , UNION ALL , EXCEPTION , INTERSECT
- vous ne pouvez pas utiliser les conseils de table et les clauses OPTION
- aucune capacité à travailler avec des cycles
- il est impossible d'afficher des données dans une vue à partir de différentes tables
Il est important de se rappeler que les avantages rĂ©els de l'utilisation d'une vue indexĂ©e ne peuvent ĂȘtre obtenus qu'en l'indexant.Mais lors de l'appel d'une vue, ces index ne peuvent pas ĂȘtre utilisĂ©s et pour les utiliser explicitement, vous devez spĂ©cifier WITH (NOEXPAND) .Puisqu'il est impossible de dĂ©finir des conseils de table dans les requĂȘtes LINQ , vous devez donc faire une autre reprĂ©sentation - un «wrapper» de la forme suivante:CREATE VIEW _ AS SELECT * FROM MAT_VIEW WITH (NOEXPAND);
4) Utilisation des fonctions de table
Souvent dans les requĂȘtes LINQ, de grands blocs de sous-requĂȘte ou des blocs qui utilisent des reprĂ©sentations avec une structure complexe forment la requĂȘte finale avec une structure d'exĂ©cution trĂšs complexe et non optimale.Principaux avantages de l'utilisation des fonctions de table dans les requĂȘtes LINQ :- La possibilitĂ©, comme dans le cas des vues, d'utiliser et de spĂ©cifier comme objet, mais vous pouvez passer un ensemble de paramĂštres d'entrĂ©e:
FROM FUNCTION (@ param1, @ param2 ...)
au final, vous pouvez obtenir un échantillonnage de données flexible - Lorsque vous utilisez une fonction de table, il n'y a pas de restrictions aussi strictes que dans le cas des vues indexées décrites ci-dessus:
- :
LINQ .
.
, - , , :
- OPTION , OPTION(MAXDOP N), . :
- OPTION (RECOMPILE)
- , OPTION (FORCE ORDER)
OPTION .
- :
( ), .
, , WHERE (a, b, c).
a = 0 and b = 0.
, c .
a = 0 and b = 0 , .
Ici, la fonction de table peut ĂȘtre une meilleure option.
De plus, la fonction de table est plus prévisible et constante en temps d'exécution.
Exemples
Prenons un exemple d'implĂ©mentation utilisant l'exemple de la base de donnĂ©es Questions.Il existe une requĂȘte SELECT qui combine plusieurs tables et utilise une seule vue (OperativeQuestions), qui vĂ©rifie par email l'affiliation (via EXISTS ) de «Active Queries» ([OperativeQuestions]):Demande n ° 1(@p__linq__0 nvarchar(4000))SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Join2].[Object_Id] AS [Object_Id],
[Join2].[ObjectType_Id] AS [ObjectType_Id],
[Join2].[Name] AS [Name],
[Join2].[ExternalId] AS [ExternalId]
FROM [dbo].[Questions] AS [Extent1]
INNER JOIN (SELECT [Extent2].[Object_Id] AS [Object_Id],
[Extent2].[Question_Id] AS [Question_Id], [Extent3].[ExternalId] AS [ExternalId],
[Extent3].[ObjectType_Id] AS [ObjectType_Id], [Extent4].[Name] AS [Name]
FROM [dbo].[ObjectQuestions] AS [Extent2]
INNER JOIN [dbo].[Objects] AS [Extent3] ON [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] AS [Extent4]
ON [Extent3].[ObjectType_Id] = [Extent4].[Id] ) AS [Join2]
ON [Extent1].[Id] = [Join2].[Question_Id]
WHERE ([Extent1].[AnswerId] IS NULL) AND (0 = [Extent1].[Exp]) AND ( EXISTS (SELECT
1 AS [C1]
FROM [dbo].[OperativeQuestions] AS [Extent5]
WHERE (([Extent5].[Email] = @p__linq__0) OR (([Extent5].[Email] IS NULL)
AND (@p__linq__0 IS NULL))) AND ([Extent5].[Id] = [Extent1].[Id])
));
La vue a une structure assez compliquĂ©e: elle a des jointures de sous-requĂȘte et l'utilisation du tri DISTINCT , qui dans le cas gĂ©nĂ©ral est une opĂ©ration plutĂŽt gourmande en ressources.Une sĂ©lection d'environ dix mille enregistrements d'OperativeQuestions.Le principal problĂšme de cette requĂȘte est que pour les enregistrements d'une requĂȘte externe, une sous-requĂȘte interne est effectuĂ©e sur la vue [OperativeQuestions], ce qui devrait limiter l'Ă©chantillon de sortie (via EXISTS ) Ă des centaines d'enregistrements pour [Email] = @ p__linq__0 .Et il peut sembler que la sous-requĂȘte devrait une fois calculer les enregistrements par [Email] = @ p__linq__0, puis ces quelques centaines d'enregistrements devraient ĂȘtre connectĂ©s par des questions Id c, et la requĂȘte sera rapide.En fait, toutes les tables sont connectĂ©es en sĂ©rie: les questions d'ID et l'ID des questions opĂ©rationnelles sont vĂ©rifiĂ©es pour la conformitĂ© et le courrier Ă©lectronique est filtrĂ©.En fait, la demande fonctionne avec les dizaines de milliers d'enregistrements d'OperativeQuestions et vous n'avez besoin que de donnĂ©es d'intĂ©rĂȘt par e-mail.Texte de la vue OperativeQuestions:Demande n ° 2
CREATE VIEW [dbo].[OperativeQuestions]
AS
SELECT DISTINCT Q.Id, USR.email AS Email
FROM [dbo].Questions AS Q INNER JOIN
[dbo].ProcessUserAccesses AS BPU ON BPU.ProcessId = CQ.Process_Id
OUTER APPLY
(SELECT 1 AS HasNoObjects
WHERE NOT EXISTS
(SELECT 1
FROM [dbo].ObjectUserAccesses AS BOU
WHERE BOU.ProcessUserAccessId = BPU.[Id] AND BOU.[To] IS NULL)
) AS BO INNER JOIN
[dbo].Users AS USR ON USR.Id = BPU.UserId
WHERE CQ.[Exp] = 0 AND CQ.AnswerId IS NULL AND BPU.[To] IS NULL
AND (BO.HasNoObjects = 1 OR
EXISTS (SELECT 1
FROM [dbo].ObjectUserAccesses AS BOU INNER JOIN
[dbo].ObjectQuestions AS QBO
ON QBO.[Object_Id] =BOU.ObjectId
WHERE BOU.ProcessUserAccessId = BPU.Id
AND BOU.[To] IS NULL AND QBO.Question_Id = CQ.Id));
Représentation cartographique originale dans DbContext (EF Core 2)public class QuestionsDbContext : DbContext
{
//...
public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
//...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
}
}
RequĂȘte LINQ d'originevar businessObjectsData = await context
.OperativeQuestions
.Where(x => x.Email == Email)
.Include(x => x.Question)
.Select(x => x.Question)
.SelectMany(x => x.ObjectQuestions,
(x, bo) => new
{
Id = x.Id,
ObjectId = bo.Object.Id,
ObjectTypeId = bo.Object.ObjectType.Id,
ObjectTypeName = bo.Object.ObjectType.Name,
ObjectExternalId = bo.Object.ExternalId
})
.ToListAsync();
Dans ce cas particulier, une solution Ă ce problĂšme est envisagĂ©e sans changements d'infrastructure, sans introduire un tableau sĂ©parĂ© avec des rĂ©sultats prĂȘts Ă l'emploi («Active Queries»), pour lesquels un mĂ©canisme serait nĂ©cessaire pour remplir ses donnĂ©es et les tenir Ă jour.Bien qu'il s'agisse d'une bonne solution, il existe une autre option pour optimiser cette tĂąche.L'objectif principal est de mettre en cache les entrĂ©es par [Email] = @ p__linq__0 Ă partir de la vue OperativeQuestions.Nous entrons la fonction de table [dbo]. [OperativeQuestionsUserMail] dans la base de donnĂ©es.En envoyant un e-mail comme paramĂštre d'entrĂ©e, nous rĂ©cupĂ©rons la table des valeurs:Demande n ° 3
CREATE FUNCTION [dbo].[OperativeQuestionsUserMail]
(
@Email nvarchar(4000)
)
RETURNS
@tbl TABLE
(
[Id] uniqueidentifier,
[Email] nvarchar(4000)
)
AS
BEGIN
INSERT INTO @tbl ([Id], [Email])
SELECT Id, @Email
FROM [OperativeQuestions] AS [x] WHERE [x].[Email] = @Email;
RETURN;
END
Cela renvoie une table de valeurs avec une structure de donnĂ©es prĂ©dĂ©finie.Pour que les requĂȘtes vers OperativeQuestionsUserMail soient optimales, pour avoir des plans de requĂȘte optimaux, une structure stricte est requise, et non RETURNS TABLE AS RETURN ...Dans ce cas, la demande 1 souhaitĂ©e est convertie en demande 4:Demande n ° 4(@p__linq__0 nvarchar(4000))SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Join2].[Object_Id] AS [Object_Id],
[Join2].[ObjectType_Id] AS [ObjectType_Id],
[Join2].[Name] AS [Name],
[Join2].[ExternalId] AS [ExternalId]
FROM (
SELECT Id, Email FROM [dbo].[OperativeQuestionsUserMail] (@p__linq__0)
) AS [Extent0]
INNER JOIN [dbo].[Questions] AS [Extent1] ON([Extent0].Id=[Extent1].Id)
INNER JOIN (SELECT [Extent2].[Object_Id] AS [Object_Id], [Extent2].[Question_Id] AS [Question_Id], [Extent3].[ExternalId] AS [ExternalId], [Extent3].[ObjectType_Id] AS [ObjectType_Id], [Extent4].[Name] AS [Name]
FROM [dbo].[ObjectQuestions] AS [Extent2]
INNER JOIN [dbo].[Objects] AS [Extent3] ON [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] AS [Extent4]
ON [Extent3].[ObjectType_Id] = [Extent4].[Id] ) AS [Join2]
ON [Extent1].[Id] = [Join2].[Question_Id]
WHERE ([Extent1].[AnswerId] IS NULL) AND (0 = [Extent1].[Exp]);
Mappage des vues et des fonctions dans DbContext (EF Core 2)public class QuestionsDbContext : DbContext
{
//...
public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
//...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
}
}
public static class FromSqlQueries
{
public static IQueryable<OperativeQuestion> GetByUserEmail(this DbQuery<OperativeQuestion> source, string Email)
=> source.FromSql($"SELECT Id, Email FROM [dbo].[OperativeQuestionsUserMail] ({Email})");
}
RequĂȘte LINQ finalevar businessObjectsData = await context
.OperativeQuestions
.GetByUserEmail(Email)
.Include(x => x.Question)
.Select(x => x.Question)
.SelectMany(x => x.ObjectQuestions,
(x, bo) => new
{
Id = x.Id,
ObjectId = bo.Object.Id,
ObjectTypeId = bo.Object.ObjectType.Id,
ObjectTypeName = bo.Object.ObjectType.Name,
ObjectExternalId = bo.Object.ExternalId
})
.ToListAsync();
L'ordre du temps d'exécution est passé de 200 à 800 ms à 2 à 20 ms, etc., c'est-à -dire dix fois plus vite.Si nous prenons plus de moyenne, alors au lieu de 350 ms, nous avons obtenu 8 ms.Des avantages évidents, nous obtenons également:- réduction générale de la charge de lecture,
- réduction significative de la probabilité de blocage
- réduction du temps de blocage moyen à des valeurs acceptables
Conclusion
L'optimisation et le rĂ©glage fin des appels Ă la base de donnĂ©es MS SQL via LINQ est un problĂšme qui peut ĂȘtre rĂ©solu.Dans ce travail, le soin et la cohĂ©rence sont trĂšs importants.Au dĂ©but du processus:- il est nĂ©cessaire de vĂ©rifier les donnĂ©es avec lesquelles la requĂȘte fonctionne (valeurs, types de donnĂ©es sĂ©lectionnĂ©s)
- indexer correctement ces données
- vérifier l'exactitude des conditions de connexion entre les tables
à la prochaine itération, l'optimisation révÚle:- la base de la demande et le filtre principal de la demande sont déterminés
- rĂ©pĂ©tition de blocs de requĂȘte similaires et de conditions croisĂ©es
- dans le SSMS ou une autre interface graphique pour SQL Server , la requĂȘte SQL elle-mĂȘme est optimisĂ©e (allocation d'un magasin de donnĂ©es intermĂ©diaire, construction de la requĂȘte rĂ©sultante Ă l'aide de ce magasin (il peut y en avoir plusieurs))
- Ă la derniĂšre Ă©tape, en prenant comme base la requĂȘte SQL rĂ©sultante , la structure de requĂȘte LINQ est reconstruite
Par consĂ©quent, la requĂȘte LINQ rĂ©sultante devrait devenir de structure identique Ă la requĂȘte SQL optimale identifiĂ©e Ă partir du paragraphe 3.Remerciements
Un grand merci à mes collÚgues jobgemws et alex_ozrde Fortis pour avoir aidé avec cet article.