如何加快LINQ WHERE的速度?

时间:2019-02-15 17:12:17

标签: c# performance linq lambda

我已经在.NET winforms应用程序(与.NET 4.7.1编译)上运行了探查器,它指出以下功能占用了我应用程序73%的CPU时间,对于简单的应用程序来说似乎太过分了实用功能:

public static bool DoesRecordExist(string keyColumn1, string keyColumn2, string keyColumn3,
        string keyValue1, string keyValue2, string keyValue3, DataTable dt)
{
    if (dt != null && dt.Rows.Count > 0) {
        bool exists = dt.AsEnumerable()
            .Where(r =>
                string.Equals(SafeTrim(r[keyColumn1]), keyValue1, StringComparison.CurrentCultureIgnoreCase) &&
                string.Equals(SafeTrim(r[keyColumn2]), keyValue2, StringComparison.CurrentCultureIgnoreCase) &&
                string.Equals(SafeTrim(r[keyColumn3]), keyValue3, StringComparison.CurrentCultureIgnoreCase)
            )
            .Any();
        return exists;
    } else {
        return false;
    }
}

此功能的目的是传递一些键列名称和匹配的键值,并检查内存c#DataTable中是否存在任何匹配的记录。

我的应用程序正在处理数十万条记录,对于每条记录,必须多次调用此函数。该应用程序执行了大量插入操作,并且在插入之前,它必须检查该记录是否已存在于数据库中。我发现对DataTable进行内存中的检查比每次都返回物理数据库要快得多,因此这就是为什么我要进行此内存中的检查。每次执行数据库插入操作时,都会在DataTable中进行相应的插入操作,以便随后对记录是否存在进行准确的检查。

所以我的问题是:有没有更快的方法?(我想我不能避免每次都检查记录是否存在,否则我将得到重复的插入和键违规。)

编辑#1 除了尝试我现在​​正在尝试的建议外,我还发现我也许应该只做一次.AsEnumerable()并传递EnumerableRowCollection<DataRow>而不是{ {1}}。您认为这会有所帮助吗?

编辑#2 我只是进行了一项受控测试,发现直接查询数据库以查看是否已存在记录比在内存中查找要慢得多。

5 个答案:

答案 0 :(得分:1)

您的解决方案找到在条件中评估为true的所有事件,然后询问是否存在。而是直接使用Any。将Any替换为Any。首次真正达到条件评估时,它将停止处理。

bool exists = dt.AsEnumerable().Any(r => condition);

答案 1 :(得分:1)

您应该尝试并行执行,这对您来说是一个很好的例子,因为您提到要使用庞大的集合,并且如果您只想检查记录是否已存在,则不需要有序。

bool exists = dt.AsEnumerable().AsParallel().Any((r =>
            string.Equals(SafeTrim(r[keyColumn1]), keyValue1, StringComparison.CurrentCultureIgnoreCase) &&
            string.Equals(SafeTrim(r[keyColumn2]), keyValue2, StringComparison.CurrentCultureIgnoreCase) &&
            string.Equals(SafeTrim(r[keyColumn3]), keyValue3, StringComparison.CurrentCultureIgnoreCase)
        )

答案 2 :(得分:0)

可能是您想转置数据结构。而不是使用DataTable,其中每行具有keyColumn1keyColumn2keyColumn3,而是具有3个HashSet<string>,其中第一行包含所有keyColumn1值,依此类推

这样做比遍历每一行要快得多:

var hashSetColumn1 = new HashSet<string>(
    dt.Rows.Select(x => x[keyColumn1]),
   StringComparison.CurrentCultureIgnoreCase);

var hashSetColumn2 = new HashSet<string>(
    dt.Rows.Select(x => x[keyColumn2]),
   StringComparison.CurrentCultureIgnoreCase);

var hashSetColumn3 = new HashSet<string>(
    dt.Rows.Select(x => x[keyColumn3]),
   StringComparison.CurrentCultureIgnoreCase);

很明显,只需创建一次,然后对其进行维护(因为您当前正在维护DataTable)。它们创建起来很昂贵,但查询起来很便宜。

然后:

bool exists = hashSetColumn1.Contains(keyValue1) &&
    hashSetColumn2.Contains(keyValue2) &&
    hashSetColumn3.Contains(keyValue3);

或者(更清晰地说),您可以定义自己的结构,该结构包含3列中的值,并使用单个HashSet:

public struct Row : IEquatable<Row>
{
    // Convenience
    private static readonly IEqualityComparer<string> comparer = StringComparer.CurrentCultureIngoreCase;

    public string Value1 { get; }
    public string Value2 { get; }
    public string Value3 { get; }

    public Row(string value1, string value2, string value3)
    {
        Value1 = value1;
        Value2 = value2;
        Value3 = value3;
    }

    public override bool Equals(object obj) => obj is Row row && Equals(row);

    public bool Equals(Row other)
    {
        return comparer.Equals(Value1, other.Value1) &&
               comparer.Equals(Value2, other.Value2) &&
               comparer.Equals(Value3, other.Value3);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 17;
            hash = hash * 23 + comparer.GetHashCode(Value1);
            hash = hash * 23 + comparer.GetHashCode(Value2);
            hash = hash * 23 + comparer.GetHashCode(Value3);
            return hash;
        }
    }

    public static bool operator ==(Row left, Row right) => left.Equals(right);
    public static bool operator !=(Row left, Row right) => !(left == right);
}

然后您可以创建一个:

var hashSet = new HashSet<Row>(dt.Select(x => new Row(x[keyColumn1], x[keyColumn2], x[keyColumn3]));

然后将其缓存。查询如下:

hashSet.Contains(new Row(keyValue1, keyValue2, keyValue3));

答案 3 :(得分:0)

我建议您将现有记录的关键列保留在HashSet中。我在这里使用元组,但是您也可以通过覆盖KeyGetHashCode来创建自己的Equals结构或类。

private HashSet<(string, string, string)> _existingKeys =
    new HashSet<(string, string, string)>();

然后,您可以使用以下方法快速测试密钥的存在性

if (_existingKeys.Contains((keyValue1, keyValue2, keyValue3))) {
    ...
}

请不要忘记将此HashSet与您的添加和删除保持同步。请注意,元组不能与CurrentCultureIgnoreCase进行比较。因此,要么将所有键转换为小写字母,要么使用自定义结构方法,可以在其中使用所需的比较方法。

public readonly struct Key
{
    public Key(string key1, string key2, string key3) : this()
    {
        Key1 = key1?.Trim() ?? "";
        Key2 = key2?.Trim() ?? "";
        Key3 = key3?.Trim() ?? "";
    }

    public string Key1 { get; }
    public string Key2 { get; }
    public string Key3 { get; }

    public override bool Equals(object obj)
    {
        if (!(obj is Key)) {
            return false;
        }

        var key = (Key)obj;
        return
            String.Equals(Key1, key.Key1, StringComparison.CurrentCultureIgnoreCase) &&
            String.Equals(Key2, key.Key2, StringComparison.CurrentCultureIgnoreCase) &&
            String.Equals(Key3, key.Key3, StringComparison.CurrentCultureIgnoreCase);
    }

    public override int GetHashCode()
    {
        int hashCode = -2131266610;
        unchecked {
            hashCode = hashCode * -1521134295 + StringComparer.CurrentCultureIgnoreCase.GetHashCode(Key1);
            hashCode = hashCode * -1521134295 + StringComparer.CurrentCultureIgnoreCase.GetHashCode(Key2);
            hashCode = hashCode * -1521134295 + StringComparer.CurrentCultureIgnoreCase.GetHashCode(Key3);
        }
        return hashCode;
    }
}

另一个问题是在比较数据库密钥时使用当前的文化是否是一个好主意。具有不同文化背景的用户可能会得到不同的结果。更好地明确指定数据库使用的相同区域性。

答案 4 :(得分:-1)

在某些情况下,使用LINQ不能像顺序查询那样优化,因此您最好使用老式的方式编写查询:

public static bool DoesRecordExist(string keyColumn1, string keyColumn2, string keyColumn3,
        string keyValue1, string keyValue2, string keyValue3, DataTable dt)
{
    if (dt != null) 
    {
        foreach (var r in dt.Rows)
        {
            if (string.Equals(SafeTrim(r[keyColumn1]), keyValue1, StringComparison.CurrentCultureIgnoreCase) &&
                string.Equals(SafeTrim(r[keyColumn2]), keyValue2, StringComparison.CurrentCultureIgnoreCase) &&
                string.Equals(SafeTrim(r[keyColumn3]), keyValue3, StringComparison.CurrentCultureIgnoreCase)
            {
                return true;
            }
        }
    }
    return false;
}

但是可能会有更多的结构改进,但这取决于您是否可以使用它。

选项1:已在数据库中进行选择 您正在使用DataTable,因此有可能从数据库中获取数据。如果您有很多记录,则将此检查移至数据库可能更有意义。使用适当的索引时,它可能比内存中的表扫描要快得多。

选项2:用自定义方法替换string.Equals+SafeTrim 每行最多使用SafeTrim次,这会创建许多新的字符串。当您创建自己的方法来比较两个字符串(string.Equals)相对于前导/尾随空格(SafeTrim)时,但创建一个新字符串时,这可能会更快,减少内存负载并减少垃圾收集。如果实现足以内联,那么您将获得很多性能。

选项3:以正确的顺序检查列 确保使用正确的顺序,并将匹配可能性最小的列指定为keyColumn1。这将使if语句结果更快地变为假。如果keyColumn1在80%的情况下匹配,那么您需要执行更多的比较。