Một số khía cạnh của việc tối ưu hóa truy vấn LINQ trong C#.NET cho MS SQL Server

LINQ đã đưa .NET vào làm ngôn ngữ thao tác dữ liệu mới mạnh mẽ. LINQ to SQL là một phần của nó cho phép bạn giao tiếp khá thuận tiện với DBMS bằng cách sử dụng Entity Framework, chẳng hạn như. Tuy nhiên, việc sử dụng nó khá thường xuyên, các nhà phát triển quên xem loại truy vấn SQL nào mà nhà cung cấp có thể truy vấn, trong trường hợp của bạn là Entity Framework, sẽ tạo ra.

Hãy xem xét hai điểm chính bằng một ví dụ.
Để thực hiện việc này, hãy tạo cơ sở dữ liệu Kiểm tra trong SQL Server và tạo hai bảng trong đó bằng truy vấn sau:

Tạo bảng

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

Bây giờ hãy điền vào bảng Ref bằng cách chạy tập lệnh sau:

Điền vào bảng 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

Tương tự, hãy điền vào bảng Khách hàng bằng cách sử dụng tập lệnh sau:

Điền vào bảng Khách hàng

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

Do đó, chúng tôi nhận được hai bảng, một bảng có hơn 1 triệu hàng dữ liệu và bảng còn lại có hơn 10 triệu hàng dữ liệu.

Bây giờ trong Visual Studio, bạn cần tạo dự án Visual C# Console App (.NET Framework) thử nghiệm:

Một số khía cạnh của việc tối ưu hóa truy vấn LINQ trong C#.NET cho MS SQL Server

Tiếp theo, bạn cần thêm thư viện để Entity Framework tương tác với cơ sở dữ liệu.
Để thêm nó, nhấp chuột phải vào dự án và chọn Quản lý gói NuGet từ menu ngữ cảnh:

Một số khía cạnh của việc tối ưu hóa truy vấn LINQ trong C#.NET cho MS SQL Server

Sau đó, trong cửa sổ quản lý gói NuGet hiện ra, bạn nhập từ “Entity Framework” vào cửa sổ tìm kiếm rồi chọn gói Entity Framework và cài đặt:

Một số khía cạnh của việc tối ưu hóa truy vấn LINQ trong C#.NET cho MS SQL Server

Tiếp theo, trong tệp App.config, sau khi đóng phần tử configSections, bạn cần thêm khối sau:

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

Trong ConnectionString bạn cần nhập chuỗi kết nối.

Bây giờ hãy tạo 3 giao diện trong các tệp riêng biệt:

  1. Triển khai giao diện IBaseEntityID
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Triển khai giao diện IBaseEntityName
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Triển khai giao diện IBaseNameInsertUTCDate
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

Và trong một tệp riêng biệt, chúng ta sẽ tạo một lớp cơ sở BaseEntity cho hai thực thể của mình, bao gồm các trường chung:

Triển khai lớp cơ sở BaseEntity

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

Tiếp theo, chúng ta sẽ tạo hai thực thể trong các tệp riêng biệt:

  1. Triển khai lớp Ref
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace TestLINQ
    {
        [Table("Ref")]
        public class Ref : BaseEntity
        {
            public int ID2 { get; set; }
        }
    }
    

  2. Triển khai lớp Khách hàng
    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; }
        }
    }
    

Bây giờ hãy tạo bối cảnh UserContext trong một tệp riêng biệt:

Triển khai lớp 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; }
    }
}

Chúng tôi đã nhận được giải pháp có sẵn để tiến hành kiểm tra tối ưu hóa với LINQ to SQL via EF cho MS SQL Server:

Một số khía cạnh của việc tối ưu hóa truy vấn LINQ trong C#.NET cho MS SQL Server

Bây giờ hãy nhập đoạn mã sau vào tệp Program.cs:

Tệp chương trình.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();
            }
        }
    }
}

Tiếp theo, hãy khởi động dự án của chúng tôi.

Khi kết thúc công việc, thông tin sau sẽ được hiển thị trên bảng điều khiển:

Truy vấn SQL đã tạo

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

Nghĩa là, nói chung, truy vấn LINQ đã tạo ra một truy vấn SQL tới MS SQL Server DBMS khá tốt.

Bây giờ hãy thay đổi điều kiện AND thành OR trong truy vấn LINQ:

truy vấn LINQ

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

Và hãy khởi chạy lại ứng dụng của chúng tôi.

Quá trình thực thi sẽ gặp sự cố và xảy ra lỗi do thời gian thực hiện lệnh vượt quá 30 giây:

Một số khía cạnh của việc tối ưu hóa truy vấn LINQ trong C#.NET cho MS SQL Server

Nếu bạn nhìn vào truy vấn được tạo bởi LINQ:

Một số khía cạnh của việc tối ưu hóa truy vấn LINQ trong C#.NET cho MS SQL Server
, thì bạn có thể đảm bảo rằng phép chọn xảy ra thông qua tích Descartes của hai tập hợp (bảng):

Truy vấn SQL đã tạo

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]

Hãy viết lại truy vấn LINQ như sau:

Truy vấn LINQ được tối ưu hóa

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

Sau đó, chúng tôi nhận được truy vấn SQL sau:

truy vấn SQL

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]

Than ôi, trong các truy vấn LINQ chỉ có thể có một điều kiện nối, vì vậy ở đây có thể tạo một truy vấn tương đương bằng cách sử dụng hai truy vấn cho mỗi điều kiện và sau đó kết hợp chúng thông qua Union để loại bỏ trùng lặp giữa các hàng.
Có, các truy vấn thường sẽ không tương đương, có tính đến việc các hàng trùng lặp hoàn chỉnh có thể được trả về. Tuy nhiên, trong cuộc sống thực, những dòng trùng lặp hoàn toàn là không cần thiết và mọi người cố gắng loại bỏ chúng.

Bây giờ hãy so sánh kế hoạch thực hiện của hai truy vấn này:

  1. đối với CROSS JOIN thời gian thực hiện trung bình là 195 giây:
    Một số khía cạnh của việc tối ưu hóa truy vấn LINQ trong C#.NET cho MS SQL Server
  2. đối với INNER JOIN-UNION thời gian thực hiện trung bình dưới 24 giây:
    Một số khía cạnh của việc tối ưu hóa truy vấn LINQ trong C#.NET cho MS SQL Server

Như bạn có thể thấy từ kết quả, đối với hai bảng có hàng triệu bản ghi, truy vấn LINQ được tối ưu hóa nhanh hơn nhiều lần so với bảng không được tối ưu hóa.

Đối với tùy chọn có AND trong các điều kiện, truy vấn LINQ có dạng:

truy vấn LINQ

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

Truy vấn SQL chính xác hầu như sẽ luôn được tạo và chạy trung bình trong khoảng 1 giây:

Một số khía cạnh của việc tối ưu hóa truy vấn LINQ trong C#.NET cho MS SQL Server
Ngoài ra, đối với các thao tác LINQ to Object thay vì truy vấn như:

Truy vấn LINQ (tùy chọn thứ 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 };

bạn có thể sử dụng một truy vấn như:

Truy vấn LINQ (tùy chọn thứ 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 };

Trong đó:

Xác định hai mảng

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

và kiểu Para được định nghĩa như sau:

Định nghĩa loại Para

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

Vì vậy, chúng tôi đã xem xét một số khía cạnh trong việc tối ưu hóa các truy vấn LINQ cho MS SQL Server.

Thật không may, ngay cả các nhà phát triển .NET hàng đầu và có kinh nghiệm cũng quên rằng họ cần phải hiểu những hướng dẫn mà họ sử dụng thực hiện ở hậu trường. Nếu không, họ sẽ trở thành người cấu hình và có thể đặt một quả bom hẹn giờ trong tương lai cả khi mở rộng giải pháp phần mềm cũng như khi có những thay đổi nhỏ trong điều kiện môi trường bên ngoài.

Một đánh giá ngắn cũng được thực hiện đây.

Các nguồn cho thử nghiệm - chính dự án, việc tạo các bảng trong cơ sở dữ liệu TEST, cũng như điền dữ liệu vào các bảng này đều được đặt đây.
Cũng trong kho này, trong thư mục Plans, còn có các kế hoạch thực hiện truy vấn với điều kiện OR.

Nguồn: www.habr.com

Thêm một lời nhận xét