Niektoré aspekty optimalizácie LINQ dotazov v C#.NET pre MS SQL Server

LINQ vstúpil do .NET ako nový výkonný jazyk na manipuláciu s údajmi. LINQ to SQL ako jeho súčasť umožňuje celkom pohodlne komunikovať s DBMS napríklad pomocou Entity Framework. Pri jeho pomerne častom používaní sa však vývojári zabúdajú pozerať na to, aký SQL dotaz vygeneruje dopytovateľný poskytovateľ, vo vašom prípade Entity Framework.

Na príklade sa pozrime na dva hlavné body.
Ak to chcete urobiť, vytvorte testovaciu databázu na serveri SQL Server a vytvorte v nej dve tabuľky pomocou nasledujúceho dotazu:

Vytváranie tabuliek

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

Teraz vyplňte tabuľku Ref spustením nasledujúceho skriptu:

Vyplnenie tabuľky 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

Podobne vyplníme tabuľku Zákazník pomocou nasledujúceho skriptu:

Vypĺňa sa tabuľka Zákazník

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

Dostali sme teda dve tabuľky, z ktorých jedna má viac ako 1 milión riadkov údajov a druhá má viac ako 10 miliónov riadkov údajov.

Teraz vo Visual Studiu musíte vytvoriť testovací projekt Visual C# Console App (.NET Framework):

Niektoré aspekty optimalizácie LINQ dotazov v C#.NET pre MS SQL Server

Ďalej musíte pridať knižnicu pre Entity Framework na interakciu s databázou.
Ak ho chcete pridať, kliknite pravým tlačidlom myši na projekt a v kontextovej ponuke vyberte položku Spravovať balíky NuGet:

Niektoré aspekty optimalizácie LINQ dotazov v C#.NET pre MS SQL Server

Potom v okne správy balíkov NuGet, ktoré sa zobrazí, zadajte do vyhľadávacieho okna slovo „Entity Framework“ a vyberte balík Entity Framework a nainštalujte ho:

Niektoré aspekty optimalizácie LINQ dotazov v C#.NET pre MS SQL Server

Ďalej, v súbore App.config, po zatvorení prvku configSections, musíte pridať nasledujúci blok:

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

V connectionString musíte zadať reťazec pripojenia.

Teraz vytvorte 3 rozhrania v samostatných súboroch:

  1. Implementácia rozhrania IBaseEntityID
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Implementácia rozhrania IBaseEntityName
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Implementácia rozhrania IBaseNameInsertUTCDate
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

A v samostatnom súbore vytvoríme základnú triedu BaseEntity pre naše dve entity, ktorá bude obsahovať spoločné polia:

Implementácia základnej triedy BaseEntity

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

Ďalej vytvoríme naše dve entity v samostatných súboroch:

  1. Implementácia triedy Ref
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace TestLINQ
    {
        [Table("Ref")]
        public class Ref : BaseEntity
        {
            public int ID2 { get; set; }
        }
    }
    

  2. Implementácia triedy Zákazník
    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; }
        }
    }
    

Teraz vytvorte kontext UserContext v samostatnom súbore:

Implementácia triedy UserContex

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

Dostali sme hotové riešenie na vykonávanie optimalizačných testov s LINQ to SQL cez EF pre MS SQL Server:

Niektoré aspekty optimalizácie LINQ dotazov v C#.NET pre MS SQL Server

Teraz zadajte nasledujúci kód do súboru Program.cs:

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

Ďalej spustíme náš projekt.

Na konci práce sa na konzole zobrazí nasledovné:

Generovaný SQL dotaz

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

To znamená, že vo všeobecnosti dotaz LINQ vygeneroval dotaz SQL do MS SQL Server DBMS celkom dobre.

Teraz zmeňme podmienku AND na OR v dotaze LINQ:

LINQ dotaz

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

A opäť spustíme našu aplikáciu.

Vykonávanie zlyhá s chybou v dôsledku času vykonávania príkazu presahujúceho 30 sekúnd:

Niektoré aspekty optimalizácie LINQ dotazov v C#.NET pre MS SQL Server

Ak sa pozriete na dotaz, ktorý vygeneroval LINQ:

Niektoré aspekty optimalizácie LINQ dotazov v C#.NET pre MS SQL Server
, potom sa môžete uistiť, že výber prebieha prostredníctvom karteziánskeho súčinu dvoch množín (tabuľiek):

Generovaný SQL dotaz

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]

Prepíšme dotaz LINQ takto:

Optimalizovaný dotaz LINQ

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

Potom dostaneme nasledujúci SQL dotaz:

SQL dotaz

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]

Bohužiaľ, v dotazoch LINQ môže existovať iba jedna podmienka spojenia, takže tu je možné vytvoriť ekvivalentný dotaz pomocou dvoch dotazov pre každú podmienku a potom ich skombinovať cez Union, aby sa odstránili duplikáty medzi riadkami.
Áno, dopyty budú vo všeobecnosti neekvivalentné, berúc do úvahy, že sa môžu vrátiť úplné duplicitné riadky. V reálnom živote však nie sú potrebné úplné duplicitné čiary a ľudia sa ich snažia zbaviť.

Teraz porovnajme plány vykonávania týchto dvoch dotazov:

  1. pre CROSS JOIN je priemerný čas vykonania 195 sekúnd:
    Niektoré aspekty optimalizácie LINQ dotazov v C#.NET pre MS SQL Server
  2. pre INNER JOIN-UNION je priemerný čas vykonania menej ako 24 sekúnd:
    Niektoré aspekty optimalizácie LINQ dotazov v C#.NET pre MS SQL Server

Ako môžete vidieť z výsledkov, pre dve tabuľky s miliónmi záznamov je optimalizovaný dotaz LINQ mnohonásobne rýchlejší ako ten neoptimalizovaný.

Pre možnosť s AND v podmienkach dopyt LINQ vo formulári:

LINQ dotaz

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

Takmer vždy sa vygeneruje správny SQL dotaz, ktorý sa spustí v priemere asi za 1 sekundu:

Niektoré aspekty optimalizácie LINQ dotazov v C#.NET pre MS SQL Server
Tiež pre manipulácie LINQ to Objects namiesto dotazu, ako napríklad:

LINQ dotaz (1. možnosť)

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

môžete použiť dotaz ako:

LINQ dotaz (2. možnosť)

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

kde:

Definovanie dvoch polí

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

a typ Para je definovaný takto:

Definícia typu para

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

Preto sme preskúmali niektoré aspekty pri optimalizácii LINQ dotazov na MS SQL Server.

Bohužiaľ, aj skúsení a poprední .NET vývojári zabúdajú, že musia rozumieť tomu, čo inštrukcie, ktoré používajú, robia v zákulisí. V opačnom prípade sa stanú konfigurátormi a môžu v budúcnosti zasadiť časovanú bombu ako pri škálovaní softvérového riešenia, tak aj pri menších zmenách vonkajších podmienok prostredia.

Uskutočnil sa aj krátky prehľad tu.

Zdroje pre test - samotný projekt, vytváranie tabuliek v databáze TEST, ako aj napĺňanie týchto tabuliek údajmi sa nachádzajú tu.
Aj v tomto úložisku sa v priečinku Plány nachádzajú plány na vykonávanie dotazov s podmienkami ALEBO.

Zdroj: hab.com

Pridať komentár