【问题标题】:Optimize EF core query in an alphabetically ordered list在按字母顺序排列的列表中优化 EF 核心查询
【发布时间】:2019-06-07 07:04:41
【问题描述】:

我最近一直在处理一个问题,虽然我有一些解决方案,但我想从各个角度找到最好的解决方案。

假设我有一个带有 EF Core 的 WPF 应用程序。我的数据库中有大约 3000 个客户(在我的情况下是 SQLite,但将来这也应该适用于速度较慢的客户)。当用户打开客户列表时,我仅按字母顺序加载其中一些(数量 = 50,页面 = 0)。一旦用户向下滚动到底部,就会再加载 50 个(数量 = 50,页面 = 1)。

CustomerRepository.GetQueryableAll().Skip(page * quantity).Take(quantity).ToList();

一切正常。但是问题来了:有一个按钮可以创建一个新客户,它会打开一个模式窗口。假设用户创建了一个以 W 开头的客户。一旦他/她点击 SAVE,新客户就会保存到数据库中,窗口关闭,并且必须重新加载列表。但是,将整个列表加载到 W 之前当然非常慢。

到目前为止,我已经尝试在后台任务中查询数据库,并将有多少客户以数据库的每个字母开头存储在静态字典中:只要点击 SAVE,我就能或多或少猜出多少“pages”到数据库中的 Skip() 并获取新客户所在的 50 组。它有效,速度相当快,但我担心它在非拉丁字母的国家/地区不起作用:

public async Task<Dictionary<char, int>> GetCustomersByInitialsCount()
{
    return await Task.Run(async delegate
    {
        var dictionary = new Dictionary<char, int>();
        for (char c = 'A'; c <= 'Z'; c++)
        {
            var count = await CustomerRepository.GetCustomerCountStartingWith(c.ToString());
            dictionary.Add(c, count);
        }
        return dictionary;
    });
}

[... and in the repository:]

public async Task<int> GetCustomerCountStartingWith(string startingLetter)
{
    using (var dbContext = new MyDbContext())
    {
        return await dbContext.Set<Customer>().CountAsync(p => p.LastName.ToUpper().StartsWith(startingLetter.ToUpper()));
    }
}

否则,除了这个后台查询,我还可以尝试根据起始字符“猜测”正确的页面,但我仍然对非拉丁语言可能产生的意外结果感到困惑。

如果有人知道更好的工具或有任何其他有用的想法,我很乐意考虑!

非常感谢您提前祝您编码愉快。

【问题讨论】:

  • 如果你有无限分页和按pageIndex分页怎么办。在新用户之后,您只需从当前页面加载列表?

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


【解决方案1】:

如果您添加一个请求以获取表中的所有第一个“字母”怎么办?

public async Task<List<string>> GetCustomerFirstLetter()
{
    using (var dbContext = new MyDbContext())
    {
        return await dbContext.Set<Customer>().Select(x => x.lastName.Substring(0, 1)).Distinct().ToList();
    }
}

然后

public async Task<Dictionary<char, int>> GetCustomersByInitialsCount()
{
    return await Task.Run(async delegate
    {
        var dictionary = new Dictionary<char, int>();
        var letters = GetCustomerFirstLetter();
        foreach(letter in letters)
        {
            var count = await CustomerRepository.GetCustomerCountStartingWith(letter);
            dictionary.Add(letter, count);
        }
        return dictionary;
    });
}

【讨论】:

    【解决方案2】:

    替代解决方案从我的角度来看,效率更高一些

    您的问题归结为如何在按客户姓名排序的整个数据集中获取新客户的行号。

    首先,对于 SQLite 或 MSSQL,简单的 SQL 可以解决您使用 ROW_NUMBER 函数获取正确页码的问题。查询示例:

    SELECT TOP 1 rnd.rownum, rnd.LastName
      from (SELECT  ROW_NUMBER() OVER( ORDER BY c.LastName) AS rownum, c.LastName
      FROM [Customer] c) rnd
    WHERE rnd.LastName = '<your new customers name here>'
    

    因此,在获得准确的 rownumber 值并已有页数参数后,您可以轻松计算所需的页数。

    回到您的代码。此功能可以在 EF 中通过 Select 方法的重载版本实现,但不幸的是,它尚未在 IQueryable 的 EF Core 中实现 (see this)。 但是您仍然可以使用 FromSql 方法将精确查询权传递给 db。

    解决方案包括两个步骤:

    1. 要获取所需的数据,您需要以这种方式为模型构建器定义Query(例如,附加字段,您只需要RowNum):

      protected override void OnModelCreating(ModelBuilder modelBuilder)
      {
          modelBuilder.Query<CustomerRownNum>();
      }
      
      public class CustomerRownNum
      {
          public long RowNum { get; set; }
          public Guid Id { get; set; }
          public string LastName { get; set; }
      }
      
    2. 那么你需要通过这种方式将上述 SQL 查询传递给上下文的 Query 方法:

          string customerLastName = "<your customer's last name>";
          var result = dbContext.Query<CustomerRownNum>().FromSql(
              @"select top 1 rnd.RowNum, rnd.Id, rnd.LastName
                  from 
                  (SELECT  ROW_NUMBER() OVER( ORDER BY c.LastName) AS RowNum
                      , c.Id, c.LastName
                    FROM [Customer] c) rnd
                  WHERE rnd.LastName = {0}", customerLastName).FirstOrDefault();
      

    最后你会在result变量中得到你需要的数据。

    希望有帮助!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-03-29
      • 2018-09-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多