LINQ влезе во .NET како нов моќен јазик за манипулација со податоци. LINQ на SQL како дел од него ви овозможува да комуницирате прилично удобно со DBMS користејќи, на пример, Entity Framework. Меѓутоа, користејќи го доста често, програмерите забораваат да погледнат каков вид на SQL барање ќе генерира давателот на барање, во вашиот случај Entity Framework.
Ајде да погледнеме две главни точки користејќи пример.
За да го направите ова, креирајте тест база на податоци во 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 со извршување на следната скрипта:
Пополнување на табелата 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
Ајде на сличен начин да ја пополниме табелата за клиенти користејќи ја следната скрипта:
Пополнување на табелата за клиенти
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
Така, добивме две табели, од кои едната има повеќе од 1 милион редови податоци, а другата има повеќе од 10 милиони редови податоци.
Сега во Visual Studio треба да креирате тест проект за апликација Visual C# Console (.NET Framework):
Следно, треба да додадете библиотека за Entity Framework за интеракција со базата на податоци.
За да го додадете, кликнете со десното копче на проектот и изберете Manage NuGet Packages од контекстното мени:
Потоа, во прозорецот за управување со пакети NuGet што се појавува, внесете го зборот „Entity Framework“ во прозорецот за пребарување и изберете го Entity Framework пакетот и инсталирајте го:
Следно, во датотеката App.config, откако ќе го затворите елементот configSections, треба да го додадете следниот блок:
<connectionStrings>
<add name="DBConnection" connectionString="data source=ИМЯ_ЭКЗЕМПЛЯРА_MSSQL;Initial Catalog=TEST;Integrated Security=True;" providerName="System.Data.SqlClient" />
</connectionStrings>
Во connectionString треба да ја внесете низата за поврзување.
Сега ајде да создадеме 3 интерфејси во посебни датотеки:
- Имплементирање на интерфејсот 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 за нашите два ентитета, која ќе вклучува заеднички полиња:
Имплементација на основната класа BaseEntity
namespace TestLINQ
{
public class BaseEntity : IBaseEntityID, IBaseEntityName, IBaseNameInsertUTCDate
{
public int ID { get; set; }
public string Name { get; set; }
public DateTime InsertUTCDate { get; set; }
}
}
Следно, ќе ги создадеме нашите два ентитета во посебни датотеки:
- Имплементација на класата 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 во посебна датотека:
Имплементација на класата 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; }
}
}
Добивме готово решение за спроведување на тестови за оптимизација со LINQ to SQL преку EF за MS SQL Server:
Сега внесете го следниов код во датотеката Program.cs:
Датотека 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();
}
}
}
}
Следно, да го започнеме нашиот проект.
На крајот од работата, следново ќе се прикаже на конзолата:
Генерирано SQL барање
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 доста добро генерирало SQL барање до MS SQL Server DBMS.
Сега да го смениме условот И во ИЛИ во барањето LINQ:
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 };
И да ја стартуваме нашата апликација повторно.
Извршувањето ќе падне со грешка поради времето на извршување на командата што надминува 30 секунди:
Ако го погледнете барањето што беше генерирано од LINQ:
, тогаш можете да се уверите дека изборот се случува преку Декартов производ од две множества (табели):
Генерирано SQL барање
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 на следниов начин:
Оптимизирано барање за LINQ
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 });
Потоа го добиваме следното SQL барање:
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]
За жал, во барањата за LINQ може да има само еден услов за приклучување, така што овде е можно да се направи еквивалентно барање користејќи две барања за секој услов и потоа да се комбинираат преку Унијата за да се отстранат дупликатите меѓу редовите.
Да, прашањата генерално нема да бидат еквивалентни, имајќи предвид дека може да се вратат целосните дупликати редови. Меѓутоа, во реалниот живот, не се потребни целосни дупликат линии и луѓето се обидуваат да се ослободат од нив.
Сега да ги споредиме плановите за извршување на овие две прашања:
- за CROSS JOIN просечното време на извршување е 195 секунди:
- за ВНАТРЕШЕН ПРИКЛУЧУВАЊЕ-УНИЈА просечното време на извршување е помало од 24 секунди:
Как видно из результатов, для двух таблиц с миллионами записей оптимизированный LINQ-запрос работает в разы быстрее, чем неоптимизированный.
Для варианта с И в условиях LINQ-запрос вида:
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 };
почти всегда будет сгенерирован правильный SQL-запрос, который будет выполняться в среднем примерно 1 сек:
Также для манипуляций LINQ to Objects вместо запроса вида:
LINQ-запрос (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 };
можно использовать запрос вида:
LINQ-запрос (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 };
каде што:
Определение двух массивов
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 определяется следующим образом:
Определение типа Para
class Para
{
public int Key1, Key2;
public string Data;
}
Таким образом мы рассмотрели некоторые аспекты в оптимизации LINQ-запросов к MS SQL Server.
К сожалению даже опытные и ведущие .NET-разработчики забывают о том, что необходимо понимать что делают за кадром те инструкции, которые они используют. Иначе они становятся конфигураторами и могут заложить бомбу замедленного действия в будущем как при масштабировании программного решения, так и при незначительных изменениях внешних условий среды.
Также небольшой обзор проводился и
Исходники для теста-сам проект, создание таблиц в базе данных TEST, а также наполнение данными этих таблиц находится
Исто така во ова складиште, во папката Планови, има планови за извршување на барања со услови ИЛИ.
Извор: www.habr.com