【问题标题】:Troubleshooting LINQ Query Performance in ASP.NET MVC对 ASP.NET MVC 中的 LINQ 查询性能进行故障排除
【发布时间】:2018-09-27 23:12:29
【问题描述】:

在将新表加入到 LINQ 查询后,我遇到了问题。实际上,它返回了我期望的数据,并且在测试中运行得很快。但是,似乎随着更多用户连接到数据库,查询开始超时。例如,在生产的前 30 或 45 分钟一切正常,但在上午 8:20 左右,它开始超时。同样,我认为这是由于整体上数据库的使用量增加所致。

这里有一点关于 ASP.NET MVC (5) 应用程序的背景知识,以防万一。

  • 用户向我们的诊所提交了转诊
  • 推荐包含一个或多个订单
  • 如果提供的人员信息与现有人员不匹配,我会做几件事,包括在“订单”表中插入记录(推荐中选择的每个订单对应一条记录)。
  • 如果提供的人员信息与我们系统中的现有人员匹配,则我将推荐“保留”在队列中,直到通过将其与现有人员匹配或覆盖它并在其中创建新人员手动解决它系统。此时,推荐中选择的任何订单都会在表格中创建。

因此,在这种情况下要考虑的两个主要表是“referral”(在我的代码中命名为“Referrals”)和“order”(在我的代码中命名为“ReferralPPA”)表。到目前为止,我不需要将相关查询从 Referrals 表链接到 ReferralPPA 表(将查询链接到 ReferralPPA 表似乎是在数据库/应用程序使用量增加时会减慢查询速度的原因)。

此外,如果这有帮助,推荐是由外部用户输入的,而我从推荐中创建的订单在一个单独的应用程序中工作,内部员工作为用户,尽管它们都在同一个数据库中。 ReferralPPA 表可能在一天中的大部分时间都被大量使用。

查询如下所示:

            IQueryable<ReferralListViewModel> referrals = (from r in _context.Referrals
                                                           join cu in _context.ClinicUsers on r.ClinicId equals cu.ClinicId
                                                           /* Here is the seemingly problematic join */ 
                                                           from ppa in _context.ReferralPPAs
                                                                        .Where(p => p.ref_id == r.seq_no.ToString())
                                                                        .DefaultIfEmpty()
                                                           /* End of seemingly problematic join */
                                                           join ec in _context.EnrolledClinics on r.ClinicId equals ec.ClinicId
                                                           join pm in _context.ProviderMasters on ec.ClinicId equals pm.ClinicId
                                                           join ml in _context.MasterLists on pm.HealthSystemGuid equals ml.Id
                                                           join au in _context.Users on r.ApplicationUserId equals au.Id
                                                           where cu.UserId == userId
                                                           select new ReferralListViewModel()
                                                              {
                                                                  ClinicName = pm.Description,
                                                                  ClinicId = r.ClinicId,
                                                                  ReferralId = r.seq_no,
                                                                  EnteredBy = (au.FirstName ?? string.Empty) + " " + (au.LastName ?? string.Empty),
                                                                  PatientName = (r.LastName ?? string.Empty) + ", " + (r.FirstName ?? string.Empty),
                                                                  DateEntered = r.create_timestamp,
                                                                  Status = ppa != null ? ppa.Status : string.Empty
                                                              });

因此,如果没有我上面提到的连接,我不会遇到任何问题,而且它运行得非常快。添加连接似乎也很快,直到系统上有一定数量的用户(至少这是我的假设)。

我还尝试了其他一些方法来帮助提高性能并防止出现问题。我将 UseDatabaseNullSemantics 设置为 True,这似乎对整体性能产生了很大影响。

_context.Configuration.UseDatabaseNullSemantics = true;

我还想知道问题是否是对相关表的锁定问题,因此我尝试将查询包装在事务中以执行 ReadUncommitted。

            using (var transaction = _context.Database.BeginTransaction(System.Data.IsolationLevel.ReadUncommitted))
            {
              //query
            }

同样,虽然这稍微提高了整体性能,但似乎并没有最终解决问题。

如果有人对如何解决这个问题有任何想法、想法或建议,我将不胜感激。

【问题讨论】:

  • 我对这种形式的 LINQ 不是很熟悉,但是你为什么要做一个 .Where 而不是另一个加入呢?它没有左连接吗?其他想法是您似乎正在通过调用 .ToString 来更改数据类型,这会很快变得非常昂贵
  • 不要害怕将其中一些复杂的 sql 查询作为 procs 放入数据库中。由于已经建立了执行计划,它们可以被预编译并运行得更快。仅仅因为您可以使用 Linq 构建复杂的查询并不意味着是最好的方法。
  • 为了获得 LEFT OUTER JOIN(这是我在这里需要的),您需要在加入时调用 DefaultIfEmpty()。这是似乎有效的语法示例之一。关于 .ToString(),我知道这很昂贵,但这是在我们的系统中完成的,不管你信不信,这不是问题。我什至在另一个查询中执行此操作,该查询在同一页面加载时被调用,并且运行速度很快。感谢您的参与!
  • @Kevbo - 如果我不在控制器中进行搜索、排序和分页,我可以这样做,这个查询就在里面。这只是核心查询,但如果使用了这些功能中的任何一个,就会被修改。
  • 没问题,这就是我们在这里的原因 :) 现在,问题是没有官方方法可以删除演员表。我正在考虑一些骇客,当有具体的事情时会告诉你。

标签: c# sql-server asp.net-mvc entity-framework linq


【解决方案1】:

根据来自 cmets 的附加信息,看起来像是连接条件中的 GuidString 的转换

p.ref_id == r.seq_no.ToString()

翻译成

t1.ref_id = LOWER(CAST(t2.seq_no AS nvarchar(max))))

使查询不可 sargable,而隐式 SqlServer 转换

t1.ref_id = t2.seq_no

工作得很好。

所以问题是如何删除该演员表。没有选项,查询表达式树也不允许删除它。如果 SqlServer 提供程序 sql 生成器正在执行该优化,那就太好了,但它没有,也没有简单的方法来挂钩。

作为一种解决方法,我可以提供以下解决方案。它使用自定义的IDbCommandTreeInterceptorDbExpressionVisitor 来修改查询的DbCommandTree

拦截代码如下:

using System;
using System.Data.Entity.Core.Common.CommandTrees;
using System.Data.Entity.Core.Common.CommandTrees.ExpressionBuilder;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure.Interception;
using System.Linq.Expressions;
using System.Reflection;

namespace EFHacks
{
    public class MyDbCommandTreeInterceptor : IDbCommandTreeInterceptor
    {
        public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
        {
            if (interceptionContext.OriginalResult.DataSpace != DataSpace.SSpace) return;
            var queryCommand = interceptionContext.Result as DbQueryCommandTree;
            if (queryCommand != null)
            {
                var newQuery = queryCommand.Query.Accept(new GuidToStringComparisonRewriter());
                if (newQuery != queryCommand.Query)
                {
                    interceptionContext.Result = new DbQueryCommandTree(
                        queryCommand.MetadataWorkspace,
                        queryCommand.DataSpace,
                        newQuery);
                }
            }
        }
    }

    class GuidToStringComparisonRewriter : DefaultExpressionVisitor
    {
        public override DbExpression Visit(DbComparisonExpression expression)
        {
            if (IsString(expression.Left.ResultType) && IsString(expression.Right.ResultType))
            {
                var left = expression.Left;
                var right = expression.Right;
                if (RemoveCast(ref right) || RemoveCast(ref left))
                    return CreateComparison(expression.ExpressionKind, left, right);
            }
            return base.Visit(expression);
        }

        static bool IsGuid(TypeUsage type)
        {
            var pt = type.EdmType as PrimitiveType;
            return pt != null && pt.PrimitiveTypeKind == PrimitiveTypeKind.Guid;
        }

        static bool IsString(TypeUsage type)
        {
            var pt = type.EdmType as PrimitiveType;
            return pt != null && pt.PrimitiveTypeKind == PrimitiveTypeKind.String;
        }

        static bool RemoveCast(ref DbExpression expr)
        {
            var funcExpr = expr as DbFunctionExpression;
            if (funcExpr != null &&
                funcExpr.Function.BuiltInTypeKind == BuiltInTypeKind.EdmFunction &&
                funcExpr.Function.FullName == "Edm.ToLower" &&
                funcExpr.Arguments.Count == 1)
            {
                var castExpr = funcExpr.Arguments[0] as DbCastExpression;
                if (castExpr != null && IsGuid(castExpr.Argument.ResultType))   
                {
                    expr = castExpr.Argument;
                    return true;
                }
            }
            return false;
        }

        static readonly Func<DbExpressionKind, DbExpression, DbExpression, DbComparisonExpression> CreateComparison = BuildCreateComparisonFunc();

        static Func<DbExpressionKind, DbExpression, DbExpression, DbComparisonExpression> BuildCreateComparisonFunc()
        {
            var kind = Expression.Parameter(typeof(DbExpressionKind), "kind");
            var booleanResultType = Expression.Field(null, typeof(DbExpressionBuilder), "_booleanType");
            var left = Expression.Parameter(typeof(DbExpression), "left");
            var right = Expression.Parameter(typeof(DbExpression), "right");
            var result = Expression.New(
                typeof(DbComparisonExpression).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null,
                new[] { kind.Type, booleanResultType.Type, left.Type, right.Type }, null),
                kind, booleanResultType, left, right);
            var expr = Expression.Lambda<Func<DbExpressionKind, DbExpression, DbExpression, DbComparisonExpression>>(
                result, kind, left, right);
            return expr.Compile();
        }
    }
}

DbConfiguration 安装它:

class MyDbConfiguration : DbConfiguration
{
    public MyDbConfiguration()
    {
        AddInterceptor(new EFHacks.MyDbCommandTreeInterceptor());
    }
}

使用 SqlServer 数据库在 EF6.1.3 和 EF6.2 中测试和工作。

但请谨慎使用。

首先,它仅适用于 SqlServer。

其次,这很骇人听闻,因为我必须使用内部字段和内部类构造函数来跳过检查比较操作操作数的相等类型。所以未来的一些 EF6 更新可能会破坏它。

【讨论】:

  • 这看起来很有希望,但代码没有为我编译。我不得不承认——这看起来有点吓人,所以也许我只是有点害羞。另外,我把 MyDbConfiguration 类放在哪里重要吗?我正在使用带有 MVC 5 的 EF 6.1.3,如果这有什么不同的话。
  • 是的,确实如此。它应该与 db 上下文在同一个程序集中。代码本身只不过是智能查找和替换 - 找到 ToLower(Cast(guidExpr, string)) 并将其替换为 guidExpr :) 编译怎么样,代码使用 C#7,但如果你告诉它,可以很容易地使用旧式结构我什么不适合你。
  • 啊,一定是这样。在 TreeCreated 中,queryCommand 带有下划线(首先表示它需要一个右括号,而所有其他实例都表示它在此上下文中无法识别)。 Visit 方法下面的静态布尔也没有编译。它必须只是 C# 7 的东西,因为每一行都有下划线。有屏幕截图会有帮助吗?让我知道如何为您提供更好的信息。
  • 一旦你开始使用这些新的小语言糖,就很难回头了 :) 无论如何,现在它应该在旧的 C# 中编译得很好。
  • 我能够解决这个问题,现在我已经开始使用您提供的这个解决方案。我讨厌使用惊人这个词,但这个“hack”的影响实际上是惊人的。因为我们在整个系统中存储为 GUID 的 UNIQUEIDENTIFIER 存在这个问题,这似乎极大地提高了应用程序的整体性能。老实说,以前的性能非常好(除了这个新问题),但现在页面加载以毫秒为单位。如果您有兴趣浏览代码,我很想进一步了解它的工作原理。这太棒了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-03
  • 2012-03-16
  • 1970-01-01
相关资源
最近更新 更多