Vissa aspekter av optimering av LINQ-frÄgor i C#.NET för MS SQL Server

LINQ introducerades i .NET som ett nytt kraftfullt sprĂ„k för datamanipulering. LINQ to SQL som en del av det gör det ganska enkelt att kommunicera med databaser via till exempel Entity Framework. Men, nĂ€r utvecklare anvĂ€nder det ofta, glömmer de att fundera pĂ„ vilken SQL-frĂ„ga som kommer att genereras av queryable provider, i ditt fall – Entity Framework.

Vi kommer att gÄ igenom tvÄ huvudpunkter med ett exempel.
För detta ÀndamÄl skapar vi en databas i SQL Server som heter Test, och i den skapar vi tvÄ tabeller med följande frÄga:

Skapande av tabeller

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

Nu fyller vi tabellen Ref genom att köra följande skript:

Fyllning av tabellen 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

PÄ liknande sÀtt fyller vi tabellen Customer med hjÀlp av följande skript:

Fyllning av tabellen 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

SÄledes har vi fÄtt tvÄ tabeller, dÀr den ena innehÄller mer Àn 1 miljon rader data och den andra mer Àn 10 miljoner rader data.

Nu i Visual Studio mÄste du skapa ett testprojekt Visual C# Console App (.NET Framework):

Vissa aspekter av optimering av LINQ-frÄgor i C#.NET för MS SQL Server

Sedan mÄste du lÀgga till ett bibliotek för Entity Framework för att interagera med databasen.
För att lÀgga till den, högerklicka pÄ projektet och vÀlj "Hantera NuGet-paket" frÄn kontextmenyn:

Vissa aspekter av optimering av LINQ-frÄgor i C#.NET för MS SQL Server

I det följande fönstret för NuGet-pakethanterare skriver vi "Entity Framework" i sökfÀltet, vÀljer paketet Entity Framework och installerar det:

Vissa aspekter av optimering av LINQ-frÄgor i C#.NET för MS SQL Server

Sedan i filen App.config efter slutet av elementet configSections mÄste vi lÀgga till följande block:


I connectionString mÄste vi ange anslutningsstrÀngen.

Nu skapar vi tre grÀnssnitt i separata filer:

  1. Implementering av grÀnssnittet IBaseEntityID
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Implementering av grÀnssnittet IBaseEntityName
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Implementering av grÀnssnittet IBaseNameInsertUTCDate
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

Och i en separat fil skapar vi basklass BaseEntity för vÄra tvÄ entiteter, som kommer att innehÄlla gemensamma fÀlt:

Implementering av basklass BaseEntity

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

Sedan i separata filer skapar vi vÄra tvÄ entiteter:

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

  2. Implementering av klassen 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; }
        }
    }
    

Nu skapar vi kontexten UserContext i en separat fil:

Implementering av klassen 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; }
    }
}

Vi har fÄtt en fÀrdig lösning för att genomföra tester för optimering med LINQ to SQL via EF för MS SQL Server:

Vissa aspekter av optimering av LINQ-frÄgor i C#.NET för MS SQL Server

Nu skriver vi följande kod i filen Program.cs:

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

LÄt oss nu köra vÄrt projekt.

I slutet av körningen kommer följande att skrivas ut i konsolen:

Genererad SQL-frÄga

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

Det vill sÀga, genererade SQL-frÄgan frÄn LINQ Àr i allmÀnhet ganska bra för MS SQL Server.

Nu ska vi Àndra villkoret frÄn OCH till ELLER i LINQ-frÄgan:

LINQ-frÄgan

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

Och vi kör vÄrt program igen.

Exekveringen kommer att krascha med ett fel relaterat till överskridande av körningstiden för kommandot pÄ 30 sekunder:

Vissa aspekter av optimering av LINQ-frÄgor i C#.NET för MS SQL Server

Om vi ser vilken frÄga som genererades av LINQ:

Vissa aspekter av optimering av LINQ-frÄgor i C#.NET för MS SQL Server
, kan vi konstatera att urvalet görs genom kartesiskt produkt av tvÄ mÀngder (tabeller):

Genererad SQL-frÄga

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]

LÄt oss skriva om LINQ-frÄgan pÄ följande sÀtt:

Optimerad LINQ-frÄga

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

DÄ fÄr vi följande SQL-frÄga:

SQL-frÄga

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]

TyvÀrr kan det i LINQ-frÄgor bara finnas en anslutningsvillkor, sÄ hÀr kan en ekvivalent frÄga göras genom tvÄ frÄgor för varje villkor, och sedan förenas de med Union för att ta bort dubbletter bland rader.
Ja, frÄgorna blir i allmÀnhet inte ekvivalenta med hÀnsyn till att fullstÀndiga dubbletter av rader kan returneras. Men i verkliga livet behövs inte fullstÀndiga dublicerade rader och man försöker bli av med dem.

LÄt oss nu jÀmföra exekveringsplanerna för dessa tvÄ frÄgor:

  1. för CROSS JOIN Àr den genomsnittliga exekveringstiden 195 sek:
    Vissa aspekter av optimering av LINQ-frÄgor i C#.NET för MS SQL Server
  2. för INNER JOIN-UNION Àr den genomsnittliga exekveringstiden mindre Àn 24 sek:
    Vissa aspekter av optimering av LINQ-frÄgor i C#.NET för MS SQL Server

Som vi kan se frÄn resultaten, sÄ fungerar den optimerade LINQ-frÄgan för tvÄ tabeller med miljoner poster ett flertal gÄnger snabbare Àn den icke optimerade.

För alternativet med OCH i villkoren för LINQ-frÄgan av typen:

LINQ-frÄgan

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

kommer nÀstan alltid att en korrekt SQL-frÄga att genereras, vilket i genomsnitt kommer att köras pÄ ungefÀr 1 sek:

Vissa aspekter av optimering av LINQ-frÄgor i C#.NET för MS SQL Server
Även för manipulationer av LINQ till objekt istĂ€llet för en frĂ„ga av typen:

LINQ-frÄga (första alternativet)

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

kan en frÄga av typen anvÀndas:

LINQ-frÄga (andra alternativet)

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

dÀr:

Definition av tvÄ arrayer

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

, och typen Para definieras som följer:

Definition av typen Para

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

SÄledes har vi behandlat nÄgra aspekter av optimeringen av LINQ-frÄgor mot MS SQL Server.

TyvÀrr glömmer till och med erfarna och ledande .NET-utvecklare att de mÄste förstÄ vad som hÀnder bakom kulisserna av de instruktioner de anvÀnder. Annars blir de endast konfiguratörer och kan lÀgga en tidsinstÀlld bomb för framtiden bÄde vid skalning av programlösningen och vid mindre förÀndringar i miljöns externa förhÄllanden.

En liten översikt gjordes ocksÄ hÀr.

KĂ€llkoden för testet — sjĂ€lva projektet, skapandet av tabeller i databasen TEST, samt ifyllandet av dessa tabeller finns hĂ€r.
I detta arkiv finns Àven en mapp som heter Plans dÀr planerna för att utföra förfrÄgningar med OCH-villkor finns.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster