Algúns aspectos da optimización de consultas LINQ en C#.NET para MS SQL Server

LINQ entrou en .NET como unha poderosa linguaxe de manipulación de datos. LINQ to SQL como parte del permítelle comunicarse de xeito bastante cómodo cun DBMS usando, por exemplo, Entity Framework. Non obstante, usándoo con bastante frecuencia, os desenvolvedores esquécense de mirar que tipo de consulta SQL xerará o provedor consultable, no seu caso Entity Framework.

Vexamos dous puntos principais usando un exemplo.
Para iso, cree unha base de datos de proba en SQL Server e cree nela dúas táboas usando a seguinte consulta:

Creación de táboas

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

Agora imos encher a táboa Ref executando o seguinte script:

Enchendo a táboa de 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

De igual xeito, enchemos a táboa de Clientes usando o seguinte script:

Poboando a táboa Cliente

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

Así, recibimos dúas táboas, unha das cales ten máis de 1 millón de filas de datos e a outra máis de 10 millóns de filas de datos.

Agora, en Visual Studio, debes crear un proxecto de proba da aplicación de consola Visual C# (.NET Framework):

Algúns aspectos da optimización de consultas LINQ en C#.NET para MS SQL Server

A continuación, cómpre engadir unha biblioteca para que Entity Framework interactúe coa base de datos.
Para engadilo, fai clic co botón dereito sobre o proxecto e selecciona Xestionar paquetes NuGet no menú contextual:

Algúns aspectos da optimización de consultas LINQ en C#.NET para MS SQL Server

A continuación, na xanela de xestión de paquetes NuGet que aparece, introduza a palabra "Entity Framework" na xanela de busca e seleccione o paquete Entity Framework e instáleo:

Algúns aspectos da optimización de consultas LINQ en C#.NET para MS SQL Server

A continuación, no ficheiro App.config, despois de pechar o elemento configSections, cómpre engadir o seguinte bloque:

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

En connectionString cómpre introducir a cadea de conexión.

Agora imos crear 3 interfaces en ficheiros separados:

  1. Implementación da interface IBaseEntityID
    namespace TestLINQ
    {
        public interface IBaseEntityID
        {
            int ID { get; set; }
        }
    }
    

  2. Implementación da interface IBaseEntityName
    namespace TestLINQ
    {
        public interface IBaseEntityName
        {
            string Name { get; set; }
        }
    }
    

  3. Implementación da interface IBaseNameInsertUTCDate
    namespace TestLINQ
    {
        public interface IBaseNameInsertUTCDate
        {
            DateTime InsertUTCDate { get; set; }
        }
    }
    

E nun ficheiro separado crearemos unha clase base BaseEntity para as nosas dúas entidades, que incluirá campos comúns:

Implementación da clase base BaseEntity

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

A continuación, crearemos as nosas dúas entidades en ficheiros separados:

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

  2. Implantación da clase Cliente
    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; }
        }
    }
    

Agora imos crear un contexto UserContext nun ficheiro separado:

Implementación da clase 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; }
    }
}

Recibimos unha solución preparada para realizar probas de optimización con LINQ to SQL a través de EF para MS SQL Server:

Algúns aspectos da optimización de consultas LINQ en C#.NET para MS SQL Server

Agora introduza o seguinte código no ficheiro Program.cs:

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

A continuación, imos lanzar o noso proxecto.

Ao final do traballo, aparecerá o seguinte na consola:

Consulta SQL xerada

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

É dicir, en xeral, a consulta LINQ xerou bastante ben unha consulta SQL ao DBMS MS SQL Server.

Agora imos cambiar a condición AND a OR na consulta LINQ:

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

E imos lanzar de novo a nosa aplicación.

A execución fallará cun erro debido a que o tempo de execución do comando supera os 30 segundos:

Algúns aspectos da optimización de consultas LINQ en C#.NET para MS SQL Server

Se mira a consulta que foi xerada por LINQ:

Algúns aspectos da optimización de consultas LINQ en C#.NET para MS SQL Server
, entón podes asegurarte de que a selección se produce a través do produto cartesiano de dous conxuntos (táboas):

Consulta SQL xerada

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]

Reescribamos a consulta LINQ do seguinte xeito:

Consulta LINQ optimizada

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

Despois obtemos a seguinte consulta SQL:

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

Por desgraza, nas consultas LINQ só pode haber unha condición de unión, polo que aquí é posible facer unha consulta equivalente usando dúas consultas para cada condición e combinándoas despois a través de Union para eliminar duplicados entre as filas.
Si, as consultas xeralmente non serán equivalentes, tendo en conta que se poden devolver filas duplicadas completas. Non obstante, na vida real, non son necesarias liñas duplicadas completas e a xente intenta desfacerse delas.

Agora imos comparar os plans de execución destas dúas consultas:

  1. para CROSS JOIN, o tempo medio de execución é de 195 segundos:
    Algúns aspectos da optimización de consultas LINQ en C#.NET para MS SQL Server
  2. para INNER JOIN-UNION o tempo medio de execución é inferior a 24 segundos:
    Algúns aspectos da optimización de consultas LINQ en C#.NET para MS SQL Server

Como podes ver nos resultados, para dúas táboas con millóns de rexistros, a consulta LINQ optimizada é moitas veces máis rápida que a non optimizada.

Para a opción con AND nas condicións, unha consulta LINQ do formulario:

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

Case sempre xerarase a consulta SQL correcta, que se executará de media en aproximadamente 1 segundo:

Algúns aspectos da optimización de consultas LINQ en C#.NET para MS SQL Server
Tamén para manipulacións de LINQ to Objects en lugar dunha consulta como:

Consulta LINQ (1ª opción)

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

podes usar unha consulta como:

Consulta LINQ (2ª opción)

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

en que:

Definición de dúas matrices

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

, e o tipo Para defínese como segue:

Definición de tipo de para

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

Así, examinamos algúns aspectos na optimización de consultas LINQ para MS SQL Server.

Desafortunadamente, incluso os desenvolvedores de .NET experimentados e líderes esquecen que teñen que entender o que fan as instrucións que usan entre bastidores. En caso contrario, convértense en configuradores e poden poñer unha bomba de reloxería no futuro tanto ao escalar a solución de software como con pequenos cambios nas condicións ambientais externas.

Tamén se realizou unha pequena revisión aquí.

Localízanse as fontes para a proba: o propio proxecto, a creación de táboas na base de datos TEST, así como o enchemento destas táboas con datos. aquí.
Tamén neste repositorio, no cartafol Plans, hai plans para executar consultas con condicións OU.

Fonte: www.habr.com

Engadir un comentario