Kelkaj aspektoj de optimumigo de LINQ-demandoj en C#.NET por MS SQL Server

LINQ eniris .NET kiel potenca nova datummanipula lingvo. LINQ al SQL kiel parto de ĝi permesas vin sufiĉe oportune komuniki kun DBMS uzante, ekzemple, Entity Framework. Tamen, uzante ĝin sufiĉe ofte, programistoj forgesas rigardi kian SQL-demandon la demandebla provizanto, en via kazo Entity Framework, generos.

Ni rigardu du ĉefajn punktojn uzante ekzemplon.
Por fari tion, kreu Testan datumbazon en SQL-Servilo, kaj kreu du tabelojn en ĝi uzante la jenan demandon:

Kreante tabelojn

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

Nun ni plenigu la Ref-tabelon rulante la sekvan skripton:

Plenigante la Ref-tabelon

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

Ni simile plenigu la tabelon de Kliento uzante la jenan skripton:

Plenigante la tabelon de Kliento

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

Tiel, ni ricevis du tabelojn, unu el kiuj havas pli ol 1 milionon da vicoj da datumoj, kaj la alia havas pli ol 10 milionojn da vicoj da datumoj.

Nun en Visual Studio vi devas krei provan projekton de Visual C# Console App (.NET Framework):

Kelkaj aspektoj de optimumigo de LINQ-demandoj en C#.NET por MS SQL Server

Poste, vi devas aldoni bibliotekon por la Enta Kadro por interagi kun la datumbazo.
Por aldoni ĝin, dekstre alklaku la projekton kaj elektu Administri NuGet-Pakojn el la kunteksta menuo:

Kelkaj aspektoj de optimumigo de LINQ-demandoj en C#.NET por MS SQL Server

Poste, en la fenestro de administrado de pakaĵoj de NuGet, kiu aperas, enigu la vorton "Entity Framework" en la serĉfenestron kaj elektu la pakaĵon Entity Framework kaj instalu ĝin:

Kelkaj aspektoj de optimumigo de LINQ-demandoj en C#.NET por MS SQL Server

Poste, en la dosiero App.config, post fermo de la elemento configSections, vi devas aldoni la sekvan blokon:

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

En connectionString vi devas enigi la konektan ĉenon.

Nun ni kreu 3 interfacojn en apartaj dosieroj:

  1. Efektivigo de la IBaseEntityID-interfaco
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Efektivigo de la IBaseEntityName-interfaco
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Efektivigo de la IBaseNameInsertUTCDate-interfaco
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

Kaj en aparta dosiero ni kreos bazan klason BaseEntity por niaj du estaĵoj, kiuj inkluzivos komunajn kampojn:

Efektivigo de la baza klaso BaseEntity

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

Poste, ni kreos niajn du entojn en apartaj dosieroj:

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

  2. Efektivigo de la Klaso Kliento
    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; }
        }
    }
    

Nun ni kreu kuntekston de UserContext en aparta dosiero:

Efektivigo de la UserContex klaso

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

Ni ricevis pretan solvon por fari optimumigajn testojn kun LINQ to SQL per EF por MS SQL Server:

Kelkaj aspektoj de optimumigo de LINQ-demandoj en C#.NET por MS SQL Server

Nun enigu la sekvan kodon en la dosieron Program.cs:

Program.cs dosiero

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

Poste, ni lanĉu nian projekton.

Ĉe la fino de la laboro, la jena aperos sur la konzolo:

Generita SQL-Demando

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

Tio estas, ĝenerale, la LINQ-demando generis SQL-demandon al la MS SQL Server DBMS sufiĉe bone.

Nun ni ŝanĝu la AND-kondiĉon al AŬ en la LINQ-demando:

LINQ-demando

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

Kaj ni lanĉu nian aplikaĵon denove.

La ekzekuto kraŝos kun eraro pro la komanda ekzekuttempo superanta 30 sekundojn:

Kelkaj aspektoj de optimumigo de LINQ-demandoj en C#.NET por MS SQL Server

Se vi rigardas la demandon, kiu estis generita de LINQ:

Kelkaj aspektoj de optimumigo de LINQ-demandoj en C#.NET por MS SQL Server
, tiam vi povas certigi, ke la elekto okazas per la kartezia produkto de du aroj (tabeloj):

Generita SQL-Demando

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]

Ni reverku la LINQ-demandon jene:

Optimumigita LINQ-demando

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

Tiam ni ricevas la sekvan SQL-demandon:

SQL-demando

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]

Ve, en LINQ-demandoj povas esti nur unu kunigkondiĉo, do ĉi tie eblas fari ekvivalentan demandon uzante du demandojn por ĉiu kondiĉo kaj poste kombinante ilin per Union por forigi duplikatojn inter la vicoj.
Jes, la demandoj ĝenerale estos ne-ekvivalentaj, konsiderante ke kompletaj duplikataj vicoj povas esti resenditaj. Tamen, en la reala vivo, kompletaj duobligitaj linioj ne estas bezonataj kaj homoj provas forigi ilin.

Nun ni komparu la ekzekutplanojn de ĉi tiuj du demandoj:

  1. por CROSS JOIN la averaĝa ekzekuttempo estas 195 sekundoj:
    Kelkaj aspektoj de optimumigo de LINQ-demandoj en C#.NET por MS SQL Server
  2. por INNER JOIN-UNION la averaĝa ekzekuttempo estas malpli ol 24 sekundoj:
    Kelkaj aspektoj de optimumigo de LINQ-demandoj en C#.NET por MS SQL Server

Kiel vi povas vidi el la rezultoj, por du tabeloj kun milionoj da rekordoj, la optimumigita LINQ-demando estas multajn fojojn pli rapida ol la neoptimumigita.

Por la opcio kun AND en la kondiĉoj, LINQ-demando de la formo:

LINQ-demando

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

La ĝusta SQL-demando preskaŭ ĉiam estos generita, kiu funkcios averaĝe en ĉirkaŭ 1 sekundo:

Kelkaj aspektoj de optimumigo de LINQ-demandoj en C#.NET por MS SQL Server
Ankaŭ por manipuladoj de LINQ to Objects anstataŭ demando kiel:

Demando LINQ (unua opcio)

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

vi povas uzi demandon kiel:

Demando LINQ (unua opcio)

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

kie:

Difinante du tabelojn

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

, kaj la Para-tipo estas difinita jene:

Para Tipo Difino

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

Tiel, ni ekzamenis kelkajn aspektojn en optimumigado de LINQ-demandoj al MS SQL Server.

Bedaŭrinde, eĉ spertaj kaj gvidaj .NET-programistoj forgesas, ke ili devas kompreni, kion faras la instrukcioj, kiujn ili uzas, malantaŭ la scenoj. Alie, ili fariĝas agordiloj kaj povas planti horloĝbombon en la estonteco kaj dum grimpado de la programara solvo kaj kun etaj ŝanĝoj en eksteraj mediaj kondiĉoj.

Mallonga revizio ankaŭ estis farita tie.

La fontoj por la testo - la projekto mem, la kreado de tabeloj en la TEST-datumbazo, same kiel plenigi ĉi tiujn tabelojn per datumoj troviĝas. tie.
Ankaŭ en ĉi tiu deponejo, en la dosierujo Planoj, estas planoj por efektivigi demandojn kun AŬ kondiĉoj.

fonto: www.habr.com

Aldoni komenton