【问题标题】:How to improve performance of ordering list multiple times after getting data from database从数据库中获取数据后如何多次提高排序列表的性能
【发布时间】:2019-02-12 07:53:32
【问题描述】:

我刚刚遇到了排序列表性能非常慢的问题。对数据库运行查询后,我得到结果,需要订购 4 次才能满足要求。查询运行得非常快,我几乎可以立即获得所有结果,但是对记录进行排序几乎需要 8 秒。

我也在使用分页,因此每次我每页只选择 50 条记录,但我必须一次又一次地重新排序整个列表,这是一场噩梦。你们有什么办法让它跑得更快吗?

var studentMessages = context.Students
    .Where(s => s.SchoolId == SchoolId).ToList();
var sSorted = studentMessages
    .Where(x => x.message == null && x.student.Status != (int)StudentStatusEnum.NotActive)
    .OrderByDescending(x => x.student.UserId)
    .ToList();

sSorted = sSorted
    .Concat(studentMessages
        .Where(x => x.message != null && x.student.Status != (int)StudentStatusEnum.NotActive)
        .OrderBy(x => x.message.NextFollowUpDate)
        .ToList()
    ).ToList();

sSorted = sSorted
    .Concat(studentMessages
        .Where(x => x.message != null && x.student.Status == (int)StudentStatusEnum.NotActive)
        .OrderByDescending(x => x.message.NextFollowUpDate)
        .ToList()
    ).ToList();

sSorted = sSorted
    .Concat(studentMessages
    .Where(x => x.message == null && x.student.Status == (int)StudentStatusEnum.NotActive)
    .OrderByDescending(x => x.user.Id)
    .ToList()
    ).ToList();

var allStudents = (isSelectAll == true ? sSorted  : sSorted .Skip(skipNumber).Take(query.AmountEachPage)).ToList();

【问题讨论】:

  • 你能说明你是如何分配studentMessages的吗?查询是否已在分配时针对数据库执行,或者您是否只分配查询(每当您调用 ToList 时执行 4 次)?
  • 如果您需要内存中的整个列表进行重新排序,分页并没有多大帮助......
  • @DevilSuichiro 是的,没错,这几乎毫无意义,因为我仍然一遍又一遍地得到所有结果,这就是我公司的设计方式,但我刚刚开始我的编码之旅,所以不确定如何让它变得更好
  • @Markus 这只是一个简单的查询 var studentMessages = context.Students .Where(s => s.SchoolId == SchoolId) .ToList();
  • student.UserIdmessage.NextFollowUpDate 属性的类型是什么?

标签: c# performance entity-framework linq entity-framework-6


【解决方案1】:

我认为你的问题的原因是因为你得到了子集或你的序列并订购了这个子集。您这样做了好几次,然后决定列出所有中间结果。

让我们先看看你想如何安排你的学生。

所以你有一个schoolId 和一个Students 的序列。每个Student 都有属性SchoolIdMessageStatus

你把学校里的所有StudentsschoolId 联系起来,出于某种原因你决定给这些学生打电话studentMessages

那么您想按以下顺序订购这些Students(学生消息):

  • 首先是所有消息为空且状态不等于 notActive 的学生,按降序排列 UserId
  • 然后所有具有非空消息且状态不等于 notActive 的学生,按 Message.NextFollowUpdate 排序
  • 然后所有具有非空消息且状态等于 notActive 的学生,按 Message.NextFollowUpdate 排序
  • 最后,所有消息为 null 且状态等于 notActive 的学生,按降序排列 User.Id(确定您不是指 UserId?我认为应该是一样的)

在表格中:

group | Message |  Status      | Order by
  1   | == null | != notActive | descending UserId
  2   | != null | != notActive | ascending  message.NextFollowUpdate
  3   | != null | == notActive | descending message.NextFollowUpdate
  4   | == null | == notActive | ascending  UserId

其中一种方法是让您的数据库管理系统执行此操作(AsQueryable)。排序算法似乎相当复杂。我不确定 DBMS 是否可以比您的流程更有效地做到这一点。

另一种方法是仅获取您实际需要的学生并让您的进程进行排序 (AsEnumerable)。提供一个实现IComparer<Student> 的类来决定顺序。

int schoolId = ...
IComparer<Student> mySpecialStudentComparer = ...
var orderedStudents = dbContext.Students
    .Where(student => student.SchoolId == schoolId)
    .AsEnumerable()         // move the selected data to local process

    // now that the data is local, we can use our local Student Comparer
    .OrderBy(mySpecialStudentComparer);

如果您的 Student 有很多属性在您获取数据后不会使用,请考虑创建一个仅包含您需要的属性的本地类,并将所选数据限制为该本地类,例如 FetchedStudent

    .Select(student => new FetchedStudent
    {
        // select only the properties you actually plan to use,

        // for the sorting we need at least the following:
        Message = student.Message,
        Status = student.Status
        UserId = student.UserId,

        // Select the other Student properties you plan to use, for example:
        Id = student.Id,
        Name = student.Name, 
        ...
    }

当然,在这种情况下,您的比较器需要实现IComparer&lt;FetchedStudent&gt;

让我们创建一个StudentComparer,它会根据您的要求对学生进行排序!

class StudentComparer : IComparer<FetchedStudent>
{
    private readonly IComparer<int> UserIdComparer = Comparer<int>.Default;
    private readonly IComparer<DateTime> nextFollowUpdateComparer =
                     Comparer<DateTime>.Default;

    public int CompareTo(FetchedStudent x, FetchedStudent y)
    {
        // TODO: decide what to do with null students: exception?
        // or return as smallest or largest

        // Case 1: check if x is in sorting group 1
        if (x.Message == null && x.Status == notActive)
        {
            // x is in sorting group 1
            if (y.Message == null && y.Status == notActive)
            {
                // x and y are in sorting group 1.
                // order by descending UserId
                return -UserIdComparer.CompareTo(x.UserId, y.UserId);
                // the minus sign is because of the descending
            }
            else
            {   // x is in group 1, y in group 2 / 3 / 4: x comes first
                return -1;
            }
        }

        // case 2: check if X is in sorting group 2
        else if (x.Message != null && x.Status != notActive)
        {   // x is in sorting group 2
            if (y.Message == null && y.Status != notActive)
            {   // x is in group 2; y is in group 1: x is larger than y
                return +1;
            }
            else if (y.Message == null && y.Status != notActive)
            {   // x and y both in group 2: order by descending nextFollowUpDate
                // minus sign is because descending
                return -nextFollowUpdateComparer.CompareTo(
                       x.Message.NextFollowUpdate,
                       y.Message.NextFollowUpdate);
            }
            else
            {   // x in group 2, y in 3 or 4: x comes first
                return -1;
            }
        }

        // case 3: check if X in sorting group 3
        else if (x.Message == null && x.Status != notActive)
        {
           ... etc, you'll know the drill by know
    }
}    

可能的改进

您看到比较器不断比较x.Message是否等于null,x.Status是否等于notActive,以检测x和y属于哪个排序组。

考虑创建一个函数,只计算一次学生属于哪个排序组并记住排序组:

.Select(student => new FetchedStudent
{
    SortingGroup = student.ToSortingGroup(),

    ... // other properties you need
}

public int CompareTo(FetchedStudent x, FetchedStudent y)
{
    switch (x.SortingGroup)
    {
        case 1:
           switch y.SortingGroup:
           {
               case 1: // x and y both in sorting group 1
                  return -UserIdComparer.CompareTo(x.UserId, y.UserId);

               default: // x in sorting group 1, y in 2 / 3 / 4: x smaller
                  return -1;
           }
        case 2:
           switch y.SortingGroup:
           {
               case 1: // x in sorting group 2; y in sorting group 1: x larger
                  return +1;
               case 2: // x and y both in sorting group 2
                  return -nextFollowUpdateComparer.CompareTo(
                       x.Message.NextFollowUpdate,
                       y.Message.NextFollowUpdate);
           }    

等等。这样与 Message 和 Status 的比较只进行一次

【讨论】:

  • 这是一个令人惊叹且非常彻底的解释,非常感谢您抽出宝贵的时间,我真的很感激!
【解决方案2】:

代码的性能问题很可能是延迟加载的结果。通过使用studentmessage 属性(在第四个查询的情况下还使用user 属性),再次查询数据库的每一行。 studentMessage 包含的行越多,代码执行的速度就越慢。这是一个所谓的“n+1 SELECTs”问题。有关详细信息,请参阅此link

如果你想快速解决问题,你需要断言相关的子实体也加载了第一个请求。为此,您需要更改以下行并包含所有相关实体:

var studentMessages = context.Students
  .Where(s => s.SchoolId == SchoolId)
  .ToList();    

应进行更改,以便还包括实体 messageuserstudent

var studentMessages = context.Students
  .Include(x => x.message)
  .Include(x => x.student)
  .Include(x => x.user)
  .Where(s => s.SchoolId == SchoolId)
  .ToList();    

这样,数据是通过对数据库的一个请求加载的,而不是稍后加载。

【讨论】:

  • @Edward 很高兴听到它有帮助。祝你有美好的一天!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-05-01
  • 2011-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多