Proper Usage of Distinct in LINQ and Performance Optimization

Nov 20, 2025 · Programming · 14 views · 7.8

Keywords: LINQ | Distinct | IEquatable | Performance Optimization | C#

Abstract: This article provides an in-depth exploration of the correct usage of the Distinct operation in LINQ, analyzing why the default Distinct method may not work as expected and offering multiple solutions. It details the implementation of the IEquatable<T> interface, the use of the DistinctBy extension method, and the combination of GroupBy and First, while incorporating performance optimization principles to guide developers in writing efficient LINQ queries. Through practical code examples and performance comparisons, it helps readers fully understand the execution mechanisms and optimization strategies of LINQ queries.

Analysis of LINQ Distinct Operation Issues

When using LINQ for data querying, developers often encounter the need to retrieve unique records. As shown in the Q&A data, the user attempted to use the .Distinct() method to obtain unique Id values from a table but found that the method still returned duplicates. This situation typically stems from a misunderstanding of how the Distinct method works.

By default, the Enumerable.Distinct method uses the default equality comparer, Default, to compare values. For reference types, this compares whether the object references are the same, not the content of the objects. Therefore, even if two objects have the same Id value, if they are different object instances, the Distinct method will treat them as distinct items.

Solution 1: Implementing the IEquatable<T> Interface

To make the Distinct method compare based on specific properties (such as Id), you need to implement the IEquatable<T> interface for your custom data type and provide appropriate implementations of the Equals and GetHashCode methods.

public class TableClassChanges : IEquatable<TableClassChanges>
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public DateTime Date { get; set; }
    public string Field { get; set; }
    public string FieldName { get; set; }

    public bool Equals(TableClassChanges other)
    {
        if (other is null) return false;
        return this.Id == other.Id;
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as TableClassChanges);
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

After implementing the interface, the Distinct method will compare based on the Id property, ensuring that only objects with different Id values are retained in the result set.

Solution 2: Using Select and Distinct Combination

Another simple and effective method is to first use the Select method to extract the desired property and then apply the Distinct method. This approach does not require modifying the original class definition and is suitable for temporary uniqueness requirements.

var distinctIds = tableClass.Items
    .Where(x => x.UserId == emp.UserId 
        && x.Date > DateBeforeChanges 
        && x.Date < DateAfterEffective 
        && (x.Field == Inserted || x.Field == Deleted))
    .Select(x => x.Id)
    .Distinct()
    .OrderByDescending(id => id);

The advantage of this method is its clear query logic and good performance. By filtering first, then selecting specific properties, and finally applying distinct, it effectively reduces memory usage and computational overhead.

Solution 3: Using GroupBy and First Combination

For more complex requirements, or in scenarios such as using LINQ to SQL, you can use a combination of GroupBy and First to achieve an effect similar to Distinct.

var uniqueItems = tableClass.Items
    .Where(x => x.UserId == emp.UserId 
        && x.Date > DateBeforeChanges 
        && x.Date < DateAfterEffective 
        && (x.Field == Inserted || x.Field == Deleted))
    .GroupBy(x => x.Id)
    .Select(g => g.First())
    .OrderByDescending(x => x.Id);

This method first groups by Id, then selects the first element from each group, ensuring that each Id appears only once in the result set. This approach is particularly useful in LINQ to SQL, as it can be translated into efficient SQL queries.

Performance Optimization and Best Practices

According to the analysis in the reference article, the order of LINQ queries significantly impacts performance. When writing LINQ queries, you should follow the principle of "filter first, transform later":

// Recommended: filter first, then select
var efficientQuery = dataSource
    .Where(item => item.IsActive && item.Category == "Target")
    .Select(item => new { item.Id, item.Name })
    .Distinct();

// Not recommended: select first, then filter
var inefficientQuery = dataSource
    .Select(item => new { item.Id, item.Name, item.IsActive, item.Category })
    .Where(item => item.IsActive && item.Category == "Target")
    .Distinct();

Performance tests show that the query that filters first can significantly reduce memory allocation and computation time, especially when processing large datasets. In the benchmark test from the reference article, the optimized query was nearly 7 times faster than the unoptimized one.

Practical Application Scenario

In the code from the original Q&A, the correct implementation should be:

private void WriteStuff(SqlHelper db, EmployeeHelper emp)
{
    String checkFieldChange;
    AnIList tableClass = new AnIList(db, (int)emp.PersonId);
    
    var distinctItems = tableClass.Items
        .Where(x => x.UserId == emp.UserId 
            && x.Date > DateBeforeChanges 
            && x.Date < DateAfterEffective 
            && (x.Field == Inserted || x.Field == Deleted))
        .GroupBy(x => x.Id)
        .Select(g => g.First())
        .OrderByDescending(x => x.Id);

    if (distinctItems != null && distinctItems.Any())
    {
        foreach (TableClassChanges item in distinctItems)
        {
            AnotherIList payTxn = new AnotherIList(db, item.Id);
            checkFieldChange = GetChangeType(item.FieldName);
            
            // Other processing logic
        }
    }
}

This implementation ensures that each Id is processed only once, avoiding duplicate operations while maintaining query performance and readability.

Conclusion

The LINQ Distinct method, by default, compares based on reference equality, which may lead to unexpected results. By implementing the IEquatable<T> interface, using a combination of Select and Distinct, or using a combination of GroupBy and First, you can effectively address uniqueness query requirements. Additionally, following the performance optimization principle of "filter first, transform later" can significantly improve query efficiency. In practical development, you should choose the most appropriate solution based on the specific scenario.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.