Osy#betaRun it on Osyrin: osyrin.com

Reference / Query

GroupBy (and HAVING)

<Entity>[.Where(p)].GroupBy(o => key).Select(g => new T { K = g.Key, N = g.Sum(o => v) })[.Where(havings)][.OrderBy(…)][.Take(n)]

Group rows by a key and reduce each group to one row — `g.Key`, `g.Count()`, `g.Count(o => p)`, `g.Sum/Average/Min/Max(…)` — computed by the database as a real GROUP BY. A `Where` BEFORE the grouping filters the rows that get grouped; a `Where` AFTER the projection filters the groups themselves, which is SQL's HAVING. You never write the word "having": which side of the `GroupBy` a `Where` sits on says what it filters.

stable5 examples compiled by CIquerydataaggregatesgrouping

Summary#

Group rows by a key and reduce each group to a single row, in the database:

class RegionTotal {
  public string Region;
  public int Orders;
  public decimal Total;
}

entity Order {
  [Required, MaxLength(60)] string Region;
  decimal Total;
  bool Cancelled;
  int Month;
}

List<RegionTotal> Revenue() {
  return Order.Where(o => !o.Cancelled)                 // filters the ROWS, before grouping
              .GroupBy(o => o.Region)
              .Select(g => new RegionTotal {
                Region = g.Key,
                Orders = g.Count(),
                Total  = g.Sum(o => o.Total) })
              .Where(r => r.Total > 1000m)              // filters the GROUPS — this is HAVING
              .OrderByDescending(r => r.Total)
              .Take(10)
              .ToList();
}

That is one SELECT … GROUP BY region HAVING SUM(total) > 1000 ORDER BY … LIMIT 10. No rows travel.

Signature#

<Entity>
  [.Where(o => rowPredicate)]              // optional — filters the ROWS that get grouped
  .GroupBy(o => key)                        // one key, or a composite: o => new() { A = o.X, B = o.Y }
  .Select(g => new T {                      // REQUIRED, and must come next
      Col = g.Key,                          //   the key (or g.Key.A for a composite)
      Agg = g.Sum(o => v),                  //   g.Count() · g.Count(o => p) · g.Sum/Average/Min/Max(o => v)
  })
  [.Where(x => groupPredicate)]             // optional — filters the GROUPS (HAVING)
  [.OrderBy(x => col) | .OrderByDescending(x => col)]
  [.Take(n)]                                // a constant, or a count chosen at run time
  [.ToList()]

Description#

Does my Where filter rows, or groups?#

The position of a Where is what it means, and this is the whole grammar of grouping:

Where it sitsWhat it filtersSQL
before GroupBythe rows that get groupedWHERE
after the Selectthe groups, by their aggregatesHAVING

Exclude cancelled orders from the totals is a row filter. Only show regions that made over £1000 is a group filter — and it cannot be a row filter, because no single row knows its group's total. You never type the word having; you put the Where on the side that says what you mean.

When is it a database GROUP BY?#

When GroupBy is followed by a Select that reduces each group to one row of its key and its aggregates. That is the shape this page is about, and it is the one that never fetches the rows.

Anything else you do with a group — hold it, iterate it, take its first element, filter the groups by their size — needs the group's rows, so the rows the query selects are read and then grouped exactly as C#'s LINQ groups them. That is IGrouping<K, T>, and it gives C#'s answer; it is just not one row per group in the database.

Inside the reducing Select, a column is exactly one of two things:

  • the key — g.Key (or g.Key.<Part> for a composite key), or
  • an aggregate — g.Count(), g.Count(o => p), g.Sum(o => v), g.Average(o => v), g.Min(o => v), g.Max(o => v).

Any other column (g.First().Name, the group itself) needs the group's rows, which makes the read the row-reading form instead.

g.Count(o => p) counts the rows of the group the condition holds for — 0 for a group where it holds for none, so the group still appears. That is the difference from a Where before the grouping, which drops the rows first: a group none of whose rows pass is then not a group at all. One read, any number of conditional counts:

class WallStat { public string Wall; public int Routes; public int Hard; public int Open; }

entity Route {
  [Required, MaxLength(20)] string Wall;
  int Grade;
  bool Retired = false;
}

List<WallStat> PerWall() {
  return Route.GroupBy(r => r.Wall)
              .Select(g => new WallStat {
                Wall   = g.Key,
                Routes = g.Count(),
                Hard   = g.Count(r => r.Grade >= 7),     // COUNT(*) FILTER (WHERE …), in effect
                Open   = g.Count(r => !r.Retired) })
              .Where(w => w.Open > 0)                     // walls with at least one open route
              .ToList();
}

How do I group by more than one column?#

Group by more than one column with an object key, and read the parts back off g.Key:

class MonthlyRegion {
  public string Region;
  public int Month;
  public decimal Total;
}

List<MonthlyRegion> ByRegionAndMonth() {
  return Order.GroupBy(o => new() { Region = o.Region, Month = o.Month })
              .Select(g => new MonthlyRegion {
                Region = g.Key.Region,          // ← per PART, not a bare g.Key
                Month  = g.Key.Month,
                Total  = g.Sum(o => o.Total) })
              .OrderBy(r => r.Region)
              .ToList();
}

A bare g.Key on a composite key is a compile error — there is no tuple type to hand you, and each part is a column in its own right.

The projection target is a class#

Project into a class you declared (a plain data shape). That is what gives the result a real type you can return, index and iterate — List<RegionTotal> above. An anonymous new { … } also works, but it has no name, so it cannot cross a function boundary; use it only where the result is consumed on the spot.

What may NOT precede the grouping#

One GROUP BY takes only Where before it. An OrderBy or a Skip/Take before the GroupBy is still ordinary LINQ and gives C#'s answer — the OrderBy sets the order the groups come out in (the order their keys first appear) and Take groups only the rows it takes — but it is answered over the rows read, as [[query-igrouping#stored-rows]] describes, not as one GROUP BY. To order the groups by a total, sort after the projection, which stays one statement.

After the projection, Take takes a constant (Take(10)) or a count chosen at run time (Take(n)) — either becomes the statement's LIMIT, and a negative count takes nothing, as C#'s Take does:

List<RegionTotal> TopRegions(int n) {
  return Order.GroupBy(o => o.Region)
              .Select(g => new RegionTotal { Region = g.Key, Orders = g.Count(), Total = g.Sum(o => o.Total) })
              .OrderByDescending(r => r.Total)
              .Take(n)
              .ToList();
}

Examples#

Every aggregate at once, over a parent's children — a per-invoice summary computed entirely in the database:

class SkuStat {
  public string Sku;
  public int Times;
  public int TotalQty;
  public decimal Cheapest;
  public decimal Dearest;
  public decimal Average;
}

entity Line {
  [Required, MaxLength(60)] string Sku;
  int Qty;
  decimal Price;
}

List<SkuStat> PerSku() {
  return Line.GroupBy(l => l.Sku)
             .Select(g => new SkuStat {
               Sku      = g.Key,
               Times    = g.Count(),
               TotalQty = g.Sum(l => l.Qty),
               Cheapest = g.Min(l => l.Price),
               Dearest  = g.Max(l => l.Price),
               Average  = g.Average(l => l.Price) })
             .Where(s => s.Times > 1)              // HAVING COUNT(*) > 1 — SKUs sold more than once
             .OrderByDescending(s => s.TotalQty)
             .ToList();
}

See also#

Related

Querying data

How you read data in Osy#. You write C# LINQ; it becomes one SQL statement. The rules that follow from that are the…

Sum / Average / Min / Max / Count

Fold rows down to a single number — a query or a `List<T>` you already hold. The one thing to know before you use them:…

Select (projections)

Reshape what a query returns: one column, an anonymous row, or a `class` you declared. The projection becomes the SQL…

OrderBy / ThenBy

Sort a query by one key or several. `OrderBy`/`OrderByDescending` start the sort, `ThenBy`/`ThenByDescending` add…

class methods

Behaviour attached to a class — a method with a receiver, called as value.Method(). Classes are in-memory values, so a…