Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

Select the fields a page shows

live var rows = Order.OrderBy(o => o.PlacedAt).Take(10000).Select(o => new OrderRow { Reference = o.Reference, Total = o.Total }).ToList();

osy lint's cost-page-ships-whole-rows-for-few-fields (CONSIDER) fires when a page reads whole rows of a wide entity and shows only a few of their fields. One answer to a page's query is about 2 MB, so whole wide rows fill it quickly; a Select of the fields the page shows into a small class fits several times the rows in the same answer, and the database reads only those columns. The finding quotes both row counts.

stable1 example compiled by CIuilintquerylimits

Summary#

cost-page-ships-whole-rows-for-few-fields is a CONSIDER finding: a page reads whole rows of an entity with many fields, and its list shows two or three of them. Every row still carries every field to the browser. A page's answer is bounded by its size — about 2 MB (How many rows a page is sent) — so the fields nobody sees take the room of rows the page could have shown.

Select the fields the page shows into a small class. The loop reads the same names, and each answer carries several times the rows whole rows would:

entity Shipment {
  [Required, MaxLength(24)] string Reference;
  decimal Total;
  DateTime PlacedAt;
  [MaxLength(80)] string? Carrier;
  [MaxLength(120)] string? Tracking;
  [MaxLength(400)] string? Note;
  [MaxLength(80)] string? Street;
  [MaxLength(40)] string? City;
  [MaxLength(12)] string? Postcode;
  [MaxLength(40)] string? Country;
  decimal Weight;
  int Parcels;
  bool Fragile = false;
  DateTime? DeliveredAt;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

class ShipmentRow {
  public string Reference;
  public decimal Total;
}

[Route("/shipments")] [Render(CSR)] [AllowAnonymous]
component ShipmentsPage() {
  live var shipments = Shipment.OrderByDescending(s => s.PlacedAt).Take(10000)
    .Select(s => new ShipmentRow { Reference = s.Reference, Total = s.Total }).ToList();

  render {
    Stack(gap: 2, p: 6) {
      foreach (var s in shipments) { Text($"{s.Reference} — {s.Total}"); }
    }
  }
}

Signature#

class Row { public T1 A; public T2 B; }
live var rows = Entity.….Select(x => new Row { A = x.A, B = x.B }).ToList();

Description#

What it costs#

Measured on a 71-field order: a whole row is 1,813 bytes on the wire, and a Select into a class of three fields is 353. In one answer of about 2 MB that is about 1,150 whole rows against about 5,900 projected — and the database reads three columns instead of seventy-one. The finding prints the same two numbers for your entity, estimated from the fields it declares, so you can judge whether the difference matters for this page.

When does it fire?#

On a component field that reads whole rows of an entity with at least twelve stored fields, when:

  • the read is a list (no Select, no Include, no one-row or aggregate verb) that asks for more rows than one answer of whole rows holds — no Take, or a Take(n) larger than that (a Take(5) shows five rows whatever they carry),
  • it is used only as the source of foreach loops in the render,
  • each loop's row is used only to read a field (s.Reference), never handed whole to an action, a child component or a comparison, and never assigned to, and
  • the fields read are at most five, no more than a third of the entity, and none is a reference to another entity.

When is it silent?#

When any of those does not hold, because then a projection is not the one-line change: a row handed to an action may need any of its fields, an edited field needs the row, and a reference shown through (s.Customer.Name) needs the include. It is also silent on a narrow entity, where the whole row is already small.

Why CONSIDER?#

Because the page is correct as written. For a table that stays small the whole row costs nothing anyone will notice; for one that grows, the projection is what lets the same answer hold several times the rows.

See also#

Related

How many rows a page is sent

A page's query is sent bounded. Its own .Take(n) is the ceiling: the browser may ask for fewer of those rows, never…

A list or table with no bound

osy lint's ui-unbounded-list-does-not-page (SHOULD) fires when a page draws a list with foreach from a query that reads…

The reactivity & lifecycle model

How an Osy# component comes alive and stays in sync: declarations are live value bindings, `on mount`/`on unmount` are…