Beberapa aspek mengoptimumkan pertanyaan LINQ dalam C#.NET untuk MS SQL Server

LINQ memasukkan .NET sebagai bahasa manipulasi data baharu yang berkuasa. LINQ kepada SQL sebagai sebahagian daripadanya membolehkan anda berkomunikasi dengan agak mudah dengan DBMS menggunakan, sebagai contoh, Rangka Kerja Entiti. Walau bagaimanapun, menggunakannya agak kerap, pembangun terlupa untuk melihat jenis pertanyaan SQL yang akan dijana oleh pembekal boleh pertanyaan, dalam kes anda Rangka Kerja Entiti.

Mari kita lihat dua perkara utama menggunakan contoh.
Untuk melakukan ini, buat pangkalan data Ujian dalam SQL Server dan buat dua jadual di dalamnya menggunakan pertanyaan berikut:

Mencipta jadual

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

Sekarang mari isi jadual Ref dengan menjalankan skrip berikut:

Mengisi meja Rujukan

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

Mari kita sama-sama mengisi jadual Pelanggan menggunakan skrip berikut:

Mengisi jadual Pelanggan

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

Oleh itu, kami menerima dua jadual, satu daripadanya mempunyai lebih daripada 1 juta baris data, dan satu lagi mempunyai lebih daripada 10 juta baris data.

Sekarang dalam Visual Studio anda perlu mencipta projek Visual C# Console App (.NET Framework) ujian:

Beberapa aspek mengoptimumkan pertanyaan LINQ dalam C#.NET untuk MS SQL Server

Seterusnya, anda perlu menambah perpustakaan untuk Rangka Kerja Entiti untuk berinteraksi dengan pangkalan data.
Untuk menambahkannya, klik kanan pada projek dan pilih Urus Pakej NuGet daripada menu konteks:

Beberapa aspek mengoptimumkan pertanyaan LINQ dalam C#.NET untuk MS SQL Server

Kemudian, dalam tetingkap pengurusan pakej NuGet yang muncul, masukkan perkataan "Rangka Kerja Entiti" dalam tetingkap carian dan pilih pakej Rangka Kerja Entiti dan pasangnya:

Beberapa aspek mengoptimumkan pertanyaan LINQ dalam C#.NET untuk MS SQL Server

Seterusnya, dalam fail App.config, selepas menutup elemen configSections, anda perlu menambah blok berikut:

<connectionStrings>
    <add name="DBConnection" connectionString="data source=ИМЯ_Π­ΠšΠ—Π•ΠœΠŸΠ›Π―Π Π_MSSQL;Initial Catalog=TEST;Integrated Security=True;" providerName="System.Data.SqlClient" />
</connectionStrings>

Dalam connectionString anda perlu memasukkan rentetan sambungan.

Sekarang mari kita buat 3 antara muka dalam fail berasingan:

  1. Melaksanakan antara muka IBaseEntityID
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Pelaksanaan antara muka IBaseEntityName
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Pelaksanaan antara muka IBaseNameInsertUTCDate
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

Dan dalam fail berasingan kami akan mencipta BaseEntity kelas asas untuk dua entiti kami, yang akan merangkumi medan biasa:

Pelaksanaan kelas asas BaseEntity

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

Seterusnya, kami akan mencipta dua entiti kami dalam fail berasingan:

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

  2. Pelaksanaan kelas Pelanggan
    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; }
        }
    }
    

Sekarang mari kita buat konteks UserContext dalam fail berasingan:

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

Kami menerima penyelesaian sedia untuk menjalankan ujian pengoptimuman dengan LINQ ke SQL melalui EF untuk MS SQL Server:

Beberapa aspek mengoptimumkan pertanyaan LINQ dalam C#.NET untuk MS SQL Server

Sekarang masukkan kod berikut ke dalam fail Program.cs:

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

Seterusnya, mari kita lancarkan projek kami.

Pada akhir kerja, perkara berikut akan dipaparkan pada konsol:

SQL Query yang dihasilkan

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

Iaitu, secara umum, pertanyaan LINQ menghasilkan pertanyaan SQL kepada MS SQL Server DBMS dengan baik.

Sekarang mari kita tukar syarat AND kepada OR dalam pertanyaan LINQ:

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

Dan mari melancarkan aplikasi kami sekali lagi.

Pelaksanaan akan ranap dengan ralat kerana masa pelaksanaan arahan melebihi 30 saat:

Beberapa aspek mengoptimumkan pertanyaan LINQ dalam C#.NET untuk MS SQL Server

Jika anda melihat pertanyaan yang dihasilkan oleh LINQ:

Beberapa aspek mengoptimumkan pertanyaan LINQ dalam C#.NET untuk MS SQL Server
, maka anda boleh memastikan bahawa pemilihan berlaku melalui produk Cartesian dua set (jadual):

SQL Query yang dihasilkan

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]

Mari kita tulis semula pertanyaan LINQ seperti berikut:

Pertanyaan LINQ yang dioptimumkan

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

Kemudian kami mendapat pertanyaan SQL berikut:

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

Malangnya, dalam pertanyaan LINQ hanya terdapat satu syarat gabungan, jadi di sini adalah mungkin untuk membuat pertanyaan setara menggunakan dua pertanyaan untuk setiap syarat dan kemudian menggabungkannya melalui Union untuk mengalih keluar pendua antara baris.
Ya, pertanyaan biasanya tidak setara, dengan mengambil kira baris pendua yang lengkap mungkin dikembalikan. Walau bagaimanapun, dalam kehidupan sebenar, baris pendua lengkap tidak diperlukan dan orang ramai cuba menyingkirkannya.

Sekarang mari kita bandingkan rancangan pelaksanaan dua pertanyaan ini:

  1. untuk CROSS JOIN purata masa pelaksanaan ialah 195 saat:
    Beberapa aspek mengoptimumkan pertanyaan LINQ dalam C#.NET untuk MS SQL Server
  2. untuk INNER JOIN-UNION purata masa pelaksanaan adalah kurang daripada 24 saat:
    Beberapa aspek mengoptimumkan pertanyaan LINQ dalam C#.NET untuk MS SQL Server

Seperti yang anda lihat daripada keputusan, untuk dua jadual dengan berjuta-juta rekod, pertanyaan LINQ yang dioptimumkan adalah berkali-kali lebih pantas daripada yang tidak dioptimumkan.

Untuk pilihan dengan DAN dalam syarat, pertanyaan LINQ borang:

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

Pertanyaan SQL yang betul akan hampir selalu dihasilkan, yang akan berjalan secara purata dalam kira-kira 1 saat:

Beberapa aspek mengoptimumkan pertanyaan LINQ dalam C#.NET untuk MS SQL Server
Juga untuk manipulasi LINQ kepada Objek dan bukannya pertanyaan seperti:

Pertanyaan LINQ (pilihan pertama)

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

anda boleh menggunakan pertanyaan seperti:

Pertanyaan LINQ (pilihan pertama)

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

di mana:

Mentakrifkan dua tatasusunan

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

, dan jenis Para ditakrifkan seperti berikut:

Definisi Jenis Para

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

Oleh itu, kami mengkaji beberapa aspek dalam mengoptimumkan pertanyaan LINQ kepada MS SQL Server.

Malangnya, walaupun pembangun .NET yang berpengalaman dan terkemuka lupa bahawa mereka perlu memahami arahan yang mereka gunakan di sebalik tabir. Jika tidak, mereka menjadi konfigurator dan boleh menanam bom jangka pada masa hadapan apabila menskalakan penyelesaian perisian dan dengan perubahan kecil dalam keadaan persekitaran luaran.

Tinjauan ringkas juga telah dijalankan di sini.

Sumber untuk ujian - projek itu sendiri, penciptaan jadual dalam pangkalan data TEST, serta mengisi jadual ini dengan data terletak di sini.
Juga dalam repositori ini, dalam folder Plans, terdapat rancangan untuk melaksanakan pertanyaan dengan syarat ATAU.

Sumber: www.habr.com

Tambah komen