首页 > 解决方案 > 优化咨询服务

问题描述

我在带有 linq 的 DataGridView 中进行了以下搜索,但搜索速度太慢,如何优化此查询或为什么会发生这种情况?DataGridView 中有 800 条记录

private void txtBusqueda_TextChanged(object sender, EventArgs e)
{
    var filtro = (from p in Global.ArticulosGlobales 
                  where p.codigo.ToUpper().StartsWith(txtBusqueda.Text.ToUpper()) 
                  select p).ToList();
    dtGridDatos.DataSource = filtro;
}

标签: c#

解决方案


链接到实体

在查询中对列使用函数会使谓词不可 SARGable(如果您有物化视图或索引,则有例外,但我认为这在这种情况下不适用)。

尽管如此,鉴于 ISO SQL 默认情况下不区分大小写,这意味着您根本不需要执行大小写转换,因为 Linq 查询中的所有字符串比较都将不区分大小写,除非您使用的是非默认排序规则,即明确区分大小写。

还要确保您对codigo列进行了索引,以便前缀搜索是最佳的:

CREATE INDEX IX_codigo ON dbo.ArticulosGlobales ( codigo );

所以这会做:

String prefix = this.txtBusqueda.Text;

var filtro = await Global.ArticulosGlobales
    .Where( p => p.codigo.StartsWith( prefix ) )
    .ToListAsync()
    .ConfigureAwait(true); // <-- Explicit continuation on UI thread.

dtGridDatos.DataSource = filtro;

Linq 到对象

与 Linq-to-Entities 一样, usingToUpper()是执行不区分大小写的字符串比较的最糟糕方法 - 在 lambda 中使用它也意味着过多的字符串分配,这是一件坏事。相反,使用StringComparer.OrdinalIgnoreCase(或者StringComparison.OrdinalIgnoreCase如果它是一个参数)。

此外,保存this.txtBusqueda.Text到一个临时变量,因为.Text访问TextBox器是非平凡的并执行线程访问检查,这进一步减慢了速度。

String prefix = this.txtBusqueda.Text;

var filtro = Global.ArticulosGlobales
    .Where( p => p.codigo.StartsWith( prefix, StringComparison.OrdinalIgnoreCase ) )
    .ToList()

dtGridDatos.DataSource = filtro;

进一步优化:缓存之前的结果

我假设这是一个 find-as-you-type 过滤器 - 在这种情况下请注意,当用户输入一个新字母时,每个后续结果集都是前一个结果集的子集,这意味着您只需要访问数据库一次并将这些结果存储在内存中(当然,将它们存储在前缀搜索优化的结构中)。然后,所有后续更具体的搜索将对最后获得的子集进行操作。

一个初始优化,仍然可以极大地改进,可能如下所示:

private readonly Dictionary< String, List<ArticulosGlobales > > searchSubsets = new Dictionary< String, List<ArticulosGlobales > >( StringComparer.OrdinalIgnoreCase );

private String connectionString = "...";

private async Task LoadAsync()
{
    List<ArticulosGlobales> all;
    using( MyDbContext db = new MyDbContext( this.connectionString ) )
    {
        all = await db.ArticulosGlobales
            .OrderBy( p => p.codigo )
            .ToListAsync()
            .ConfigureAwaita(false);
    }

    this.searchSubsets.Clear();
    this.searchSubsets[ String.Empty ] = all;
}

private async void txtBusqueda_TextChanged(object sender, EventArgs e)
{
    if( this.searchSubsets.Count == 0 )
    {
        await this.LoadAsync();
    }

    String search = ((TextBox)sender).Text;
    
    // Has this search already been performed?
    if( this.searchSubsets.TryGetValue( search, out List<ArticulosGlobales> cachedResults ) )
    {
        dtGridDatos.DataSource = cachedResults;
        return;
    }
    
    // Is there a previous search we can take a subset of?
    for( Int32 i = search.Length - 2; i >= 0; --i ) 
    {
        String prefix = search.Substring( 0, i );
        if( this.searchSubsets.TryGetValue( prefix, out List<ArticulosGlobales> prefixResults )
        {
            List<ArticulosGlobales> subset = prefixResults
                .Where( p => p.codigo.StartsWith( search, StringComparison.OrdinalIgnoreCase ) )
                .ToList();

            this.searchSubsets[ search ] = subset;
            dtGridDatos.DataSource = cachedResults;
            return;
        }
    }

    // Program will never get to this point because the `for` loop above will eventually use an empty-string with `this.searchSubsets.TryGetValue` which *will* have a result, so just throw an exception here instead to keep the C# compiler happy:
    throw new InvalidOperationException( "This will never be thrown." );
}

推荐阅读