Några aspekter av optimering av LINQ-frågor i C#.NET för MS SQL Server

LINQ gick in i .NET som ett kraftfullt nytt datamanipuleringsspråk. LINQ till SQL som en del av det låter dig kommunicera ganska bekvämt med ett DBMS med hjälp av till exempel Entity Framework. Men med hjälp av det ganska ofta glömmer utvecklare att titta på vilken typ av SQL-fråga den frågebara leverantören, i ditt fall Entity Framework, kommer att generera.

Låt oss titta på två huvudpunkter med hjälp av ett exempel.
För att göra detta, skapa en testdatabas i SQL Server och skapa två tabeller i den med hjälp av följande fråga:

Skapa 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

Låt oss nu fylla i Ref-tabellen genom att köra följande skript:

Fyller Ref tabellen

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

Låt oss på samma sätt fylla Kundtabellen med följande skript:

Fyller i kundtabellen

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

Således fick vi två tabeller, varav den ena har mer än 1 miljon rader med data och den andra har mer än 10 miljoner rader med data.

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

Några aspekter av optimering av LINQ-frågor i C#.NET för MS SQL Server

Därefter måste du lägga till ett bibliotek för att Entity Framework ska kunna interagera med databasen.
För att lägga till det, högerklicka på projektet och välj Hantera NuGet-paket från snabbmenyn:

Några aspekter av optimering av LINQ-frågor i C#.NET för MS SQL Server

Sedan, i NuGet-pakethanteringsfönstret som visas, skriv in ordet "Entity Framework" i sökfönstret och välj Entity Framework-paketet och installera det:

Några aspekter av optimering av LINQ-frågor i C#.NET för MS SQL Server

Därefter, i filen App.config, efter att ha stängt configSections-elementet, måste du lägga till följande block:

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

I connectionString måste du ange anslutningssträngen.

Låt oss nu skapa 3 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 kommer vi att skapa en basklass BaseEntity för våra två enheter, som kommer att inkludera vanliga fält:

Implementering av basklassen BaseEntity

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

Därefter kommer vi att skapa våra två enheter i separata filer:

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

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

Låt oss nu skapa en UserContext-kontext i en separat fil:

Implementering av UserContex-klassen

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

Vi fick en färdig lösning för att genomföra optimeringstester med LINQ till SQL via EF för MS SQL Server:

Några aspekter av optimering av LINQ-frågor i C#.NET för MS SQL Server

Ange nu följande kod i filen Program.cs:

Program.cs-filen

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

Låt oss sedan lansera vårt projekt.

I slutet av arbetet kommer följande att visas på 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, i allmänhet genererade LINQ-frågan en SQL-fråga till MS SQL Server DBMS ganska bra.

Låt oss nu ändra AND-villkoret till OR i LINQ-frågan:

LINQ-fråga

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 låt oss starta vår applikation igen.

Körningen kommer att krascha med ett fel på grund av att kommandots körtid överstiger 30 sekunder:

Några aspekter av optimering av LINQ-frågor i C#.NET för MS SQL Server

Om du tittar på frågan som genererades av LINQ:

Några aspekter av optimering av LINQ-frågor i C#.NET för MS SQL Server
, då kan du se till att urvalet sker genom den kartesiska produkten av två uppsättningar (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 enligt följande:

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, i LINQ-frågor kan det bara finnas ett kopplingsvillkor, så här är det möjligt att göra en likvärdig fråga med två frågor för varje villkor och sedan kombinera dem genom Union för att ta bort dubbletter bland raderna.
Ja, frågorna kommer i allmänhet inte att vara likvärdiga, med hänsyn till att fullständiga dubbletter av rader kan returneras. Men i verkliga livet behövs inte kompletta dubbletter av linjer och människor försöker bli av med dem.

Låt oss nu jämföra genomförandeplanerna för dessa två frågor:

  1. för CROSS JOIN är den genomsnittliga exekveringstiden 195 sekunder:
    Några 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 sekunder:
    Några aspekter av optimering av LINQ-frågor i C#.NET för MS SQL Server

Som du kan se av resultaten, för två tabeller med miljontals poster, är den optimerade LINQ-frågan många gånger snabbare än den ooptimerade.

För alternativet med OCH i villkoren, en LINQ-fråga i formen:

LINQ-fråga

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

Den korrekta SQL-frågan kommer nästan alltid att genereras, som körs i genomsnitt på cirka 1 sekund:

Några aspekter av optimering av LINQ-frågor i C#.NET för MS SQL Server
Även för LINQ to Objects-manipulationer istället för en fråga som:

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

du kan använda en fråga som:

LINQ-fråga (första 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:

Definiera 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 Para-typen definieras enligt följande:

Para Type Definition

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

Därför undersökte vi några aspekter av att optimera LINQ-frågor till MS SQL Server.

Tyvärr glömmer även erfarna och ledande .NET-utvecklare att de måste förstå vad instruktionerna de använder gör bakom kulisserna. Annars blir de konfiguratorer och kan plantera en tidsinställd bomb i framtiden både vid skalning av mjukvarulösningen och med mindre förändringar i yttre miljöförhållanden.

En kort genomgång gjordes också här.

Källorna för testet - själva projektet, skapandet av tabeller i TEST-databasen, samt att fylla dessa tabeller med data finns här.
Även i det här arkivet, i mappen Planer, finns det planer på att köra frågor med OR-villkor.

Källa: will.com

Lägg en kommentar