Unele aspecte ale optimizării interogărilor LINQ în C#.NET pentru MS SQL Server

LINQ a fost introdus în .NET ca un nou limbaj puternic pentru manipularea datelor. LINQ to SQL, ca parte a acestuia, permite comunicarea destul de convenabilă cu bazele de date, de exemplu, prin Entity Framework. Totuși, utilizându-l frecvent, dezvoltatorii uită să verifice ce interogare SQL exact va genera providerul queryable, în cazul dumneavoastră - Entity Framework.

Să analizăm două aspecte principale printr-un exemplu.
Pentru aceasta, în SQL Server vom crea o bază de date Test, iar în aceasta vom crea două tabele folosind următoarea interogare:

Crearea tabelelor

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

Acum să umplem tabela Ref folosind următorul script:

Umplerea tabelei 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

În mod similar, să umplem tabela Customer folosind următorul script:

Umplerea tabelei Customer

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=0) SET @ind_ref=4;
	ELSE IF (@ind=0) SET @ind_ref=5;
	ELSE IF (@ind=0) SET @ind_ref=6;
	ELSE IF (@ind=0) SET @ind_ref=7;
	ELSE IF (@ind=0) SET @ind_ref=8;
	ELSE IF (@ind=0) SET @ind_ref=9;
	ELSE IF (@ind=0) SET @ind_ref=10;
	ELSE IF (@ind=0) SET @ind_ref=11;
	ELSE SET @ind_ref=@ind90000;
	
	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

Astfel, am obținut două tabele, una cu peste 1 milion de rânduri de date, iar cealaltă cu peste 10 milioane de rânduri de date.

Acum, în Visual Studio, trebuie să creăm un proiect de testare Visual C# Console App (.NET Framework):

Unele aspecte ale optimizării interogărilor LINQ în C#.NET pentru MS SQL Server

Apoi, trebuie să adăugăm o bibliotecă pentru Entity Framework pentru a interacționa cu baza de date.
Pentru a-l adăuga, vom da clic dreapta pe proiect și vom alege din meniul contextual Manage NuGet Packages:

Unele aspecte ale optimizării interogărilor LINQ în C#.NET pentru MS SQL Server

Apoi, în fereastra de gestionare a pachetelor NuGet care apare, în câmpul de căutare vom introduce cuvântul „Entity Framework” și vom alege pachetul Entity Framework și îl vom instala:

Unele aspecte ale optimizării interogărilor LINQ în C#.NET pentru MS SQL Server

Ulterior, în fișierul App.config, după închiderea elementului configSections, trebuie să adăugăm următorul bloc:


În connectionString trebuie să introducem șirul de conexiune.

Acum vom crea în fișiere separate 3 interfețe:

  1. Implementarea interfeței IBaseEntityID
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Implementarea interfeței IBaseEntityName
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Implementarea interfeței IBaseNameInsertUTCDate
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

Și în fișiere separate vom crea clasa de bază BaseEntity pentru cele două entități, care va conține câmpurile comune:

Implementarea clasei de bază BaseEntity

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

Apoi, în fișiere separate vom crea cele două entități:

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

  2. Implementarea clasei 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; }
        }
    }
    

Acum vom crea în fișierul separat contextul UserContext:

Implementarea clasei UserContext

using System.Data.Entity;

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

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

Am obținut o soluție gata pentru a efectua teste de optimizare cu LINQ to SQL prin EF pentru MS SQL Server:

Unele aspecte ale optimizării interogărilor LINQ în C#.NET pentru MS SQL Server

Acum, în fișierul Program.cs vom introduce următorul cod:

Fișierul 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();
                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();
            }
        }
    }
}

Apoi, să lansăm proiectul nostru.

La sfârșitul execuției, va fi afișat pe consolă:

Interogarea SQL generată

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

Adică, în general, interogarea LINQ a generat un SQL destul de bun pentru SGBD MS SQL Server.

Acum, să schimbăm condiția AND în OR în interogarea LINQ:

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

Și din nou să lansăm aplicația noastră.

Execuția va eșua cu o eroare legată de depășirea timpului de execuție de 30 de secunde:

Unele aspecte ale optimizării interogărilor LINQ în C#.NET pentru MS SQL Server

Dacă ne uităm ce interogare a fost generată de LINQ:

Unele aspecte ale optimizării interogărilor LINQ în C#.NET pentru MS SQL Server
, putem observa că selecția se face prin produsul cartesians al două mulțimi (tabele):

Interogarea SQL generată

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]

Să rescriem interogarea LINQ astfel:

Interogarea LINQ optimizată

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

Atunci vom obține următoarea interogare SQL:

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

Din păcate, în interogările LINQ condiția de unire poate fi doar una, așa că este posibil să se facă o interogare echivalentă prin două interogări pentru fiecare condiție, urmate de unirea lor prin Union pentru a elimina duplicatele din rânduri.
Da, în general interogările vor fi neechivalente având în vedere că pot fi returnate duplicate complete ale rândurilor. Totuși, în viața reală, duplicatele complete nu sunt necesare și se evită.

Acum să comparăm planurile de execuție ale acestor două interogări:

  1. pentru CROSS JOIN, timpul mediu de execuție este de 195 secunde:
    Unele aspecte ale optimizării interogărilor LINQ în C#.NET pentru MS SQL Server
  2. pentru INNER JOIN-UNION, timpul mediu de execuție este mai mic de 24 secunde:
    Unele aspecte ale optimizării interogărilor LINQ în C#.NET pentru MS SQL Server

După cum se poate observa din rezultate, pentru două tabele cu milioane de înregistrări, interogarea LINQ optimizată funcționează de câteva ori mai repede decât cea neoptimizată.

Pentru varianta cu AND în condițiile interogării LINQ de tip:

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

aproape întotdeauna va fi generată o interogare SQL corectă, care se va executa în medie în aproximativ 1 secundă:

Unele aspecte ale optimizării interogărilor LINQ în C#.NET pentru MS SQL Server
De asemenea, pentru manipulările LINQ to Objects, în loc de interogarea de tip:

Interogare LINQ (varianta 1)

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

se poate utiliza o interogare de tip:

Interogare LINQ (varianta 2)

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

unde:

Definirea a două array-uri

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

, iar tipul Para este definit astfel:

Definirea tipului Para

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

Astfel, am analizat unele aspecte ale optimizării interogărilor LINQ pentru MS SQL Server.

Din păcate, chiar și cei mai experimentați și renumiți dezvoltatori .NET uită că este esențial să înțeleagă ce se întâmplă în spatele instrucțiunilor pe care le utilizează. Altfel, ei devin doar configuratori și pot lăsa o bombă cu efect întârziat pentru viitor, atât atunci când se scalează soluția de programare, cât și în cazul unor schimbări minore în condițiile externe ale mediului.

De asemenea, a fost realizat un mic rezumat și aici.

Sursele pentru testare - proiectul în sine, crearea tabelelor în baza de date TEST, precum și popularea acestor tabele se află aici.
De asemenea, în acest repository, în folderul Plans, se găsesc planurile de executare a interogărilor cu condiții OR.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster