在按字母顺序排列的列表中优化 EF 核心查询

Optimize EF core query in an alphabetically ordered list

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

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

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

一切正常。但是问题来了:有一个创建新客户的按钮,它会打开一个模式 window。假设用户创建了一个以字母 W 开头的客户。一旦 he/she 点击保存,新客户就会保存到数据库中,window 关闭,并且必须重新加载列表。当然,加载整个列表直到 W 真的很慢。

到目前为止,我已经尝试在后台任务中查询数据库,并在静态词典中存储有多少客户以数据库的每个字母开头:只要点击“保存”,我就能猜出或多或少有多少 "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()));
    }
}

否则,除了这个后台查询,我还可以尝试 "guess" 正确的页面,具体取决于起始字符,但我仍然对使用非拉丁语言可能产生的意外结果感到困惑。

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

非常感谢您,祝您编码愉快。

如果您添加一个请求以获取 table 中的所有前 "letters" 怎么办?

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;
    });
}

备选方案从我的角度来看效率更高一点

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

首先,对于 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>'

因此,在获得准确的行号值和页数参数后,您可以轻松计算出所需的页数。

回到你的代码。此功能可以在 EF 中使用 Select 方法的重载版本实现,但不幸的是,它尚未在 EF Core 中实现 IQueryable (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查询通过这种方式传递给context的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 变量中获得所需的数据。

希望对您有所帮助!