无法使用where子句将DB索引与Entity Framework一起使用

时间:2017-11-22 09:27:34

标签: c# .net sql-server performance entity-framework

我们正在使用实体框架来处理包含5000万个条目的表(在Microsoft SQL Server上)。

public async Task<List<TableName>> MyMethod(int fooId, int count)
{
    using (var context = new Context(ConnectionString))
    {
        return
            await context.TableName.AsNoTracking()
                .Where(_ => _.FooId== fooId)
                .OrderByDescending(_ => _.DateCreated)
                .Take(count)
                .ToListAsync();
    }
}

实体框架将其转换为(美化):

declare @fooId int = 42
SELECT TOP (100) *
FROM TableName
WHERE FooId = @fooId
ORDER BY DateCreated DESC

列FooId和DateCreated都有一个索引,但SQL Server无论如何都会进行全表扫描,这需要很长时间。这是因为在语句之外分配了42(并且似乎与选择性有关)。如果你写了

,将使用索引
WHERE FooId = 42

有没有办法让Entity Framework优化生成的查询?目前,我的唯一方法似乎是在我的代码中对大型表使用原始SQL查询: - / 有更好的解决方法吗?

编辑: 评论中要求的更多细节: 由实体框架生成的非美化查询:

SELECT TOP (100) 
    [Project1].[DateCreated] AS [DateCreated], 
    [Project1].[FooId] AS [FooId]
    FROM ( SELECT 
        [Extent1].[DateCreated] AS [DateCreated], 
        [Extent1].[FooId] AS [FooId]
        FROM [dbo].[TableName] AS [Extent1]
        WHERE [Extent1].[FooId] = @p__linq__0
    )  AS [Project1]
    ORDER BY [Project1].[DateCreated] DESC

-- p__linq__0: '42' (Type = Int32, IsNullable = false) 

索引的创建脚本:

CREATE NONCLUSTERED INDEX [IX_TableName_FooId] ON [dbo].[TableName]
(
    [FooId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 85) ON [SECONDARY]
GO

为我的表创建脚本:

CREATE TABLE [dbo].[TableName](
    [DateCreated] [datetime] NOT NULL,
    [FooId] [int] NULL
) ON [PRIMARY]

Execution Plan

2 个答案:

答案 0 :(得分:1)

参数嗅探存在问题。

您的查询将由获取输入参数的存储过程执行:

EXEC sp_executesql N'SELECT TOP (100) *
FROM TableName
WHERE FooId = = @p__linq__0
ORDER BY DateCreated DESC', ' @p__linq__0 int', @p__linq__0 = 42

您应该更改查询以将查询参数更改为本地变量:

EXEC sp_executesql N'
    DECLARE @pp__linq__0 int = @p__linq__0
    SELECT TOP (100) *
    FROM TableName
    WHERE FooId = = @pp__linq__0
    ORDER BY DateCreated DESC', ' @p__linq__0 int', @p__linq__0 = 42

例如我们针对此问题的解决方案:

public class ParamPositioningInterceptor : DbCommandInterceptor
{
    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        if (command.CommandText.StartsWith("SELECT") && command.Parameters.Count > 0)
        {
            StringBuilder sb1 = new StringBuilder(command.CommandText);
            StringBuilder sb2 = new StringBuilder();

            SqlParameter[] array = new SqlParameter[command.Parameters.Count];
            command.Parameters.CopyTo(array, 0);

            foreach (SqlParameter p in array.OrderByDescending(x => x.ParameterName.Length))
            {
                sb1.Replace("@" + p, "@p" + p);

                switch (p.SqlDbType)
                {
                    case SqlDbType.Char:
                    case SqlDbType.VarChar:
                    case SqlDbType.NChar:
                    case SqlDbType.NVarChar:
                        sb2.AppendFormat("DECLARE @p{0} {1}({2}) = @{0}", p, p.SqlDbType, p.Size);
                        break;
                    case SqlDbType.Decimal:
                        sb2.AppendFormat("DECLARE @p{0} {1}({2},{3}) = @{0}", p, p.SqlDbType, p.Precision, p.Scale);
                        break;
                    default:
                        sb2.AppendFormat("DECLARE @p{0} {1} = @{0}", p, p.SqlDbType);
                        break;
                }

                sb2.AppendLine();
            }

            command.CommandText = sb2.Append(sb1).ToString();
        }
        //
        base.ReaderExecuting(command, interceptionContext);
    }
}

答案 1 :(得分:0)

如果您的代码执行完全如下:

declare @fooId int = 42
SELECT TOP (100) *
FROM TableName
WHERE FooId = @fooId
ORDER BY DateCreated DESC

即。没有参数,只有局部变量,估计的行数是C ^ 1/2(C =表基数),具有非唯一值。这意味着全扫描。

您可以尝试使用option(recompile)传递实际值。

这将导致在已经分配变量时重新编译语句,即执行计划将考虑您有效传递的值。

要查看实际执行的内容以及您应该提供实际执行计划的估算和实际行数。