Native extreme-row strategies (SelectWhereMax / SelectWhereMin)
For a
SelectWhereMax/SelectWhereMinrequest withExtremeRowTies.One, PostgreSQL and ClickHouse render a native SQL strategy instead of the portable window-function lowering — automatically, from the provider and the query form, with no opt-in and no per-instance switch.
Prerequisites: Keeping the extreme row · Provider overview · API quick reference
Operator names. The built-in operators are
SelectWhereMinOld/SelectWhereMaxOld; they cover the compatibility path, including the in-memory provider.SelectWhereMin/SelectWhereMaxfrom the extensions package are built on the public query-component layer.
Overview
SelectWhereMax and
SelectWhereMin keep the row whose value under a selector is the greatest or smallest in the source, globally or per group. They are a row filter, not an aggregate: a surviving row keeps every column. The semantics are unchanged from the portable implementation — see Keeping the extreme row for the full contract.
What this page adds is how the One request reaches the database. Selection is automatic and deterministic by provider and query form/type:
- on PostgreSQL, native-eligible key and group selectors render
DISTINCT ON(grouped) or an innerORDER BY ... LIMIT 1(global); - on ClickHouse, a native-eligible key, group and payload render a single
argMin/argMaxaggregate; - every other combination keeps the portable window-function lowering.
There is no opt-in, no per-instance or per-query switch, and no retry-on-error: once the dialect accepts a request it renders it, and a rendering failure is surfaced instead of silently re-running the portable path. The hook covers ExtremeRowTies.One only: there is no native All renderer, so an All request takes the aggregate lowering described in Keeping the extreme row — GROUP BY plus a join-back per group, a scalar aggregate plus a join-back globally, component by component for a composite key. The one exception is a floating-point value key on ClickHouse, which keeps the ranking window: that server's max()/min() and its ORDER BY disagree about NaN (the aggregate returns NaN as the extremum while the ordering ranks it last), so the aggregate form would return a different winner set than the contract the window implements.
Usage
The public API does not change; only the provider decides the SQL.
// Global: the single row with the greatest Score.
var top = dataContext.From<Player>()
.SelectWhereMax(p => p.Score)
.ToList();
// Per group: the single row with the smallest Score in each TeamId.
// The group selector is compound; a bare p => p.TeamId would stay on the portable path.
var perTeam = dataContext.From<Player>()
.SelectWhereMin(p => p.Score, ExtremeRowTies.One, p => new { p.TeamId })
.ToList();
Native or portable
| Provider | Query form | Strategy |
|---|---|---|
| PostgreSQL | grouped One |
one DISTINCT ON (<group columns>) ... ORDER BY <group columns>, <extreme key> ASC/DESC |
| PostgreSQL | global One |
an inner ORDER BY <extreme key> ASC/DESC LIMIT 1 |
| ClickHouse | global One |
one argMin/argMax(tuple(<payload columns>), <extreme key>), plus HAVING count() > 0 to suppress the empty-input default row |
| ClickHouse | grouped One |
one argMin/argMax(tuple(<payload columns>), <extreme key>) next to the group columns and a GROUP BY |
Any other provider, or a non-eligible One request |
any | the portable window-function lowering |
Every provider, ExtremeRowTies.All |
any | the aggregate lowering: GROUP BY + join-back per group, a scalar aggregate + join-back globally, component by component for a composite key; a floating-point value key on ClickHouse keeps the ranking window instead (the server's aggregate and ordering disagree about NaN) |
The PostgreSQL grouped form keeps the first row per group under the ordering, so ordering the group components first and the full extreme key after them selects the extreme row per group. The ClickHouse aggregate picks a whole payload tuple and projects its elements back to the canonical payload aliases. In both cases the shared builder still applies the outer projection, DISTINCT and the user's output ordering afterwards.
For the Player example above (players(id, score, team_id, name)) the two calls render as follows.
-- PostgreSQL, global (SelectWhereMax): the inner ORDER BY ... LIMIT 1
select t1.id, t1.score, t1.team_id as "TeamId", t1.name
from (select *
from players
where score is not null order by "score" desc limit 1) as "t1"
-- PostgreSQL, grouped (SelectWhereMin over new { TeamId }): DISTINCT ON over the group columns
select t1.id, t1.score, t1.team_id as "TeamId", t1.name
from (select distinct on ("team_id") *
from (select *
from players
where score is not null) __nextorm_extreme order by "team_id", "score") as "t1"
-- ClickHouse, global (SelectWhereMax): one argMax aggregate over a payload tuple
select t1.id, t1.score, t1.team_id as `TeamId`, t1.name
from (select tupleElement(`__nextorm_extreme_tuple`, 1) as `id`, tupleElement(`__nextorm_extreme_tuple`, 2) as `score`, tupleElement(`__nextorm_extreme_tuple`, 3) as `team_id`, tupleElement(`__nextorm_extreme_tuple`, 4) as `name`
from (
select argMax(tuple(`id`, `score`, `team_id`, `name`), `score`) as `__nextorm_extreme_tuple`
from (select *
from players
where score is not null) as `__nextorm_extreme_src`
having count() > 0)) as `t1`
-- ClickHouse, grouped (SelectWhereMin over new { TeamId }): the aggregate next to the group columns plus GROUP BY
select t1.id, t1.score, t1.team_id as `TeamId`, t1.name
from (select `team_id`, tupleElement(`__nextorm_extreme_tuple`, 1) as `id`, tupleElement(`__nextorm_extreme_tuple`, 2) as `score`, tupleElement(`__nextorm_extreme_tuple`, 4) as `name`
from (
select `team_id`, argMin(tuple(`id`, `score`, `team_id`, `name`), `score`) as `__nextorm_extreme_tuple`
from (select *
from players
where score is not null) as `__nextorm_extreme_src`
group by `team_id`)) as `t1`
Eligible queries
Eligibility is intentionally narrow, and is decided from a prepared description of the command before any SQL, alias or parameter is built.
| Part | Native eligibility |
|---|---|
| Extreme key | a direct mapped column bound to short/int/long or float/double (including nullable), or a composite of such components; a composite that contains a floating component is limited to three components, while a purely integral composite has no arity limit; a floating component is adapted NaN-safe |
| Group key | the same integral types as the extreme key (short/int/long, including nullable), or a composite of those of any arity; a floating group key keeps the portable path |
| ClickHouse payload | integral types (sbyte/byte/short/ushort/int/uint/long/ulong, including nullable), floating-point types (float/double, including nullable) and string |
Everything else (an All request, whatever its keys) |
the portable lowering of that tie policy: the window for One, the aggregate form for All — except a floating-point value key on ClickHouse, which keeps the window |
A composite key is compared lexicographically component by component. A Float32/Float64 component of the extreme key is adapted in the same query instead of being passed to argMin/argMax verbatim: a leading isNaN rank keeps NaN last in both directions, matching the portable contract, and a Float32 component is widened to Float64 for the comparison. The raw direct aggregate is deliberately not used, because ClickHouse seeds it with the first row and x > NaN/x < NaN are both false, so a leading NaN would otherwise win. The adaptation covers single, two- and three-component keys across Min/Max, global/grouped, and nullable/non-nullable components. The arity limits are deliberately asymmetric: a purely integral extreme key or group key is a plain lexicographic tuple and is not arity-capped, while only a key that contains a floating component is capped at three components, because only that adaptation was proven arity by arity. The group selector must be compound (e => new { e.TeamId }); a bare single-column group selector keeps the portable path. Everything outside the table above keeps the portable lowering with the same results contract: the other providers, every ExtremeRowTies.All request (which takes the aggregate form, since there is no native All renderer, except a floating-point value key on ClickHouse — that server's max()/min() treat NaN as the extreme value while its ORDER BY ranks it last, so the rank window stays the reference there), floating-point group keys, floating extreme keys with more than three components, Float16/Decimal keys, value converters, computed expressions, and other key or payload types.
Semantics
The native strategies do not change the observable result:
- One real row per partition.
Onereturns exactly one winning source row per group (one row for the global form), and every field comes from that same source row, so a full-entity read and a projection are consistent. Which tied row is chosen is unspecified — there is no tie-break API. - Output ordering is separate. A user
OrderBysorts the result only. It is applied outside winner selection and is never folded into the winner ordering. - NULL handling. A row with a
NULLextreme-key component is excluded before winner selection, and a partition whose key components are allNULLyields no row. Nullable group keys and nullable payload values are preserved. - Empty input. The result is zero rows, including the ClickHouse global form, where
HAVING count() > 0suppresses the aggregate's default tuple row instead of returning it. - Rejected shapes. The combinations the portable path forbids — joins, paging,
DistinctOn,GroupBy,Having, named windows, CTEs, set operations,LimitBy,ArrayJoin,PreWhereand non-physical sources — still reject before dispatch on both paths.
Dialect extensibility
The native strategy is a public dialect capability, not a hard-coded provider branch: ISqlDialect.ExtremeRowRenderer is an optional renderer hook (a default interface member returning null), and PostgresDialect/ClickHouseDialect are deliberately not sealed, so an external assembly can install its own public IExtremeRowRenderer. Returning null from the override — or a renderer whose CanRender answers false — keeps the portable window-function lowering.
To build such a dialect and register it on a context, see Extensibility: custom SQL dialects and renderers.