65.9K
CodeProject 正在变化。 阅读更多。
Home

Linq to SQL 性能考虑 - 第 2 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (14投票s)

2010 年 4 月 29 日

CPOL

8分钟阅读

viewsIcon

47905

Linq to SQL 性能考虑 - 第 2 部分

引言

在发布了“LINQ to SQL 性能考量”之后,许多读者回复并要求我扩展基准测试示例,以包含 LINQ to SQL 编译查询。

本文将探讨将 LINQ 查询转换为编译查询所需的操作以及可以获得的性能提升。

这是上一篇文章的链接:第一部分

我还决定使用仪器分析方法,通过 Visual Studio 性能分析器运行本文第一部分中讨论的每种数据访问场景。

仪器分析方法比采样分析方法更进一步,采样分析方法仅在程序积极使用 CPU 时收集信息。

仪器分析方法在每个函数开始和结束处添加探针,用于测量经过的时间。

探针还用于确定每个函数使用了多少 CPU 时间以及外部函数有多昂贵。(《使用 Visual Studio Profiler 查找应用程序瓶颈》,2010 年。)

请参考上一篇文章,因为我计划只列出原始代码中的更改。

本文第一部分将 LINQ to SQL 的基准测试与各种数据访问方法进行了比较,这些方法包括访问 SQL Server 存储过程的 ADO.NET、访问相同存储过程的 LINQ to SQL 以及访问 SQL Server 用户定义函数的 LINQ to SQL。

编译查询

要创建 LINQ to SQL 编译查询,有几种设计模式可以用作转换过程的指南。本文将使用一种创建 `static` 方法来调用委托的设计模式。

有一些关于这些设计模式的非常好的文章,我将在后面的参考部分发布链接。

让我们看一个在本文第一部分中引用的简单查询表达式。

var shift1 =
	from sft1 in sq.Shifts
	where sft1.ShiftName == "Shift1"
	select new { sft1.StartTime };

此查询表达式用于提取第一个班次的开始时间。

为了将此查询转换为编译查询,必须将查询转换为一个方法。为此,我们必须参数化几个关键数据。(《为高流量 ASP.NET 网站解决 LINQ to SQL 编译查询的常见问题》,2008 年。)

IQueryable<string> GetShift1(SQLDataDataContext sq, string strShift)
{
    return from sft1 in sq.Shifts
        where sft1.ShiftName == strShift
        select sft1.StartTime;
}

在上面的代码中,创建了一个泛型方法,该方法接收一个数据上下文和一个用于所需班次的 `string` 参数。

接下来,必须将方法转换为包含委托和 Lambda 表达式。`Func<>` 委托和 Lambda 表达式是 C# 3.0 中引入的两个新构造,用于帮助构建 LINQ to SQL 表达式。

public static readonly Func<sqldatadatacontext,string,IQueryable<string>>
    GetShift1 = (sq, strShift) =>
        from sft1 in sq.Shifts
        where sft1.ShiftName == strShift
        select sft1.StartTime;

请注意,上面的委托声明为 `static readonly`。您希望编译查询只被创建一次并在所有线程中重用。如果省略 `static` 修饰符,编译查询将在每次引用时重新编译,您将失去任何性能提升。

现在可以进行最终转换了。通过添加以下语法,该方法可以转换为编译查询:

public static readonly Func<SQLDataDataContext, string, IQueryable<string>>
GetShift1 = CompiledQuery.Compile((SQLDataDataContext sq, 
	string strShift) => from sft1 in sq.Shifts
        where sft1.ShiftName == strShift
        select sft1.StartTime);

通过向代码添加 `CompiledQuery.Compile()` 方法,委托将在执行时被编译一次,并且在后续执行中不再进行编译。

创建编译查询时还有一些问题需要解决。当我们讨论本文第一部分中定义的其他 LINQ to SQL 查询时,将解决这些问题。

var shift2 =
     from sft2 in sq.Shifts
     where sft2.ShiftName == "Shift2"
     select new {sft2.StartTime, sft2.Hours };

在上面的 LINQ to SQL 查询中,`select new {sft2.StartTime, sft2.Hours}` 语句暗示了一个匿名类型。没有提供类型名称。编译查询不能包含匿名类型。泛型需要指定类型名称。

有一种解决方法。

public static readonly Func<SQLDataDataContext, string, IQueryable<Shift>>
     GetShift2 = CompiledQuery.Compile((SQLDataDataContext sq, string  strShift 
      => from sft2 in sq.Shifts
         where sft2.ShiftName == strShift
         select sft2 ); 

请注意,返回类型是 `IQueryable`。一种解决方法是指定一个类型。在这种情况下,编译查询引用了由设计器创建的 `Shift` 类。

var icount =
   from insp in sq.Inspections
   where insp.TestTimeStamp > dStartTime && insp.TestTimeStamp < dEndTime
   && insp.Model == "EP"
   group insp by insp.Model into grp
   select new { Count = grp.Count() };

请注意,此查询也包含一个匿名类型。下面的编译查询通过引用已知的 `IQueryable` 类型来解决此问题。

public static readonly Func<SQLDataDataContext, 
	string, DateTime, DateTime, IQueryable<Int32>>
    GetModelCnt = CompiledQuery.Compile((SQLDataDataContext sq, 
	string strModel, DateTime dStartTime, DateTime dEndTime) =>
        from insp in sq.Inspections
        where insp.TestTimeStamp > dStartTime && insp.TestTimeStamp < dEndTime
        && insp.Model == strModel
        group insp by insp.Model into grp
        select grp.Count());

我们删除了引用匿名类型的代码,并使用投影查询了 `Count()` 方法的分组。

var unordered =
    from insp in sq.Inspections
    where insp.TestTimeStamp > dStartTime && insp.TestTimeStamp < dEndTime
    && insp.Model == "EP" && insp.TestResults != "P"
    group insp by new { insp.TestResults, insp.FailStep } into grp

    select new
    {                
        FailedCount = (grp.Key.TestResults == "F" ? grp.Count() : 0),
        CancelCount = (grp.Key.TestResults == "C" ? grp.Count() : 0),
        grp.Key.TestResults,
        grp.Key.FailStep,
        PercentFailed = Convert.ToDecimal(1.0 * grp.Count() / tcount * 100)
    };

将上述查询转换为编译查询有两个问题。如上所述,此查询包含一个匿名类型。第二个问题与必须传递给方法的类型数量有关。

显然,编译查询可以传递的参数类型数量有限。(《为高流量 ASP.NET 网站解决 LINQ to SQL 编译查询的常见问题》,2008 年。)

解决方法是使用 `struct` 或类来打包参数并将对象传递给编译查询。

public struct testargs
{
    public int tcount;
    public string strModel;
    public string strTest;
    public DateTime dStartTime;
    public DateTime dEndTime;
}

public static readonly Func<SQLDataDataContext, testargs, 
	IQueryable<CalcFailedTestResult>>
    GetInspData = CompiledQuery.Compile((SQLDataDataContext sq, testargs targs) =>
        from insp in sq.Inspections
        where insp.TestTimeStamp > targs.dStartTime && 
	insp.TestTimeStamp < targs.dEndTime 
        && insp.Model == targs.strModel && insp.TestResults != targs.strTest
        group insp by new { insp.TestResults, insp.FailStep } into grp

        select new CalcFailedTestResult
        {
            FailedCount = (grp.Key.TestResults == "F" ? grp.Count() : 0),
            CancelCount = (grp.Key.TestResults == "C" ? grp.Count() : 0),
            TestResults = grp.Key.TestResults,
            FailStep = grp.Key.FailStep,
            PercentFailed = Convert.ToDecimal(1.0 * grp.Count() / targs.tcount * 100)
        });

匿名类型问题通过提供一个由设计器构建的命名类型来解决。在调用编译查询之前,构建并填充了 `struct`。

var fStepc =
    from selection in unorderedc
    orderby selection.FailedCount descending, selection.CancelCount descending

    select selection;

最后一个查询是对由前一个编译查询创建的 `IQueryable` 对象进行查询。由于查询操作是在对象而不是数据上下文上执行的,因此我们不必编译此查询。

调用编译查询

一旦编译查询构建完成,如何调用它们?您只需通过方法调用即可调用编译查询。

这是重写为调用编译查询的代码。

Program p = new Program();
SQLDataDataContext sq = new SQLDataDataContext
	(p.GetConnectionString("Production_Monitoring"));
sq.ObjectTrackingEnabled = false;

// get Shift1 start time
var shift1c = GetShift1(sq, "Shift");

foreach (var sft in shift1c)
{
       s1StartTime = sft;
}

// Get Shift2 Hours
var shift2c = GetShift2(sq, "Shift2");

foreach (var sft in shift2c)
{
    s2StartTime = sft.StartTime;
    iHours = Convert.ToInt32(sft.Hours);
}

DateTime dStartTimec = Convert.ToDateTime(sDate + " " + s1StartTime);
DateTime dEndStartTimec = Convert.ToDateTime(sDate + " " + s2StartTime);
DateTime dEndTimec = dEndStartTimec.AddHours(iHours);

var icountc = GetModelCnt(sq, "EP", dStartTimec, dEndTimec);

foreach (var i in icountc)
{
    tcount = i;    
}

testargs targs = new testargs();
targs.strModel = "EP";
targs.strTest = "P";
targs.dStartTime = dStartTimec;
targs.dEndTime = dEndTimec;
targs.tcount = tcount;

var unorderedc = GetInspData(sq, targs);

var fStepc =
    from selection in unorderedc
    orderby selection.FailedCount descending, selection.CancelCount descending

    select selection;


stopwatch.Stop();
Console.WriteLine("Linq precompile with compiling time - 
	" + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();

基准测试

与本文第一部分一样,数据访问场景运行在一个使用 C# 和 Visual Studio 2008 开发的控制台应用程序中。在进行基准测试时,正在访问的数据表的行数约为 202,000 行,最终查询返回 14 行数据。SQL Server 的版本是 2008。

基准测试时间以毫秒为单位,应用程序执行了 5 次以获得更好的样本量。

场景 时间 时间 时间 时间 时间
Linq 239 203 162 161 172
DataLayer 90 90 90 90 90
SP 102 103 104 109 102
FN 91 92 92 92 92
LINQ 编译 – 第 1 次传递 84 84 85 85 85
LINQ 编译 第 2 次传递 49 49 49 48 49

结果非常惊人,与数据层查询相比,编译后的第二次传递查询的基准测试快了 45.5%。与第一次传递查询的基准测试相比,第二次传递编译查询快了 41.6%。第二次传递编译查询与原生 LINQ 查询(平均时间)相比快了 73.82%。

性能分析

除了编译查询之外,所有场景都分别通过 Visual Studio 性能分析器运行。这些查询被分组在一起,一个接一个地运行,以比较第一次传递查询执行时间和第二次传递查询执行时间之间的性能。

由于编译查询只需要编译一次,因此第二次传递查询的执行基准测试时间将继续用于进一步的查询执行。

性能摘要本身并没有提供太多见解。但当将摘要与各种数据访问场景进行比较时,会出现一些非常有趣的结果。

在进行比较时,请注意性能摘要中各部分的调用次数和时间。

原生 LINQ 查询

Linq_UnCompiled.jpg

请注意调用次数的减少;这归因于应用程序将大部分查询卸载到数据库。执行时间也表明存储过程的执行效率高于原生 LINQ 查询。

数据层查询

Data_Layer.JPG

请注意,调用次数与数据层查询相同。但执行时间略有增加。LINQ 调用存储过程仍有一些开销。

LINQ 存储过程

Linq_sp.jpg

LINQ 存储过程查询和 LINQ 用户定义函数查询的结果非常相似。基准测试时间相差约 10 毫秒。

LINQ 用户定义函数

Linq_FN.jpg

LINQ 编译

Linq__Compiled.jpg

请记住,此摘要包括了第一次传递编译查询和第二次传递编译查询的性能比较。换句话说,此摘要中有两个查询正在运行,而其他摘要中只有一个查询。

如上所示,编译后的第二次传递查询比编译后的第一次传递查询快 41.6%。性能摘要表明,编译查询确实占用的调用次数更少,并且执行效率更高。

结论

以上文章详细介绍了将 LINQ to SQL 查询转换为编译查询的基本细节。文章介绍了一种更常见的转换设计模式,并试图阐明需要进行修改的一些领域。

在基准测试部分,利用编译查询的好处也变得非常明显。

参考

历史

  • 2010 年 4 月 29 日:首次发布
© . All rights reserved.