Alcuni aspetti dell'ottimizzazione delle query LINQ in C#.NET per MS SQL Server

LINQ è entrato in .NET come un nuovo e potente linguaggio di manipolazione dei dati. LINQ to SQL come parte di esso consente di comunicare in modo abbastanza conveniente con un DBMS utilizzando, ad esempio, Entity Framework. Tuttavia, utilizzandolo abbastanza spesso, gli sviluppatori dimenticano di considerare il tipo di query SQL che il provider interrogabile, nel tuo caso Entity Framework, genererà.

Esaminiamo due punti principali utilizzando un esempio.
Per fare ciò, crea un database di test in SQL Server e crea due tabelle al suo interno utilizzando la seguente query:

Creazione di tabelle

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

Ora popoliamo la tabella Ref eseguendo il seguente script:

Compilazione della tabella 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

Allo stesso modo riempiamo la tabella Clienti utilizzando il seguente script:

Popolamento della tabella Clienti

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

Pertanto, abbiamo ricevuto due tabelle, una delle quali contiene più di 1 milione di righe di dati e l'altra più di 10 milioni di righe di dati.

Ora in Visual Studio è necessario creare un progetto di prova dell'app console Visual C# (.NET Framework):

Alcuni aspetti dell'ottimizzazione delle query LINQ in C#.NET per MS SQL Server

Successivamente, è necessario aggiungere una libreria affinché Entity Framework possa interagire con il database.
Per aggiungerlo, fare clic con il pulsante destro del mouse sul progetto e selezionare Gestisci pacchetti NuGet dal menu contestuale:

Alcuni aspetti dell'ottimizzazione delle query LINQ in C#.NET per MS SQL Server

Successivamente, nella finestra di gestione dei pacchetti NuGet che appare, inserisci la parola “Entity Framework” nella finestra di ricerca e seleziona il pacchetto Entity Framework e installalo:

Alcuni aspetti dell'ottimizzazione delle query LINQ in C#.NET per MS SQL Server

Successivamente, nel file App.config, dopo aver chiuso l'elemento configSections, è necessario aggiungere il seguente blocco:

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

In ConnectionString è necessario inserire la stringa di connessione.

Ora creiamo 3 interfacce in file separati:

  1. Implementazione dell'interfaccia IBaseEntityID
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Implementazione dell'interfaccia IBaseEntityName
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Implementazione dell'interfaccia IBaseNameInsertUTCDate
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

E in un file separato creeremo una classe base BaseEntity per le nostre due entità, che includerà campi comuni:

Implementazione della classe 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; }
    }
}

Successivamente, creeremo le nostre due entità in file separati:

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

  2. Implementazione della classe Customer
    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; }
        }
    }
    

Ora creiamo un contesto UserContext in un file separato:

Implementazione della 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; }
    }
}

Abbiamo ricevuto una soluzione già pronta per condurre test di ottimizzazione con LINQ to SQL tramite EF per MS SQL Server:

Alcuni aspetti dell'ottimizzazione delle query LINQ in C#.NET per MS SQL Server

Ora inserisci il seguente codice nel file Program.cs:

File Program.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();
            }
        }
    }
}

Successivamente, lanciamo il nostro progetto.

Al termine del lavoro sulla console verrà visualizzato quanto segue:

Query SQL generata

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])

Cioè, in generale, la query LINQ ha generato abbastanza bene una query SQL sul DBMS MS SQL Server.

Ora cambiamo la condizione AND in OR nella query LINQ:

Domanda 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 };

E lanciamo di nuovo la nostra applicazione.

L'esecuzione si bloccherà con un errore a causa del tempo di esecuzione del comando superiore a 30 secondi:

Alcuni aspetti dell'ottimizzazione delle query LINQ in C#.NET per MS SQL Server

Se guardi la query generata da LINQ:

Alcuni aspetti dell'ottimizzazione delle query LINQ in C#.NET per MS SQL Server
, allora puoi fare in modo che la selezione avvenga tramite il prodotto cartesiano di due insiemi (tabelle):

Query SQL generata

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]

Riscriviamo la query LINQ come segue:

Query LINQ ottimizzata

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 });

Quindi otteniamo la seguente query SQL:

Interrogazione 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]

Purtroppo, nelle query LINQ può esserci solo una condizione di join, quindi qui è possibile creare una query equivalente utilizzando due query per ciascuna condizione e poi combinandole tramite Union per rimuovere i duplicati tra le righe.
Sì, le query generalmente non saranno equivalenti, tenendo conto che potrebbero essere restituite righe duplicate complete. Tuttavia, nella vita reale, non sono necessarie linee duplicate complete e le persone cercano di sbarazzarsene.

Ora confrontiamo i piani di esecuzione di queste due query:

  1. per CROSS JOIN il tempo medio di esecuzione è di 195 secondi:
    Alcuni aspetti dell'ottimizzazione delle query LINQ in C#.NET per MS SQL Server
  2. per INNER JOIN-UNION il tempo medio di esecuzione è inferiore a 24 secondi:
    Alcuni aspetti dell'ottimizzazione delle query LINQ in C#.NET per MS SQL Server

Come puoi vedere dai risultati, per due tabelle con milioni di record, la query LINQ ottimizzata è molte volte più veloce di quella non ottimizzata.

Per l'opzione con AND nelle condizioni, una query LINQ nel formato:

Domanda 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 };

Verrà quasi sempre generata la query SQL corretta, che verrà eseguita mediamente in circa 1 secondo:

Alcuni aspetti dell'ottimizzazione delle query LINQ in C#.NET per MS SQL Server
Anche per le manipolazioni LINQ to Objects invece di una query come:

Query LINQ (prima opzione)

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 };

puoi usare una query come:

Query LINQ (prima opzione)

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 };

dove:

Definizione di due array

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" } };

e il tipo Para è definito come segue:

Definizione del tipo di paragrafo

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

Pertanto, abbiamo esaminato alcuni aspetti dell'ottimizzazione delle query LINQ su MS SQL Server.

Sfortunatamente, anche gli sviluppatori .NET più esperti e leader dimenticano che è necessario comprendere cosa fanno dietro le quinte le istruzioni che utilizzano. Altrimenti diventano configuratori e possono piazzare una bomba a orologeria per il futuro sia quando si scala la soluzione software che con piccoli cambiamenti nelle condizioni ambientali esterne.

È stata effettuata anche una breve revisione qui.

Si trovano le fonti del test: il progetto stesso, la creazione di tabelle nel database TEST e il riempimento di queste tabelle con i dati qui.
Anche in questo repository, nella cartella Plans, sono presenti i piani per l'esecuzione di query con condizioni OR.

Fonte: habr.com

Aggiungi un commento