Certains aspects de l'optimisation des requêtes LINQ en C#.NET pour MS SQL Server

LINQ a introduit .NET comme nouveau langage puissant de manipulation de données. LINQ to SQL dans le cadre de celui-ci vous permet de communiquer assez facilement avec un SGBD en utilisant, par exemple, Entity Framework. Cependant, en l'utilisant assez souvent, les développeurs oublient de regarder quel type de requête SQL le fournisseur interrogeable, dans votre cas Entity Framework, générera.

Examinons deux points principaux à l'aide d'un exemple.
Pour ce faire, créez une base de données de test dans SQL Server et créez-y deux tables à l'aide de la requête suivante :

Création de tableaux

USE [TEST]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Ref](
	[ID] [int] NOT NULL,
	[ID2] [int] NOT NULL,
	[Name] [nvarchar](255) NOT NULL,
	[InsertUTCDate] [datetime] NOT NULL,
 CONSTRAINT [PK_Ref] PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Ref] ADD  CONSTRAINT [DF_Ref_InsertUTCDate]  DEFAULT (getutcdate()) FOR [InsertUTCDate]
GO

USE [TEST]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Customer](
	[ID] [int] NOT NULL,
	[Name] [nvarchar](255) NOT NULL,
	[Ref_ID] [int] NOT NULL,
	[InsertUTCDate] [datetime] NOT NULL,
	[Ref_ID2] [int] NOT NULL,
 CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Customer] ADD  CONSTRAINT [DF_Customer_Ref_ID]  DEFAULT ((0)) FOR [Ref_ID]
GO

ALTER TABLE [dbo].[Customer] ADD  CONSTRAINT [DF_Customer_InsertUTCDate]  DEFAULT (getutcdate()) FOR [InsertUTCDate]
GO

Remplissons maintenant la table Ref en exécutant le script suivant :

Remplir la table Ref

USE [TEST]
GO

DECLARE @ind INT=1;

WHILE(@ind<1200000)
BEGIN
	INSERT INTO [dbo].[Ref]
           ([ID]
           ,[ID2]
           ,[Name])
    SELECT
           @ind
           ,@ind
           ,CAST(@ind AS NVARCHAR(255));

	SET @ind=@ind+1;
END 
GO

Remplissons de la même manière la table Customer à l'aide du script suivant :

Remplir la table Client

USE [TEST]
GO

DECLARE @ind INT=1;
DECLARE @ind_ref INT=1;

WHILE(@ind<=12000000)
BEGIN
	IF(@ind%3=0) SET @ind_ref=1;
	ELSE IF (@ind%5=0) SET @ind_ref=2;
	ELSE IF (@ind%7=0) SET @ind_ref=3;
	ELSE IF (@ind%11=0) SET @ind_ref=4;
	ELSE IF (@ind%13=0) SET @ind_ref=5;
	ELSE IF (@ind%17=0) SET @ind_ref=6;
	ELSE IF (@ind%19=0) SET @ind_ref=7;
	ELSE IF (@ind%23=0) SET @ind_ref=8;
	ELSE IF (@ind%29=0) SET @ind_ref=9;
	ELSE IF (@ind%31=0) SET @ind_ref=10;
	ELSE IF (@ind%37=0) SET @ind_ref=11;
	ELSE SET @ind_ref=@ind%1190000;
	
	INSERT INTO [dbo].[Customer]
	           ([ID]
	           ,[Name]
	           ,[Ref_ID]
	           ,[Ref_ID2])
	     SELECT
	           @ind,
	           CAST(@ind AS NVARCHAR(255)),
	           @ind_ref,
	           @ind_ref;


	SET @ind=@ind+1;
END
GO

Ainsi, nous avons reçu deux tableaux, dont l'un contient plus d'un million de lignes de données et l'autre plus de 1 millions de lignes de données.

Maintenant, dans Visual Studio, vous devez créer un projet de test d'application console Visual C# (.NET Framework) :

Certains aspects de l'optimisation des requêtes LINQ en C#.NET pour MS SQL Server

Ensuite, vous devez ajouter une bibliothèque pour qu'Entity Framework puisse interagir avec la base de données.
Pour l'ajouter, cliquez avec le bouton droit sur le projet et sélectionnez Gérer les packages NuGet dans le menu contextuel :

Certains aspects de l'optimisation des requêtes LINQ en C#.NET pour MS SQL Server

Ensuite, dans la fenêtre de gestion des packages NuGet qui apparaît, saisissez le mot « Entity Framework » dans la fenêtre de recherche, sélectionnez le package Entity Framework et installez-le :

Certains aspects de l'optimisation des requêtes LINQ en C#.NET pour MS SQL Server

Ensuite, dans le fichier App.config, après avoir fermé l'élément configSections, vous devez ajouter le bloc suivant :

<connectionStrings>
    <add name="DBConnection" connectionString="data source=ИМЯ_ЭКЗЕМПЛЯРА_MSSQL;Initial Catalog=TEST;Integrated Security=True;" providerName="System.Data.SqlClient" />
</connectionStrings>

Dans connectionString, vous devez saisir la chaîne de connexion.

Créons maintenant 3 interfaces dans des fichiers séparés :

  1. Implémentation de l'interface IBaseEntityID
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Implémentation de l'interface IBaseEntityName
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Implémentation de l'interface IBaseNameInsertUTCDate
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

Et dans un fichier séparé, nous créerons une classe de base BaseEntity pour nos deux entités, qui comprendra des champs communs :

Implémentation de la classe de base BaseEntity

namespace TestLINQ
{
    public class BaseEntity : IBaseEntityID, IBaseEntityName, IBaseNameInsertUTCDate
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public DateTime InsertUTCDate { get; set; }
    }
}

Ensuite, nous allons créer nos deux entités dans des fichiers séparés :

  1. Implémentation de la classe Ref
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace TestLINQ
    {
        [Table("Ref")]
        public class Ref : BaseEntity
        {
            public int ID2 { get; set; }
        }
    }
    

  2. Implémentation de la classe Client
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace TestLINQ
    {
        [Table("Customer")]
        public class Customer: BaseEntity
        {
            public int Ref_ID { get; set; }
            public int Ref_ID2 { get; set; }
        }
    }
    

Créons maintenant un contexte UserContext dans un fichier séparé :

Implémentation de la classe UserContex

using System.Data.Entity;

namespace TestLINQ
{
    public class UserContext : DbContext
    {
        public UserContext()
            : base("DbConnection")
        {
            Database.SetInitializer<UserContext>(null);
        }

        public DbSet<Customer> Customer { get; set; }
        public DbSet<Ref> Ref { get; set; }
    }
}

Nous avons reçu une solution prête à l'emploi pour effectuer des tests d'optimisation avec LINQ to SQL via EF pour MS SQL Server :

Certains aspects de l'optimisation des requêtes LINQ en C#.NET pour MS SQL Server

Entrez maintenant le code suivant dans le fichier Program.cs :

Fichier programme.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespace TestLINQ
{
    class Program
    {
        static void Main(string[] args)
        {
            using (UserContext db = new UserContext())
            {
                var dblog = new List<string>();
                db.Database.Log = dblog.Add;

                var query = from e1 in db.Customer
                            from e2 in db.Ref
                            where (e1.Ref_ID == e2.ID)
                                 && (e1.Ref_ID2 == e2.ID2)
                            select new { Data1 = e1.Name, Data2 = e2.Name };

                var result = query.Take(1000).ToList();

                Console.WriteLine(dblog[1]);

                Console.ReadKey();
            }
        }
    }
}

Ensuite, lançons notre projet.

A la fin des travaux, les éléments suivants seront affichés sur la console :

Requête SQL générée

SELECT TOP (1000) 
    [Extent1].[Ref_ID] AS [Ref_ID], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Name] AS [Name1]
    FROM  [dbo].[Customer] AS [Extent1]
    INNER JOIN [dbo].[Ref] AS [Extent2] ON ([Extent1].[Ref_ID] = [Extent2].[ID]) AND ([Extent1].[Ref_ID2] = [Extent2].[ID2])

Autrement dit, en général, la requête LINQ a assez bien généré une requête SQL vers le SGBD MS SQL Server.

Modifions maintenant la condition AND en OR dans la requête LINQ :

Requête LINQ

var query = from e1 in db.Customer
                            from e2 in db.Ref
                            where (e1.Ref_ID == e2.ID)
                                || (e1.Ref_ID2 == e2.ID2)
                            select new { Data1 = e1.Name, Data2 = e2.Name };

Et relançons notre application.

L'exécution plantera avec une erreur en raison du temps d'exécution de la commande dépassant 30 secondes :

Certains aspects de l'optimisation des requêtes LINQ en C#.NET pour MS SQL Server

Si vous regardez la requête générée par LINQ :

Certains aspects de l'optimisation des requêtes LINQ en C#.NET pour MS SQL Server
, alors vous pouvez vous assurer que la sélection s'effectue via le produit cartésien de deux ensembles (tables) :

Requête SQL générée

SELECT TOP (1000) 
    [Extent1].[Ref_ID] AS [Ref_ID], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Name] AS [Name1]
    FROM  [dbo].[Customer] AS [Extent1]
    CROSS JOIN [dbo].[Ref] AS [Extent2]
    WHERE [Extent1].[Ref_ID] = [Extent2].[ID] OR [Extent1].[Ref_ID2] = [Extent2].[ID2]

Réécrivons la requête LINQ comme suit :

Requête LINQ optimisée

var query = (from e1 in db.Customer
                   join e2 in db.Ref
                   on e1.Ref_ID equals e2.ID
                   select new { Data1 = e1.Name, Data2 = e2.Name }).Union(
                        from e1 in db.Customer
                        join e2 in db.Ref
                        on e1.Ref_ID2 equals e2.ID2
                        select new { Data1 = e1.Name, Data2 = e2.Name });

Nous obtenons alors la requête SQL suivante :

requête SQL

SELECT 
    [Limit1].[C1] AS [C1], 
    [Limit1].[C2] AS [C2], 
    [Limit1].[C3] AS [C3]
    FROM ( SELECT DISTINCT TOP (1000) 
        [UnionAll1].[C1] AS [C1], 
        [UnionAll1].[Name] AS [C2], 
        [UnionAll1].[Name1] AS [C3]
        FROM  (SELECT 
            1 AS [C1], 
            [Extent1].[Name] AS [Name], 
            [Extent2].[Name] AS [Name1]
            FROM  [dbo].[Customer] AS [Extent1]
            INNER JOIN [dbo].[Ref] AS [Extent2] ON [Extent1].[Ref_ID] = [Extent2].[ID]
        UNION ALL
            SELECT 
            1 AS [C1], 
            [Extent3].[Name] AS [Name], 
            [Extent4].[Name] AS [Name1]
            FROM  [dbo].[Customer] AS [Extent3]
            INNER JOIN [dbo].[Ref] AS [Extent4] ON [Extent3].[Ref_ID2] = [Extent4].[ID2]) AS [UnionAll1]
    )  AS [Limit1]

Hélas, dans les requêtes LINQ, il ne peut y avoir qu'une seule condition de jointure, il est donc ici possible de créer une requête équivalente en utilisant deux requêtes pour chaque condition, puis de les combiner via Union pour supprimer les doublons entre les lignes.
Oui, les requêtes ne seront généralement pas équivalentes, étant donné que des lignes complètes en double peuvent être renvoyées. Cependant, dans la vraie vie, des lignes en double complètes ne sont pas nécessaires et les gens essaient de s'en débarrasser.

Comparons maintenant les plans d'exécution de ces deux requêtes :

  1. pour CROSS JOIN le temps d'exécution moyen est de 195 secondes :
    Certains aspects de l'optimisation des requêtes LINQ en C#.NET pour MS SQL Server
  2. pour INNER JOIN-UNION le temps d'exécution moyen est inférieur à 24 secondes :
    Certains aspects de l'optimisation des requêtes LINQ en C#.NET pour MS SQL Server

Comme vous pouvez le voir sur les résultats, pour deux tables contenant des millions d'enregistrements, la requête LINQ optimisée est plusieurs fois plus rapide que la requête non optimisée.

Pour l'option avec AND dans les conditions, une requête LINQ de la forme :

Requête LINQ

var query = from e1 in db.Customer
                            from e2 in db.Ref
                            where (e1.Ref_ID == e2.ID)
                                 && (e1.Ref_ID2 == e2.ID2)
                            select new { Data1 = e1.Name, Data2 = e2.Name };

La requête SQL correcte sera presque toujours générée, qui s'exécutera en moyenne en 1 seconde environ :

Certains aspects de l'optimisation des requêtes LINQ en C#.NET pour MS SQL Server
Également pour les manipulations LINQ to Objects au lieu d'une requête comme :

Requête LINQ (1ère option)

var query = from e1 in seq1
                            from e2 in seq2
                            where (e1.Key1==e2.Key1)
                               && (e1.Key2==e2.Key2)
                            select new { Data1 = e1.Data, Data2 = e2.Data };

vous pouvez utiliser une requête comme :

Requête LINQ (2ère option)

var query = from e1 in seq1
                            join e2 in seq2
                            on new { e1.Key1, e1.Key2 } equals new { e2.Key1, e2.Key2 }
                            select new { Data1 = e1.Data, Data2 = e2.Data };

où:

Définir deux tableaux

Para[] seq1 = new[] { new Para { Key1 = 1, Key2 = 2, Data = "777" }, new Para { Key1 = 2, Key2 = 3, Data = "888" }, new Para { Key1 = 3, Key2 = 4, Data = "999" } };
Para[] seq2 = new[] { new Para { Key1 = 1, Key2 = 2, Data = "777" }, new Para { Key1 = 2, Key2 = 3, Data = "888" }, new Para { Key1 = 3, Key2 = 5, Data = "999" } };

, et le type Para est défini comme suit :

Définition du type de para

class Para
{
        public int Key1, Key2;
        public string Data;
}

Ainsi, nous avons examiné certains aspects de l'optimisation des requêtes LINQ vers MS SQL Server.

Malheureusement, même les développeurs .NET expérimentés et de premier plan oublient qu'ils doivent comprendre ce que font les instructions qu'ils utilisent en coulisses. Sinon, ils deviennent des configurateurs et peuvent poser une bombe à retardement à l'avenir, à la fois lors de la mise à l'échelle de la solution logicielle et en cas de changements mineurs des conditions environnementales externes.

Un bref examen a également été effectué ici.

Les sources du test - le projet lui-même, la création de tables dans la base de données TEST, ainsi que le remplissage de ces tables avec des données sont localisés ici.
Également dans ce référentiel, dans le dossier Plans, il existe des plans pour exécuter des requêtes avec des conditions OR.

Source: habr.com

Ajouter un commentaire