Skip to main content

Group By

nicyeAbout 4 minAbout 1100 words

Group By

IFreeSql fsql; // For how to create it, please refer to the introductory documentation

class Topic 
{
    [Column(IsIdentity = true, IsPrimary = true)]
    public int Id { get; set; }
    public int Clicks { get; set; }
    public string Title { get; set; }
    public DateTime CreateTime { get; set; }
}

1. Single Table Grouping

var list = fsql.Select<Topic>()
    .GroupBy(a => new { tt2 = a.Title.Substring(0, 2), mod4 = a.Id % 4 })
    .Having(g => g.Count() > 0 && g.Avg(g.Key.mod4) > 0 && g.Max(g.Key.mod4) > 0)
    .Having(g => g.Count() < 300 || g.Avg(g.Key.mod4) < 100)
    .OrderBy(g => g.Key.tt2)
    .OrderByDescending(g => g.Count())
    .ToList(g => new 
    {
        g.Key, 
        cou1 = g.Count(), 
        arg1 = g.Avg(g.Value.Clicks), 
        arg2 = g.Sum(g.Value.Clicks > 100 ? 1 : 0)
    });
//SELECT 
//substr(a.`Title`, 1, 2) as1,
//(a.`Id` % 4) as2, 
//count(1) as3, 
//avg(a.`Clicks`) as4, 
//sum(case when a.`Clicks` > 100 then 1 else 0 end) as5 
//FROM `Topic` a 
//GROUP BY substr(a.`Title`, 1, 2), (a.`Id` % 4) 
//HAVING (count(1) > 0 AND avg((a.`Id` % 4)) > 0 AND max((a.`Id` % 4)) > 0) AND (count(1) < 300 OR avg((a.`Id` % 4)) < 100)
//ORDER BY substr(a.`Title`, 1, 2), count(1) DESC

To compute aggregate values without grouping, use ToAggregate instead of ToList

var list = fsql.Select<Topic>()
    .ToAggregate(g => new 
    {
        cou1 = g.Count(), 
        arg1 = g.Avg(g.Key.Clicks), 
        arg2 = g.Sum(g.Key.Clicks > 100 ? 1 : 0)
    });

2. Multi-Table Grouping

var list = fsql.Select<Topic, Category, Area>()
    .InnerJoin((a, b, c) => b.Id == a.CategoryId)
    .InnerJoin((a, b, c) => c.Id == b.AreaId)
    .GroupBy((a, b, c) => new { a.Title, c.Name })
    .Having(g => g.Count() < 300 || g.Avg(g.Value.Item1.Clicks) < 100)
    .ToList(g => new { count = g.Count(), Name = g.Key.Name });
//SELECT count(1), c.name
//FROM topic a
//LEFT JOIN cagetory b ON b.id = a.category_id
//LEFT JOIN area c ON c.id = b.area_id
//GROUP BY a.title, c.name
//HAVING count(1) < 300 AND avg(a.clicks) < 100
  • g.Value.Item1 corresponds to Topic
  • g.Value.Item2 corresponds to Category
  • g.Value.Item3 corresponds to Area
DescriptionMethodSQL
Count.Count()select count(*) from ...
Sum.Sum(a => a.Score)select sum([Score]) from ...
Average.Avg(a => a.Score)select avg([Score]) from ...
Maximum.Max(a => a.Score)select max([Score]) from ...
Minimum.Min(a => a.Score)select min([Score]) from ...
lambdasqlDescription
SqlExt.IsNull(id, 0)isnull/ifnull/coalesce/nvlCompatible with various databases
SqlExt.DistinctCount(id)count(distinct id)
SqlExt.GreaterThan>Greater than
SqlExt.GreaterThanOrEqual>=Greater than or equal to
SqlExt.LessThan<Less than
SqlExt.LessThanOrEqual<=Less than or equal to
SqlExt.EqualIsNullIS NULLIs NULL
SqlExt.Case(dictionary)case when .. endCase based on dictionary
SqlExt.GroupConcatgroup_concat(distinct .. order by .. separator ..)MySql
SqlExt.FindInSetfind_in_set(str, strlist)MySql
SqlExt.StringAggstring_agg(.., ..)PostgreSQL
SqlExt.Rank().Over().PartitionBy().ToValue()rank() over(partition by xx)Window function
SqlExt.DenseRank().Over().PartitionBy().ToValue()dense_rank() over(partition by xx)
SqlExt.Count(id).Over().PartitionBy().ToValue()count(id) over(partition by xx)
SqlExt.Sum(id).Over().PartitionBy().ToValue()sum(id) over(partition by xx)
SqlExt.Avg(id).Over().PartitionBy().ToValue()avg(id) over(partition by xx)
SqlExt.Max(id).Over().PartitionBy().ToValue()max(id) over(partition by xx)
SqlExt.Min(id).Over().PartitionBy().ToValue()min(id) over(partition by xx)
SqlExt.RowNumber(id).Over().PartitionBy().ToValue()row_number(id) over(partition by xx)

3. First Record in Group

fsql.Select<User1>()
    .Where(a => a.Id < 1000)
    .WithTempQuery(a => new
    {
        item = a,
        rownum = SqlExt.RowNumber().Over().PartitionBy(a.Nickname).OrderBy(a.Id).ToValue()
    })
    .Where(a => a.rownum == 1)
    .ToList();

Tip: Supports multi-table nested queries, fsql.Select<User1, UserGroup1>()

SELECT *
FROM (
    SELECT a.[Id], a.[Nickname], row_number() over( partition by a.[Nickname] order by a.[Id]) [rownum]
    FROM [User1] a
    WHERE a.[Id] < 1000
) a
WHERE (a.[rownum] = 1)

If the database does not support window functions, you can use grouped nested queries to solve this:

fsql.Select<User1>()
    .Where(a => a.Id < 1000)
    .GroupBy(a => a.Nickname)
    .WithTempQuery(g => new { min = g.Min(g.Value.Id) })
    .From<User1>()
    .InnerJoin((a, b) => a.min == b.Id)
    .ToList((a, b) => b);
SELECT b.[Id], b.[Nickname] 
FROM ( 
    SELECT min(a.[Id]) [min] 
    FROM [User1] a 
    WHERE a.[Id] < 1000 
    GROUP BY a.[Nickname] ) a 
INNER JOIN [User1] b ON a.[min] = b.[Id]

See more in the 《Nested Queries》 documentation

4. Aggregate

Distinct

var list = fsql.Select<Topic>()
    .Aggregate(a => Convert.ToInt32("count(distinct title)"), out var count)
    .ToList();

SELECT count(distinct title) as1 FROM "Topic" a

SELECT a."Id", a."Clicks", a."Title", a."CreateTime" FROM "Topic" a

SqlExt.DistinctCount

fsql.Select<Topic>()
    .Aggregate(a => SqlExt.DistinctCount(a.Key.Title), out var count);

SELECT count(distinct a."title") as1 FROM "Topic" a

ToAggregate + SqlExt.DistinctCount

var distinctAggregate = fsql.Select<Topic>().ToAggregate(a => new
    {
        TitleCount = SqlExt.DistinctCount(a.Key.Title),
        ClicksCount= SqlExt.DistinctCount(a.Key.Clicks),
    }
);

SELECT count(distinct a."Title") as1, count(distinct a."Clicks") as2 FROM "Topic" a

5. Navigation Property Grouping

If the Topic class has a navigation property Category, and Category has a navigation property Area, the code for grouping by navigation properties is as follows:

var list = fsql.Select<Topic>()
    .GroupBy(a => new { a.Clicks, a.Category })
    .ToList(g => new { g.Key.Category.Area.Name });

Note: The above code will throw an error because a.Key.Category.Area.Name cannot be resolved. To fix this, use Include:

var list = fsql.Select<Topic>()
    .Include(a => a.Category.Area)
    // This line must be added, otherwise it will only group by Category without including its subordinate navigation property Area

    .GroupBy(a => new { a.Clicks, a.Category })
    .ToList(g => new { g.Key.Category.Area.Name });

Alternatively, you can solve it this way:

var list = fsql.Select<Topic>()
    .GroupBy(a => new { a.Clicks, a.Category, a.Category.Area })
    .ToList(g => new { g.Key.Area.Name });

API

MethodReturn TypeParametersDescription
ToSqlstringReturns the SQL query that will be executed
ToList<T>List<T>LambdaExecutes the SQL query and returns a list of records for the specified fields; if no records exist, returns a list with Count 0
ToList<T>List<T>string fieldExecutes the SQL query and returns a list of records for the field specified by field, which can be received as a tuple or basic type (int, string, long); if no records exist, returns a list with Count 0
ToAggregate<T>List<T>LambdaExecutes the SQL query and returns aggregated results for the specified fields (suitable for cases where GroupBy is not needed)
SumTLambdaSpecifies a column to calculate the sum
MinTLambdaSpecifies a column to calculate the minimum value
MaxTLambdaSpecifies a column to calculate the maximum value
AvgTLambdaSpecifies a column to calculate the average value
Group
GroupBy<this>LambdaGroups by selected columns, e.g., GroupBy(a => a.Name)
GroupBy<this>string, parmsGroups by native SQL syntax, e.g., GroupBy("concat(name, @cc)", new { cc = 1 })
Having<this>string, parmsFilters aggregated conditions by native SQL syntax, e.g., Having("count(name) = @cc", new { cc = 1 })
Members
KeyReturns the object selected by GroupBy
ValueReturns the field selector for the main table or From<T2,T3....>