Disa aspekte të optimizimit të pyetjeve LINQ në C#.NET për MS SQL Server

LINQ hyri nĂ« .NET si njĂ« gjuhĂ« e re e fuqishme pĂ«r manipulimin e tĂ« dhĂ«nave. LINQ to SQL si pjesĂ« e tij lejon komunikim tĂ« lehtĂ« me DBMS me anĂ« tĂ«, pĂ«r shembull, Entity Framework. MegjithatĂ«, shpesh ndodh qĂ« zhvilluesit harrojnĂ« tĂ« kontrollojnĂ« se çfarĂ« lloj pyetje SQL do tĂ« generejĂ« ofruesi i queryable, nĂ« rastin tuaj — Entity Framework.

Le të shqyrtojmë dy pika kryesore me një shembull.
Për këtë, do të krijojmë një bazë të dhënash Test në SQL Server, dhe në të do të krijojmë dy tabela me anë të pyetjes së mëposhtme:

Krijimi i tabelave

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

Tani, le të mbushim tabelën Ref duke ekzekutuar skriptin e mëposhtem:

Mbushja e tabelës 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

Po ashtu, do ta mbushim tabelën Customer me anë të skriptit të mëposhtëm:

Mbushja e tabelës 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%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

Kështu, ne morëm dy tabela, njëra me më shumë se 1 milion rreshta të dhënash, dhe tjetra me më shumë se 10 milion rreshta të dhënash.

Tani në Visual Studio, është e nevojshme të krijoni një projekt testues Visual C# Console App (.NET Framework):

Disa aspekte të optimizimit të pyetjeve LINQ në C#.NET për MS SQL Server

Më pas, duhet të shtoni një bibliotekë për Entity Framework për ndërveprimin me bazën e të dhënave.
Për ta shtuar atë, do të klikojmë me një kliku të djathtë mbi projektin dhe do të zgjedhim në menunë kontekstuale Menaxho Paketat NuGet:

Disa aspekte të optimizimit të pyetjeve LINQ në C#.NET për MS SQL Server

Më pas, në dritaren e menaxhimit të paketave NuGet që shfaqet, do të shkruajmë "Entity Framework" në kutinë e kërkimit dhe do të zgjedhim paketën Entity Framework dhe do ta instalojmë atë:

Disa aspekte të optimizimit të pyetjeve LINQ në C#.NET për MS SQL Server

Pastaj, në skedarin App.config, pas mbylljes së elementit configSections, duhet të shtojmë bllokun e mëposhtëm:


Në connectionString duhet të shkruajmë vargun e lidhjes.

Tani do të krijojmë 3 interface në skeda të ndara:

  1. Implementimi i interfesës IBaseEntityID
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Implementimi i interfesës IBaseEntityName
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Implementimi i interfesës IBaseNameInsertUTCDate
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

Dhe në një skedë të veçantë do të krijojmë klasën bazë BaseEntity për dy entitetet tona, e cila do të përmbajë fushat e përbashkëta:

Implementimi i klasës 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; }
    }
}

Më pas, në skeda të ndara do të krijojmë dy entitetet tona:

  1. Implementimi i klasës Ref
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace TestLINQ
    {
        [Table("Ref")]
        public class Ref : BaseEntity
        {
            public int ID2 { get; set; }
        }
    }
    

  2. Implementimi i klasës 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; }
        }
    }
    

Tani do të krijojmë kontekstin UserContext në një skedë të veçantë:

Implementimi i klasës 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; }
    }
}

Kemi marrë një zgjidhje të gatshme për të kryer teste optimizimi me LINQ to SQL përmes EF për MS SQL Server:

Disa aspekte të optimizimit të pyetjeve LINQ në C#.NET për MS SQL Server

Tani, në skedarin Program.cs, do të shkruajmë kodin e mëposhtëm:

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

Në vazhdim do të fillojmë projektin tonë.

Në fund të ekzekutimit do të shfaqet në konsolë:

Kërkesa SQL e gjeneruar

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

Pra, në përgjithësi, kërkesa LINQ gjeneroi një kërkesë SQL mjaft të mirë për DBMS MS SQL Server.

Tani do të ndryshojmë kushtin AND në OR në kërkesën LINQ:

duhet të bëhet strukturisht identike me optimale

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

Dhe përsëri do të ekzekutojmë aplikacionin tonë.

Ekzekutimi do të dështojë me një gabim që lidhet me tejkalimin e kohës së ekzekutimit të komandës prej 30 sekondash:

Disa aspekte të optimizimit të pyetjeve LINQ në C#.NET për MS SQL Server

Nëse shqyrtojmë se cila kërkesë u gjenerua nga LINQ:

Disa aspekte të optimizimit të pyetjeve LINQ në C#.NET për MS SQL Server
, mund të sigurohemi se përzgjedhja po bëhet përmes produktit kartesian të dy grupeve (tabelave):

Kërkesa SQL e gjeneruar

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]

Le të shkruajmë përsëri kërkesën LINQ si më poshtë:

Kërkesa LINQ e optimizuar

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

Atëherë do të marrim kërkesën e mëposhtme SQL:

SQL-kërkesës

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]

Fatkeqësisht, në kërkesat LINQ, kushti i bashkimit mund të jetë vetëm një, kështu që këtu është e mundur të bëni një kërkesë ekuivalente përmes dy kërkesave sipas çdo kushti dhe më pas t'i bashkoni ato përmes Union për të hequr dublikata midis rreshtave.
Po, kërkesat në përgjithësi do të rezultojnë të jenë jo ekuivalente, duke marrë parasysh se mund të kthehen rreshta të plotë dublikat. Megjithatë, në jetë reale, rreshtat e plota dublikuar nuk janë të nevojshme dhe përpiqen të hiqen.

Tani le të krahasojmë planet e ekzekutimit të këtyre dy kërkesave:

  1. për CROSS JOIN, koha mesatare e ekzekutimit është 195 sekonda:
    Disa aspekte të optimizimit të pyetjeve LINQ në C#.NET për MS SQL Server
  2. për INNER JOIN-UNION, koha mesatare e ekzekutimit është më pak se 24 sekonda:
    Disa aspekte të optimizimit të pyetjeve LINQ në C#.NET për MS SQL Server

Siç vërehet nga rezultatet, për dy tabela me miliona regjistrime, kërkesa LINQ e optimizuar punon shumë më shpejt se e paoptimizuara.

Për variantin me AND në kushtet e kërkesës LINQ si:

duhet të bëhet strukturisht identike me optimale

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

përgjithësisht do të gjenerohet një kërkesë SQL e saktë, e cila do të ketë një kohë mesatare ekzekutimi prej rreth 1 sekonde:

Disa aspekte të optimizimit të pyetjeve LINQ në C#.NET për MS SQL Server
Po ashtu, për manipulimet LINQ to Objects përveç kërkesës si:

Kërkesa LINQ (varianti i parë)

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

mund të përdorim një kërkesë si:

Kërkesa LINQ (varianti i dytë)

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

ku:

Përkufizimi i dy fushave

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

, dhe tipi Para përcaktohet si më poshtë:

Përkufizimi i tipit Para

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

Dhe kështu kemi shqyrtuar disa aspekte të optimizimit të kërkesave LINQ për MS SQL Server.

Fatkeqësisht, edhe zhvilluesit më të përvojë dhe liderët e .NET harrojnë që është e nevojshme të kuptojnë se çfarë bëjnë pas skenës ato instruksione që ata përdorin. Në të kundërt, ata bëhen konfigurues dhe mund të vunë një bombë me veprim të ngadalshëm në të ardhmen, si gjatë zgjerimit të zgjidhjes software, ashtu edhe në kushte të jashtme të ndryshme.

Po ashtu u krye një përmbledhje e vogël dhe këtu.

Burimet për testin-së projektit, krijimi i tabelave në bazën e të dhënave TEST, si dhe mbushja e këtyre tabelave ndodhen këtu.
Po ashtu në këtë depo, në dosjen Plans ndodhen planet e realizimit të kërkesave me kushte OSE.

Burimi: habr.com

Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster