【问题标题】:OPTIMIZE FOR UNKNOWN on specific EF Core query针对特定 EF Core 查询优化 UNKNOWN
【发布时间】:2021-01-30 20:16:47
【问题描述】:

我有一个使用 EF Core 3.1 在 .NET Framework 上运行的 webjob 项目。 Webjob 处理来自 Azure 服务总线的消息并将它们保存到 Azure SQL 数据库中。

我遇到的问题是 Azure SQL 数据库为 EF Core 生成的查询生成了非常糟糕的查询计划。使用生成的查询计划,执行时间为 1-2 分钟。但是,当我使用 OPTION (OPTIMIZE FOR UNKNOWN) 时,执行时间会下降到 0.01 - 0.02 分钟。

所以现在我想在 EF Core 中实现 OPTION (OPTIMIZE FOR UNKNOWN)。我发现他们在 EF Core 3.1 中添加了一个DbCommandInterceptor,您可以在其中将内容附加到您的查询中:MSDOCS

public class HintCommandInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<DbDataReader> result)
    {
        // Manipulate the command text, etc. here...
        command.CommandText += " OPTION (OPTIMIZE FOR UNKNOWN)";
        return result;
    }
}

但似乎这个拦截器会在每个查询上运行,我只希望它用于特定查询。 我可以为这个拦截器实现一个单独的 DbContext ,但这似乎不是一个可靠的解决方案。 有谁知道我如何以正确的方式实现这一点?

【问题讨论】:

  • 首先你应该明白,如果使用这个查询提示,你更有可能使这个查询的整体性能变慢而不是变快。您目前正在测试的参数可能很快,但不适用于大多数查询。请看看这篇很棒的帖子brentozar.com/archive/2013/06/… 但无论如何,有一个非常好的方法:stackoverflow.com/a/35104542/6696265
  • 我知道整体性能会更差。但平均性能可能会更好,因为大型查询不再需要 1-2 分钟。

标签: c# entity-framework entity-framework-core


【解决方案1】:

我创建了一个界面:

public interface IInterceptable
{
    bool EnableCommandInterceptors { get; set; }
}

并在我的上下文类中实现它:

public bool EnableCommandInterceptors { get; set; }

在拦截器中我有:

public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
    if(command.CommandText.StartsWith("SELECT") 
        && eventData.Context is IInterceptable intercepatable
        && intercepatable.EnableCommandInterceptors)
    {
        command.CommandText += " OPTION (OPTIMIZE FOR UNKNOWN)";
    }
    return result;
}

这允许打开和关闭此功能,如果此特定查询是上下文实例将运行的唯一查询,这可能就足够了。如果没有,您可以在if(command.CommandText.StartsWith("SELECT") 部分添加更多条件。

另一种方法是使用.TagWith 标记特定查询,并在拦截器中查找标记文本:

if (command.CommandText.StartsWith("SELECT") 
        && command.CommandText.Contains("my tagged text"))
{
    command.CommandText += " OPTION (OPTIMIZE FOR UNKNOWN)";
}

【讨论】:

  • 不幸的是,上下文中有多个选择。我刚刚找到了扩展方法TagWith 也许我可以用它来区分查询?
【解决方案2】:

我想追加到Gert Arnold's post...

如果您希望同步和 async 方法都起作用,例如 ToListAsync(),那么您也需要重载 Async 版本。

public class OptimizeForUnknownInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        command.CommandText += " OPTION (OPTIMIZE FOR UNKNOWN)";
        return base.ReaderExecuting(command, eventData, result);
    }

    public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = new CancellationToken())
    {
        command.CommandText += " OPTION (OPTIMIZE FOR UNKNOWN)";
        return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
    }
}

我还以更有针对性的方式使用这个拦截器。所以我可以将它应用到单个上下文:

var builder = new DbContextOptionsBuilder<CustomModelContext>();
builder.AddInterceptors(new OptimizeForUnknownInterceptor());

// Includes IConfiguration for appsettings ConnectionStrings, using dependency injection
await using (var db = new CustomModelContext(builder.Options, _configuration))
{
    ...
    var lst = await query.ToListAsync();
    ...
}

使用它,我为 CustomModelContext 的所有构造函数创建了一个部分:

public partial class CustomModelContext
{
    private readonly IConfiguration _configuration;

    public CustomModelContext(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public CustomModelContext(DbContextOptions<CustomModelContext> options, IConfiguration configuration)
        : base(options)
    {
        _configuration = configuration;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        if (!optionsBuilder.IsConfigured)
            optionsBuilder.UseSqlServer(_configuration.GetConnectionString("CustomModelConnection"));
    }

}

作为参考,我使用的是 .NET 5 和 EF Core 5

【讨论】:

    猜你喜欢
    • 2020-05-18
    • 2022-01-14
    • 1970-01-01
    • 2010-09-13
    • 2021-12-12
    • 2021-09-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多