Některé aspekty optimalizace dotazů LINQ v C#.NET pro MS SQL Server

LINQ vstoupil do .NET jako nový výkonný jazyk pro manipulaci s daty. LINQ to SQL jako jeho součást umožňuje poměrně pohodlně komunikovat s DBMS pomocí např. Entity Framework. Při jeho poměrně častém používání se však vývojáři zapomínají dívat na to, jaký SQL dotaz dotazovatelný poskytovatel vygeneruje, ve vašem případě Entity Framework.

Podívejme se na dva hlavní body s příkladem.
Za tímto účelem v SQL Server vytvoříme databázi Test a v ní vytvoříme dvě tabulky pomocí následujícího dotazu:

Vytvoření tabulky

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

Nyní naplníme tabulku Ref spuštěním následujícího skriptu:

Vyplnění tabulky č.j

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

Vyplňte tabulku Zákazník stejným způsobem pomocí následujícího skriptu:

Vyplnění tabulky 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

Získali jsme tedy dvě tabulky, z nichž jedna má více než 1 milion řádků dat a druhá má více než 10 milionů řádků dat.

Nyní ve Visual Studiu musíte vytvořit testovací projekt Visual C# Console App (.NET Framework):

Některé aspekty optimalizace dotazů LINQ v C#.NET pro MS SQL Server

Dále musíte přidat knihovnu pro Entity Framework pro interakci s databází.
Chcete-li jej přidat, klikněte pravým tlačítkem na projekt a z kontextové nabídky vyberte Spravovat balíčky NuGet:

Některé aspekty optimalizace dotazů LINQ v C#.NET pro MS SQL Server

Poté v okně správy balíčků NuGet, které se zobrazí, zadejte do vyhledávacího pole slovo „Entity Framework“ a vyberte balíček Entity Framework a nainstalujte jej:

Některé aspekty optimalizace dotazů LINQ v C#.NET pro MS SQL Server

Dále do souboru App.config po zavření prvku configSections přidejte následující blok:

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

Do connectionString musíte zadat připojovací řetězec.

Nyní vytvoříme 3 rozhraní v samostatných souborech:

  1. Implementace rozhraní IBaseEntityID
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Implementace rozhraní IBaseEntityName
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Implementace rozhraní IBaseNameInsertUTCDate
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

A v samostatném souboru vytvoříme základní třídu BaseEntity pro naše dvě entity, která bude obsahovat společná pole:

Implementace základní třídy 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ále v samostatných souborech vytvoříme naše dvě entity:

  1. Implementace referenční třídy
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace TestLINQ
    {
        [Table("Ref")]
        public class Ref : BaseEntity
        {
            public int ID2 { get; set; }
        }
    }
    

  2. Implementace třídy 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; }
        }
    }
    

Nyní vytvoříme kontext UserContext v samostatném souboru:

Implementace třídy 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; }
    }
}

Získali jsme hotové řešení pro provádění optimalizačních testů s LINQ to SQL přes EF pro MS SQL Server:

Některé aspekty optimalizace dotazů LINQ v C#.NET pro MS SQL Server

Nyní do souboru Program.cs zadejte následující kód:

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

Dále spusťte náš projekt.

Na konci práce se na konzole zobrazí následující:

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 obecně dotaz LINQ generoval SQL dotaz do MS SQL Server DBMS docela dobře.

Nyní změňme podmínku AND na OR v dotazu 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 };

Spusťte naši aplikaci znovu.

Provádění se zhroutí s chybou související s dobou provádění příkazu přesahující 30 sekund:

Některé aspekty optimalizace dotazů LINQ v C#.NET pro MS SQL Server

Pokud se podíváte na to, který dotaz vygeneroval LINQ:

Některé aspekty optimalizace dotazů LINQ v C#.NET pro MS SQL Server
, pak se můžete ujistit, že výběr probíhá prostřednictvím kartézského součinu dvou množin (tabulek):

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]

Přepiš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 });

Poté dostaneme následující 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žel, v dotazech LINQ může být pouze jedna podmínka spojení, proto je možné vytvořit ekvivalentní dotaz prostřednictvím dvou dotazů pro každou podmínku, po kterém následuje jejich sjednocení prostřednictvím Union, aby se odstranily duplikáty mezi řádky.
Ano, dotazy budou obecně neekvivalentní, protože mohou být vráceny úplné duplicitní řádky. V reálném životě však nejsou úplné duplicitní řádky potřeba a snaží se jich zbavit.

Nyní porovnejme plány provádění těchto dvou dotazů:

  1. pro CROSS JOIN je průměrná doba provedení 195 sekund:
    Některé aspekty optimalizace dotazů LINQ v C#.NET pro MS SQL Server
  2. pro INNER JOIN-UNION je průměrná doba provedení kratší než 24 sekund:
    Některé aspekty optimalizace dotazů LINQ v C#.NET pro MS SQL Server

Jak je z výsledků patrné, pro dvě tabulky s miliony záznamů je optimalizovaný LINQ dotaz mnohonásobně rychlejší než neoptimalizovaný.

Pro variantu s AND v podmínkách LINQ dotazu formuláře:

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

téměř vždy se vygeneruje správný SQL dotaz, který poběží v průměru asi 1 sekundu:

Některé aspekty optimalizace dotazů LINQ v C#.NET pro MS SQL Server
Také pro manipulace LINQ to Objects namísto dotazování na pohled:

LINQ dotaz (1. možnost)

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žít dotaz jako:

LINQ dotaz (2. možnost)

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:

Definování dvou 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 definován takto:

Definice typu para

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

Zvážili jsme tedy některé aspekty při optimalizaci LINQ dotazů na MS SQL Server.

Bohužel i zkušení a přední .NET vývojáři zapomínají, že je nutné rozumět tomu, co návody, které používají, dělají v zákulisí. V opačném případě se z nich stanou konfigurátory a mohou v budoucnu položit časovanou bombu, a to jak při škálování softwarového řešení, tak při drobných změnách vnějších podmínek prostředí.

Nechyběla ani malá recenze zde.

Zdroje pro test - samotný projekt, vytváření tabulek v databázi TEST, stejně jako plnění těchto tabulek daty se nachází zde.
Také v tomto úložišti ve složce Plány jsou plány pro provádění dotazů s podmínkami NEBO.

Zdroj: www.habr.com

Přidat komentář