Nogle aspekter af optimering af LINQ-forespørgsler i C#.NET til MS SQL Server

LINQ blev en del af .NET som et nyt kraftfuldt sprog til datamanipulation. LINQ to SQL tillader, som en del af det, en bekvem kommunikation med databaser, for eksempel ved hjælp af Entity Framework. Dog glemmer udviklere ofte at tjekke, hvilken SQL-forespørgsel der vil blive genereret af den queryable provider, i dit tilfælde — Entity Framework.

Lad os se på to hovedpunkter med et eksempel.
For dette opretter vi en database kaldet Test i SQL Server, og vi opretter to tabeller ved hjælp af følgende forespørgsel:

Oprettelse af 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 fylder vi tabellen Ref ved at køre følgende script:

Udfyldning af 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

Tilsvarende fylder vi tabellen Customer ved hjælp af følgende script:

Udfyldning af tabellen Customer

BRUG [TEST]
GÅ

DEKLARER @ind INT=1;
DEKLARER @ind_ref INT=1;

MENS(@ind<=12000000)
BEGIN
	HVIS(@ind%3=0) SÆT @ind_ref=1;
	ELLER HVIS (@ind%5=0) SÆT @ind_ref=2;
	ELLER HVIS (@ind%7=0) SÆT @ind_ref=3;
	ELLER HVIS (@ind=0) SÆT @ind_ref=4;
	ELLER HVIS (@ind=0) SÆT @ind_ref=5;
	ELLER HVIS (@ind=0) SÆT @ind_ref=6;
	ELLER HVIS (@ind=0) SÆT @ind_ref=7;
	ELLER HVIS (@ind=0) SÆT @ind_ref=8;
	ELLER HVIS (@ind=0) SÆT @ind_ref=9;
	ELLER HVIS (@ind=0) SÆT @ind_ref=10;
	ELLER HVIS (@ind=0) SÆT @ind_ref=11;
	ELLER SÆT @ind_ref=@ind90000;
	
	INDSTIK [dbo].[Customer]
	 ([ID]
	 ,[Navn]
	 ,[Ref_ID]
	 ,[Ref_ID2])
	 VÆLG
	 @ind,
	 CAST(@ind AS NVARCHAR(255)),
	 @ind_ref,
	 @ind_ref;


	SÆT @ind=@ind+1;
END
GÅ

Dermed har vi fået to tabeller, hvoraf den ene indeholder mere end 1 million datarækker, og den anden over 10 millioner datarækker.

Nu skal der oprettes et testprojekt i Visual Studio: Visual C# Console App (.NET Framework):

Nogle aspekter af optimering af LINQ-forespørgsler i C#.NET til MS SQL Server

Dernæst skal der tilføjes et bibliotek til Entity Framework for at interagere med databasen.
For at tilføje det, højreklik på projektet og vælg 'Administrer NuGet-pakker' i kontekstmenuen:

Nogle aspekter af optimering af LINQ-forespørgsler i C#.NET til MS SQL Server

Skriv derefter 'Entity Framework' i søgefeltet i det åbne NuGet-pakkehåndteringsvindue, vælg pakken Entity Framework og installer den:

Nogle aspekter af optimering af LINQ-forespørgsler i C#.NET til MS SQL Server

Derefter, i App.config-filen, efter at have lukket elementet configSections, skal du tilføje følgende blok:


I connectionString skal forbindelsesstrengen skrives ind.

Lad os nu oprette tre grænseflader i separate filer:

  1. Implementering af grænsefladen IBaseEntityID
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Implementering af grænsefladen IBaseEntityName
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Implementering af grænsefladen IBaseNameInsertUTCDate
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

Og i en separat fil opretter vi grundklassen BaseEntity for vores to enheder, som vil inkludere fælles felter:

Implementering af grundklassen BaseEntity

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

Derefter, i separate filer, opretter vi vores to enheder:

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

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

Lad os nu oprette konteksten UserContext i en separat fil:

Implementering af 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 nu en klar løsning til at udføre tests for optimering med LINQ til SQL via EF for MS SQL Server:

Nogle aspekter af optimering af LINQ-forespørgsler i C#.NET til MS SQL Server

Lad os nu indsætte følgende kode i Program.cs-filen:

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

Lad os køre vores projekt.

Ved afslutningen af arbejdet vil der blive vist:

Den genererede SQL-forespørgsel

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 vil sige, at LINQ-forespørgslen genererede en ret acceptabel SQL-forespørgsel til MS SQL Server.

Lad os nu ændre betingelsen fra OG til ELLER i LINQ-forespørgslen:

LINQ-forespørgsel

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

Og lad os igen køre vores applikation.

Udførelsen vil fejle med en fejl relateret til overskridelse af timeout for kommandoen på 30 sekunder:

Nogle aspekter af optimering af LINQ-forespørgsler i C#.NET til MS SQL Server

Hvis vi ser på hvilken forespørgsel der blev genereret af LINQ:

Nogle aspekter af optimering af LINQ-forespørgsler i C#.NET til MS SQL Server
, kan vi bekræfte, at udtrækningen sker gennem den cartesisk produkt af to mængder (tabeller):

Den genererede SQL-forespørgsel

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]

Lad os omskrive LINQ-forespørgslen som følger:

Optimeret LINQ-forespørgsel

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

Så vil vi få følgende SQL-forespørgsel:

SQL-forespørgsel

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]

Desværre kan der kun være én betingelse for sammenkobling i LINQ-forespørgsler, så det er muligt at lave en ækvivalent forespørgsel gennem to forespørgsler for hver betingelse med efterfølgende sammenføjning via Union for at fjerne dubletter blandt rækkerne.
Ja, forespørgslerne vil generelt set være ikke-ækvivalente, da der kan returneres fuldstændige dubletter af rækker. I virkeligheden er fuldstændige dublerende rækker dog ikke nødvendige, og man forsøger at slippe af med dem.

Lad os nu sammenligne udførelsesplanerne for disse to forespørgsler:

  1. for CROSS JOIN er den gennemsnitlige udførelsestid 195 sekunder:
    Nogle aspekter af optimering af LINQ-forespørgsler i C#.NET til MS SQL Server
  2. for INNER JOIN-UNION er den gennemsnitlige udførelsestid mindre end 24 sekunder:
    Nogle aspekter af optimering af LINQ-forespørgsler i C#.NET til MS SQL Server

Som det fremgår af resultaterne, fungerer den optimerede LINQ-forespørgsel med to tabeller med millioner af poster betydeligt hurtigere end den ikke-optimerede.

For varianten med AND i betingelserne for LINQ-forespørgslen:

LINQ-forespørgsel

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

vil næsten altid generere den korrekte SQL-forespørgsel, som i gennemsnit vil blive udført på cirka 1 sekund:

Nogle aspekter af optimering af LINQ-forespørgsler i C#.NET til MS SQL Server
Også for manipulationer af LINQ til objekter i stedet for forespørgslen:

LINQ-forespørgsel (1. variant)

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 man bruge forespørgslen:

LINQ-forespørgsel (2. variant)

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

hvor:

Definition af to arrays

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

, og typen Para defineres som følger:

Definition af typen Para

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

Dermed har vi set nogle aspekter af optimering af LINQ-forespørgsler til MS SQL Server.

Desværre glemmer selv erfarne og førende .NET-udviklere, at de skal forstå, hvad der sker bag kulisserne i de instruktioner, de bruger. Ellers bliver de konfigurerere og kan lægge en tidsindstillet bombe til fremtidige problemer, både ved skalering af softwareløsningen og ved mindre ændringer i miljøets ydre forhold.

Der blev også udført en lille gennemgang og her.

Kildematerialerne til test-projektet, oprettelsen af tabeller i databasen TEST, samt udfyldningen af disse tabeller findes her.
I dette repo i mappen Plans findes planer for udførelse af forespørgsler med betingelser ELLER.

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster