实体框架缓存的查询计划性能随参数不同而降低

Posted

技术标签:

【中文标题】实体框架缓存的查询计划性能随参数不同而降低【英文标题】:Entity Framework cached query plan performance degrades with different parameters 【发布时间】:2011-12-19 06:03:52 【问题描述】:

我有以下问题。

背景

我正在尝试使用 MVC3、EF4 和 jquery 对具有 450 万条记录的表实现自动完成选择器。

这是桌子:

CREATE TABLE [dbo].[CONSTA] (
  [afpCUIT] nvarchar(11) COLLATE Modern_Spanish_CI_AS NOT NULL,
  [afpNombre] nvarchar(30) COLLATE Modern_Spanish_CI_AS NULL,
  [afpGanancias] varchar(2) COLLATE Modern_Spanish_CI_AS NULL,
  [afpIVA] varchar(2) COLLATE Modern_Spanish_CI_AS NULL,
  [afpMonot] varchar(2) COLLATE Modern_Spanish_CI_AS NULL,
  [afpIntSoc] varchar(1) COLLATE Modern_Spanish_CI_AS NULL,
  [afpEmpl] varchar(1) COLLATE Modern_Spanish_CI_AS NULL,
  [afpAct] varchar(2) COLLATE Modern_Spanish_CI_AS NULL,
  CONSTRAINT [CONSTA_pk] PRIMARY KEY CLUSTERED ([afpCUIT])
)
ON [PRIMARY]
GO

CREATE NONCLUSTERED INDEX [CONSTA_Nombre_idx] ON [dbo].[CONSTA]
  ([afpNombre])
WITH (
  PAD_INDEX = OFF,
  DROP_EXISTING = OFF,
  STATISTICS_NORECOMPUTE = OFF,
  SORT_IN_TEMPDB = OFF,
  ONLINE = OFF,
  ALLOW_ROW_LOCKS = OFF,
  ALLOW_PAGE_LOCKS = OFF)
ON [PRIMARY]
GO

该表是相当静态的(它只需要每月批量更新)并且是只读的。

如果有人想下载记录 (54MB),这是 URL:

http://www.afip.gob.ar/genericos/cInscripcion/22102011.zip

这里是记录描述:

http://www.afip.gob.ar/genericos/cInscripcion/archivoCompleto.asp

这是应用程序的代码:

控制器:

public class AltaMasivaController : Controller

    //
    // GET: /AltaMasiva/

    public ActionResult Index()
    
        return View();
    

    public JsonResult GetUsers(string query)
    
        CENT2Entities db = new CENT2Entities();
        bool isCUIT = true;

        for(int j = 0; j < query.Length; j++)
            if (! Char.IsDigit(query, j))
            
                isCUIT = false;
                break;
            

        if (isCUIT)
        
            // nvarchar search
            var x = from u in db.CONSTA
                    where u.afpCUIT.StartsWith(query)
                    orderby u.afpNombre
                    select new  label = u.afpNombre.TrimEnd(), id = u.afpCUIT ;

            return Json(x.Take(50), JsonRequestBehavior.AllowGet);
        
        else
        
            // nvarchar search
            var x = from u in db.CONSTA
                    where u.afpNombre.StartsWith(query)
                    orderby u.afpNombre
                    select new  label = u.afpNombre.TrimEnd(), id = u.afpCUIT ;

            return Json(x.Take(50), JsonRequestBehavior.AllowGet);
        
     

查看:

@
    viewbag.title = "index";


<h2>index</h2>
@html.textbox("user", "", new  style="width: 400px;" )

<script type="text/javascript">

$("input#user").autocomplete(
 
    source: function (request, response) 
     
        // define a function to call your action (assuming usercontroller) 
        $.ajax(
         
            url: '/altamasiva/getusers', type: "post", datatype: "json", 

            // query will be the param used by your action method 
            data:  query: request.term , 

            success: function(data) 
                response( $.map(data, function (item) return  label: item.label + " (" + item.id + ")", value: item.label, id: item.id ; )); 
             
        ) 
    , 
    minlength: 1, // require at least one character from the user
);

</script>

现在:

问题

如您所见,如果查询字符串仅包含数字,则代码遵循不同的路径。

当控制器参数的所有字符都是数字时(其中 u.afpCUIT.StartsWith(query) ),查询优化器“应该”执行聚集索引查找(它会执行)并返回前 50 行发现。 当第一个“自动完成”字符串到达​​时(通常最多一个或两个字符),查询执行得非常快,但是,当字符串的长度增加时,性能会显着下降(它需要 20 秒到 2 分钟,而 9 或更多字符)。 令人惊讶的是,在“重新启动”SQL Server 服务后,如果初始字符串包含 10 个字符,它的性能也很好,但是当我们从“查询”字符串中删除字符时,性能会下降,完全相反。

为什么会这样?

当 SQL Server 编译第一个执行计划时,它会对其进行优化,以在大型结果集(反之亦然)的情况下快速执行。缩小(或扩展)结果集的后续查询需要不同的执行计划......但是...... EF 生成的 SQL 使用逗号参数(精确)避免语句重新编译......

通过执行清理执行计划缓存:

db.ExecuteStoreCommand("DBCC FREEPROCCACHE");

将性能恢复到极好的响应时间......但是......它会杀死所有数据库中的所有计划,从而降低所有其他缓存计划的性能(通常执行良好)。

在对 EF sql 语句进行了一些分析之后,我在 EF 生成 sql 之前在查询分析器中执行了 DBCC FREEPROCCACHE,结果生成了不同的执行计划,所有执行计划都在 250ms 范围内执行,与参数长度无关:

DBCC FREEPROCCACHE

exec sp_executesql N'SELECT TOP (50) 
[Project1].[C1] AS [C1], 
[Project1].[C2] AS [C2], 
[Project1].[afpCUIT] AS [afpCUIT]
FROM ( SELECT 
    [Extent1].[afpCUIT] AS [afpCUIT], 
    [Extent1].[afpNombre] AS [afpNombre], 
    1 AS [C1], 
    RTRIM([Extent1].[afpNombre]) AS [C2]
    FROM [dbo].[CONSTA] AS [Extent1]
    WHERE [Extent1].[afpCUIT] LIKE @p__linq__0 ESCAPE N''~''
)  AS [Project1]
ORDER BY [Project1].[afpNombre] ASC',N'@p__linq__0 nvarchar(4000)',@p__linq__0=N'2023291%'

问题

有没有比

更优雅的选择
db.ExecuteStoreCommand("DBCC FREEPROCCACHE");

?

令人惊讶的是,查询的第二条路径(其中 u.afpNombre.StartsWith(query) )不受相同问题的影响并且表现出色。显然,执行计划不会随着字符串长度的变化而改变......

我在旧版本的 EF 中发现了一个 ObjectContext 参数:

System.Data.EntityClient.EntityCommand.EnablePlanCaching

但我在 EF4 中找不到,我不确定全局结果是否相同。

这个问题我真的很疑惑,不知道真正的问题在哪里

索引设计不佳? 缺少分区? SQL SERVER 2008 速成版? EF生成的SQL? 运气不好?

任何帮助都会很棒。 提前谢谢!

【问题讨论】:

如果您认为 EF 导致了问题,您可以通过存储过程轻松切换 EF4。您是否使用过适当的 SQL Server 分析工具(如 Quest)并检查过缓冲区刷新、磁盘 I/O 等?您是否考虑过增加可供 SQL Server 使用的 RAM? 已经做到了,但结果同样蹩脚。我很想对“查询”参数的长度设定条件,但它只能使用相同的统计数据......关于服务器资源,它们不是问题,因为查询在笔记本电脑中表现出色(如只要查询计划没有被缓存)还是非常感谢! 【参考方案1】:

如您所见,SQL Server 编译计划以针对具有较大结果集的一个参数值进行优化。当结果集缩小时,查询性能不佳。

这种情况需要在查询中使用“选项(重新编译)”提示,因此查询将针对它收到的每个值重新编译。

用实体框架做到这一点并不容易。您将需要创建一个 DbCommandInterceptor 以在查询中包含选项(重新编译)。另一种选择是在 SQL Server 中创建计划指南,以将“选项(重新编译)”添加到查询中。

您可以在此处找到有关 DbCommandInterceptor 的信息 - Adding a query hint when calling Table-Valued Function

关于计划指南,您将需要类似以下内容:

EXEC sp_create_plan_guide   
'planguidename',   
N'SELECT TOP (50) 
[Project1].[C1] AS [C1], 
[Project1].[C2] AS [C2], 
[Project1].[afpCUIT] AS [afpCUIT]
FROM ( SELECT 
    [Extent1].[afpCUIT] AS [afpCUIT], 
    [Extent1].[afpNombre] AS [afpNombre], 
    1 AS [C1], 
    RTRIM([Extent1].[afpNombre]) AS [C2]
    FROM [dbo].[CONSTA] AS [Extent1]
    WHERE [Extent1].[afpCUIT] LIKE @p__linq__0 ESCAPE N''~''
)  AS [Project1]
ORDER BY [Project1].[afpNombre] ASC',
'SQL',   
NULL,   
N'@p__linq__0 nvarchar(4000)',
N'OPTION (recompile)'

【讨论】:

【参考方案2】:

有一种方法可以从 SQL Server 的缓存中删除单个计划。 这里有详细解释: http://sqlblog.com/blogs/kalen_delaney/archive/2007/09/29/geek-city-clearing-a-single-plan-from-cache.aspx

此外,您可以创建一个存储过程,并将其映射到实体框架而不是使用 LINQ2Entities,这样可以对 SQL 语法进行特定更改,并确保它始终相同。

【讨论】:

Svarog,非常感谢,该博客真的很到位(它发出了与我完全相同的问题)并为我提供了:a)我可以在开发中立即使用的“更优雅”的解决方法:仅刷新有问题的数据库 proc(不是整个服务器缓存)缓存: db.ExecuteStoreCommand("DECLARE @myDb AS INT = DB_ID(); DBCC FLUSHPROCINDB(@myDb)"); b) 生产环境的解决方法,因为 sp_create_plan_guide 不适用于 SQL EXPRESS。我不确定这是否是 Enterprise 中的问题,但我开始认为这必须在生产中进行。非常感谢! 此链接似乎已失效。是否可以找到相关信息,并将信息添加到答案中而不仅仅是一个链接?看来@JerónimoVargas 评论中的信息可能就是所有需要的信息,但暗示存在更多信息。

以上是关于实体框架缓存的查询计划性能随参数不同而降低的主要内容,如果未能解决你的问题,请参考以下文章

MSSQL 查询执行计划缓存和 JDBC

Oracle 使用 PARALLEL 优化计划降低查询性能

避免针对具有不同注释的相同查询的计划缓存膨胀

SQL Server 执行计划缓存

衡量查询性能:“执行计划查询成本”与“所用时间”

SqlServer 中如何查看某一个Sql语句是复用了执行计划,还是重新生成了执行计划