LINQ è entrato in .NET come un nuovo e potente linguaggio di manipolazione dei dati. LINQ to SQL come parte di esso consente di comunicare in modo abbastanza conveniente con un DBMS utilizzando, ad esempio, Entity Framework. Tuttavia, utilizzandolo abbastanza spesso, gli sviluppatori dimenticano di considerare il tipo di query SQL che il provider interrogabile, nel tuo caso Entity Framework, genererà.
Esaminiamo due punti principali utilizzando un esempio.
Per fare ciò, crea un database di test in SQL Server e crea due tabelle al suo interno utilizzando la seguente query:
Creazione di tabelle
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
Ora popoliamo la tabella Ref eseguendo il seguente script:
Compilazione della tabella 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
Allo stesso modo riempiamo la tabella Clienti utilizzando il seguente script:
Popolamento della tabella Clienti
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
Pertanto, abbiamo ricevuto due tabelle, una delle quali contiene più di 1 milione di righe di dati e l'altra più di 10 milioni di righe di dati.
Ora in Visual Studio è necessario creare un progetto di prova dell'app console Visual C# (.NET Framework):
Successivamente, è necessario aggiungere una libreria affinché Entity Framework possa interagire con il database.
Per aggiungerlo, fare clic con il pulsante destro del mouse sul progetto e selezionare Gestisci pacchetti NuGet dal menu contestuale:
Successivamente, nella finestra di gestione dei pacchetti NuGet che appare, inserisci la parola “Entity Framework” nella finestra di ricerca e seleziona il pacchetto Entity Framework e installalo:
Successivamente, nel file App.config, dopo aver chiuso l'elemento configSections, è necessario aggiungere il seguente blocco:
<connectionStrings>
<add name="DBConnection" connectionString="data source=ИМЯ_ЭКЗЕМПЛЯРА_MSSQL;Initial Catalog=TEST;Integrated Security=True;" providerName="System.Data.SqlClient" />
</connectionStrings>
In ConnectionString è necessario inserire la stringa di connessione.
Ora creiamo 3 interfacce in file separati:
- Implementazione dell'interfaccia IBaseEntityID
namespace TestLINQ { public interface IBaseEntityID { int ID { get; set; } } }
- Implementazione dell'interfaccia IBaseEntityName
namespace TestLINQ { public interface IBaseEntityName { string Name { get; set; } } }
- Implementazione dell'interfaccia IBaseNameInsertUTCDate
namespace TestLINQ { public interface IBaseNameInsertUTCDate { DateTime InsertUTCDate { get; set; } } }
E in un file separato creeremo una classe base BaseEntity per le nostre due entità, che includerà campi comuni:
Implementazione della classe 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; }
}
}
Successivamente, creeremo le nostre due entità in file separati:
- Implementazione della classe Ref
using System.ComponentModel.DataAnnotations.Schema; namespace TestLINQ { [Table("Ref")] public class Ref : BaseEntity { public int ID2 { get; set; } } }
- Implementazione della classe 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; } } }
Ora creiamo un contesto UserContext in un file separato:
Implementazione della classe 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; }
}
}
Abbiamo ricevuto una soluzione già pronta per condurre test di ottimizzazione con LINQ to SQL tramite EF per MS SQL Server:
Ora inserisci il seguente codice nel file Program.cs:
File 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();
}
}
}
}
Successivamente, lanciamo il nostro progetto.
Al termine del lavoro sulla console verrà visualizzato quanto segue:
Query SQL generata
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])
Cioè, in generale, la query LINQ ha generato abbastanza bene una query SQL sul DBMS MS SQL Server.
Ora cambiamo la condizione AND in OR nella query LINQ:
Domanda 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 lanciamo di nuovo la nostra applicazione.
L'esecuzione si bloccherà con un errore a causa del tempo di esecuzione del comando superiore a 30 secondi:
Se guardi la query generata da LINQ:
, allora puoi fare in modo che la selezione avvenga tramite il prodotto cartesiano di due insiemi (tabelle):
Query SQL generata
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]
Riscriviamo la query LINQ come segue:
Query LINQ ottimizzata
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 });
Quindi otteniamo la seguente query SQL:
Interrogazione 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]
Purtroppo, nelle query LINQ può esserci solo una condizione di join, quindi qui è possibile creare una query equivalente utilizzando due query per ciascuna condizione e poi combinandole tramite Union per rimuovere i duplicati tra le righe.
Sì, le query generalmente non saranno equivalenti, tenendo conto che potrebbero essere restituite righe duplicate complete. Tuttavia, nella vita reale, non sono necessarie linee duplicate complete e le persone cercano di sbarazzarsene.
Ora confrontiamo i piani di esecuzione di queste due query:
- per CROSS JOIN il tempo medio di esecuzione è di 195 secondi:
- per INNER JOIN-UNION il tempo medio di esecuzione è inferiore a 24 secondi:
.
Anche in questo repository, nella cartella Plans, sono presenti i piani per l'esecuzione di query con condizioni OR.
Fonte: habr.com