بهینه سازی کوئری‌های LINQ در C#.NET برای MS SQL Server

شنبه 5 مهر 1399

LINQ به عنوان یک زبان قدرتمند برای مدیریت اطلاعات به .NET اضافه شد. به عنوان مثال LINQ to SQL با استفاده ازEntity Framework به شما این امکان را می دهد با DBMS به سهولت صحبت کنید. اما اغلب هنگام استفاده از آن، توسعه‌دهندگان فراموش می‌کنند که بررسی کنند چه نوع query SQl ای توسط provider ای که قابلیت کوئری زدن دارد، تولید می‌شود (در مثال ما Entity Framework). در این مقاله ما بررسی خواهیم کرد که چگونه می‌توانیم عملکرد کوئری‌های LINQ را بهینه کنیم.

بهینه سازی کوئری‌های LINQ در C#.NET برای MS SQL Server

پیاده سازی

بیایید با استفاده از یک مثال دونکته مهم را بررسی کنیم.

ابتدا باید پایگاه داده Test  را در SQL Server ایجاد کنیم. در این دیتابیس با اجرای کوئری زیر دو جدول ایجاد خواهیم شد.


    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  

حالا جدول Ref را به کمک script  زیر پرمی کنیم. صبر کنید... ما فقط یک script  را اجرا کردیم اما آن را ذخیره نکردیم. در چنین مواردی، SQL Complete به کمک Devart بسیار راحت خواهد بود، که با SSMS و Visual Studio ادغام می شود و دارای ویژگی Execution History است.

                                                                                        

این عملکرد تاریخچه اجرا شده کوئری ها در SSMS  را نمایش می دهد.

توجه کنید پنجره از عناصر زیر تشکیل شده است :

1. باکس جستجو برای فیلتر کردن نتایج

2. باکس دامنه تاریخ برای فیلتر کردن نتایج

 3.  نتایج ارائه شده در یک جدول. می توانید داده ها را بر اساس ستون های این جدول مرتب کنید (با استفاده از کلید SHIFT می توانید مجموعه ای از ستون ها را برای مرتب سازی انتخاب کنید)

4. کد ردیف انتخاب شده

جدول نتیجه حاوی تاریخچه اسکریپت های اجرا شده در SSMS است و جداول زیر را شامل می شود:

Status .1: نشان می دهد که اسکریپت با موفقیت اجرا شده یا خیر

Query Text .2: کد اسکریپت

Size (Bytes) .3: اندازه متن در واحد بایت

Executed On .4: تاریخ و ساعت اسکریپت اجرا شده

Duration .5: مدت زمان اجرای اسکریپت

File .6: نام یک فایل یا زبانه در SSMS و همراه با نام نمونه SQL Server که اسکریپت روی آن اجرا شده است

Server .7: نام نمونه SQL Server که اسکریپت روی آن اجرا شده است

User .8: ورود به سیستم تحت اسکریپت اجرا شده

Database .9: بستر پایگاه داده ای که اسکریپت در آن اجرا شده است

ما می توانیم کوئری مورد نیاز را در این جدول پیدا کنیم،


    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  

ابزار SQL Complete در حفظ  قالب منظم کد اسکریپت ها به شما می تواند کمک کند.

به این ترتیب ، ما دو جدول ایجاد کردیم - یکی از آنها بیش از 1 میلیون ردیف دارد و دیگری بیش از 10 میلیون ردیف دارد.

اکنون باید یک پروژه آزمایشی در  Visual C# Console App ایجاد کنیم:

در قدم بعدی ، باید یک کتابخانه به Entity Framework اضافه کنیم تا بتوانیم با پایگاه داده در ارتباط باشیم.

برای افزودن این کتابخانه، روی پروژه کلیک راست کنید و‘Manage NuGet Packages …’ را در منو انتخاب کنید،

                                                              

در پنجره باز شده، "Entity Framework" را در کادر جستجو وارد کنید، بسته Entity Framework را انتخاب کنید و آن را نصب کنید:

در قدم بعد، در فایل App.config، کدهای زیر را بعد ازعنصر configSections اضافه می کنیم:


    <connectionStrings>  
        <add name="DBConnection" connectionString="data source=MSSQL_INSTANCE_NAME;Initial Catalog=TEST;Integrated Security=True;" providerName="System.Data.SqlClient" />  
    </connectionStrings>  

مطمئن شوید رشته اتصال در connection string وارد شده باشد.

حالا سه Interface  در فایل های جداگانه ایجاد می کنیم:

IBaseEntityID


    namespace TestLINQ    
    {    
        public interface IBaseEntityID    
        {    
            int ID { get; set; }    
        }    
    }

IBaseEntityName


    namespace TestLINQ      
    {      
        public interface IBaseEntityName      
        {      
            string Name { get; set; }      
        }      
    }      

IBaseNameInsertUTCDate


    namespace TestLINQ        
    {        
        public interface IBaseNameInsertUTCDate        
        {        
            DateTime InsertUTCDate { get; set; }        
        }        
    }    

در یک فایل جداگانه، یک کلاس BaseEntity برای دو موجودیت خود ایجاد می کنیم که شامل فیلدهای مشترک آنها باشد.


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

 در قدم بعد دو موجودیت (Entitie) می سازیم هر کدام در فایل جداگانه:

Ref


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

Customer


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

در آخر یک UserContext در یک فایل جداگانه ایجاد می کنیم،


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

به این ترتیب، ما یک راهکار برای انجام تست های بهینه سازی به کمک LINQ to SQL از طریق Entity Framework برای MS SQL Server دریافت می کنیم:

                                                                                               

اکنون، کد زیر را در 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();  
                }  
            }  
        }  
    }  

وقتی پروژه را اجرا  کنیم، این  خروجی است که به عنوان نتیجه در کنسول خواهیم دید،

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

همانطور که مشاهده می کنید، یک LINQ query به طور کارآمد SQL query را در  

MS SQL Server DBMS ایجاد کرده است.

حالا، شرط AND را به OR در LINQ query تغییر می دهیم

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 ثانیه به پایان رسیده است،

                     

این کوئریی است که LINQ ایجاد کرده است:

می بینیم که گزینش از یک حاصلضرب دکارتی از دو مجموعه (جدول) انجام می شود

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] 

LINQ query را دوباره به این صورت می نویسیم

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  
                                                                                    se

این SQL query هست که به عنوان نتیجه دریافت می کنیم


    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]  

متأسفانه ، فقط یک شرط اتصال در یک LINQ query  وجود دارد، بنابراین می توانیم با ایجاد یک کوئری برای هر دو شرط و سپس ترکیب آنها با استفاده از Union برای حذف خطوط تکراری ، به نتایج دلخواه برسیم.

بله، با توجه به اینکه  تمام ردیف های تکراری را می توانید برگردانید کوئری ها در اکثر موارد نابرابر خواهند بود. با این حال ، در زندگی واقعی به ردیف های تکراری نیازی نیست و معمولاً مواردی هستند که می خواهید از آنها خلاص شوید.

حال بیایید برنامه های اجرایی این دو کوئری را با هم مقایسه کنیم:

میانگین زمان اجرا برای  ,CROSS JOIN 195  ثانیه است

       

میانگین زمان اجرا برای INNER JOIN-UNION کمتر از 24 ثانیه است

همانطور که از نتایج مشاهده می کنیم، LINQ query بهینه شده چندین برابر سریعتر از یک مورد بهینه نشده در این دو جدول با میلیون ها رکورد، کار می کند.

برای ورژن با شرط AND، یک LINQ query به این شکل خواهد بود،

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

تقریباً در این حالت ، یک  SQL query صحیح ایجاد می شود ، با زمان اجرای تقریباً 24 ثانیه:

همچنین، برای عملیات LINQ to Objects، به جای کوئری که به این شکل است:

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

می توانیم از کوئری مشابه زیر استفاده کنیم:

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

نوع Para به روش زیر تعریف می شود:


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

نتیجه گیری

ما برخی از حالت های بهینه سازی LINQ query را برای MS SQL Server بررسی کردیم. همچنین، SQL Complete در جستجوی تاریخچه کوئری و همچنین قالب بندی اسکریپت هایی که در این مقاله استفاده کردیم، به ما کمک زیادی کرد.

متأسفانه، حتی توسعه دهندگان با تجربه .NET نیز اغلب فراموش می کنند که درک دستورالعمل های استفاده شده در پس زمینه ضروری است. وگرنه، آنها می توانند پیکربندی شده و در آینده بمب ساعتی مجازی تنظیم کنند، چه زمانی که راه حل مقیاس بندی شده باشد و چه زمانی که عوامل خارجی کمی تغییر کند.

همچنین ، پوشه Plans از این ریپازیتوری شامل برنامه های اجرایی کوئری ها با شرط OR است.

علاوه بر این ، یک راه حل خوب به نام dotConnect وجود دارد، که یک سطر از کامپوننت های data access از Devart برای DBMS های متنوع است. از میان آنها، مولفه های dotConnect از ابزارهای ORM مانند Entity Framework Core و LinqConnect پشتیبانی می کنند که به شما این امکان را می دهد با کلاس های LINQ to SQL کار کنید.

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید