Неки аспекти оптимизације ЛИНК упита у Ц#.НЕТ-у за МС СКЛ Сервер

ЛИНК је ушао у .НЕТ као моћан нови језик за манипулацију подацима. ЛИНК то СКЛ као његов део омогућава вам да прилично згодно комуницирате са ДБМС користећи, на пример, Ентити Фрамеворк. Међутим, користећи га прилично често, програмери заборављају да погледају какву ће врсту СКЛ упита генерисати провајдер за који се може поставити упит, у вашем случају Ентити Фрамеворк.

Погледајмо две главне тачке користећи пример.
Да бисте то урадили, креирајте тест базу података у СКЛ Сервер-у и креирајте две табеле у њој користећи следећи упит:

Креирање табела

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

Сада попунимо табелу Реф покретањем следеће скрипте:

Попуњавање табеле Реф

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

Хајде да на сличан начин попунимо табелу купаца користећи следећу скрипту:

Попуњавање табеле клијената

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

Тако смо добили две табеле, од којих једна има више од милион редова података, а друга више од 1 милиона редова података.

Сада у Висуал Студио-у морате да направите пробни пројекат Висуал Ц# Цонсоле Апп (.НЕТ Фрамеворк):

Неки аспекти оптимизације ЛИНК упита у Ц#.НЕТ-у за МС СКЛ Сервер

Затим морате додати библиотеку за Ентити Фрамеворк за интеракцију са базом података.
Да бисте га додали, кликните десним тастером миша на пројекат и изаберите Управљање НуГет пакетима из контекстног менија:

Неки аспекти оптимизације ЛИНК упита у Ц#.НЕТ-у за МС СКЛ Сервер

Затим, у прозору за управљање пакетом НуГет који се појави, унесите реч „Ентити Фрамеворк“ у прозор за претрагу и изаберите Ентити Фрамеворк пакет и инсталирајте га:

Неки аспекти оптимизације ЛИНК упита у Ц#.НЕТ-у за МС СКЛ Сервер

Затим, у датотеци Апп.цонфиг, након затварања елемента цонфигСецтионс, потребно је да додате следећи блок:

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

У цоннецтионСтринг треба да унесете низ везе.

Сада направимо 3 интерфејса у одвојеним датотекама:

  1. Имплементација интерфејса ИБасеЕнтитиИД
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Имплементација интерфејса ИБасеЕнтитиНаме
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Имплементација интерфејса ИБасеНамеИнсертУТЦДате
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

И у посебној датотеци креираћемо основну класу БасеЕнтити за наша два ентитета, која ће укључивати заједничка поља:

Имплементација основне класе БасеЕнтити

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

Затим ћемо креирати наша два ентитета у одвојеним датотекама:

  1. Имплементација класе Реф
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace TestLINQ
    {
        [Table("Ref")]
        public class Ref : BaseEntity
        {
            public int ID2 { get; set; }
        }
    }
    

  2. Имплементација класе Купац
    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; }
        }
    }
    

Сада направимо контекст УсерЦонтект у посебној датотеци:

Имплементација класе УсерЦонтек

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

Добили смо готово решење за спровођење тестова оптимизације са ЛИНК то СКЛ преко ЕФ-а за МС СКЛ Сервер:

Неки аспекти оптимизације ЛИНК упита у Ц#.НЕТ-у за МС СКЛ Сервер

Сада унесите следећи код у датотеку Програм.цс:

Програм.цс датотека

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

Затим, хајде да покренемо наш пројекат.

На крају рада, на конзоли ће бити приказано следеће:

Генерисани СКЛ упит

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

То јест, генерално, ЛИНК упит је прилично добро генерисао СКЛ упит за МС СКЛ Сервер ДБМС.

Сада променимо услов АНД у ОР у ЛИНК упиту:

ЛИНК упит

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

И хајде да поново покренемо нашу апликацију.

Извршење ће се срушити са грешком због времена извршења команде дуже од 30 секунди:

Неки аспекти оптимизације ЛИНК упита у Ц#.НЕТ-у за МС СКЛ Сервер

Ако погледате упит који је генерисао ЛИНК:

Неки аспекти оптимизације ЛИНК упита у Ц#.НЕТ-у за МС СКЛ Сервер
, онда можете да се уверите да се избор врши преко картезијанског производа два скупа (табела):

Генерисани СКЛ упит

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]

Хајде да поново напишемо ЛИНК упит на следећи начин:

Оптимизовани ЛИНК упит

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

Затим добијамо следећи СКЛ упит:

СКЛ упит

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]

Нажалост, у ЛИНК упитима може постојати само један услов спајања, тако да је овде могуће направити еквивалентан упит користећи два упита за сваки услов, а затим их комбиновати кроз Унију да бисте уклонили дупликате међу редовима.
Да, упити ће генерално бити нееквивалентни, узимајући у обзир да се комплетни дуплирани редови могу вратити. Међутим, у стварном животу, потпуне дупле линије нису потребне и људи покушавају да их се отарасе.

Хајде сада да упоредимо планове извршења ова два упита:

  1. за ЦРОСС ЈОИН просечно време извршења је 195 секунди:
    Неки аспекти оптимизације ЛИНК упита у Ц#.НЕТ-у за МС СКЛ Сервер
  2. за ИННЕР ЈОИН-УНИОН просечно време извршења је мање од 24 секунде:
    Неки аспекти оптимизације ЛИНК упита у Ц#.НЕТ-у за МС СКЛ Сервер

Као што видите из резултата, за две табеле са милионима записа, оптимизовани ЛИНК упит је много пута бржи од неоптимизованог.

За опцију са И у условима, ЛИНК упит у облику:

ЛИНК упит

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

Скоро увек ће се генерисати исправан СКЛ упит, који ће се покренути у просеку за око 1 секунду:

Неки аспекти оптимизације ЛИНК упита у Ц#.НЕТ-у за МС СКЛ Сервер
Такође за манипулације ЛИНК то Објецтс уместо упита као што је:

ЛИНК упит (1. опција)

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

можете користити упит као што је:

ЛИНК упит (2. опција)

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

где је:

Дефинисање два низа

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

, а тип Пара је дефинисан на следећи начин:

Дефиниција типа пара

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

Стога смо испитали неке аспекте у оптимизацији ЛИНК упита за МС СКЛ Сервер.

Нажалост, чак и искусни и водећи .НЕТ програмери заборављају да треба да разумеју шта упутства која користе раде иза сцене. У супротном, они постају конфигуратори и могу да подметну темпирану бомбу у будућности како приликом скалирања софтверског решења тако и са мањим променама спољашњих услова околине.

Извршен је и кратак преглед овде.

Извори за тест - сам пројекат, креирање табела у бази података ТЕСТ, као и попуњавање ових табела подацима се налазе овде.
Такође у овом спремишту, у фасцикли Планови, постоје планови за извршавање упита са условима ИЛИ.

Извор: ввв.хабр.цом

Додај коментар