String and array aggregates
SqlFunctions.Sql.string_agg is available on PostgreSQL, SQL Server 2017+, ClickHouse, MySQL/MariaDB and SQLite
(SupportsStringAgg, which defaults to the umbrella SupportsStringArrayAggregates);
ClickHouse renders it as arrayStringConcat(groupArray(x), delimiter), MySQL/MariaDB as
group_concat(x separator delimiter) and SQLite as group_concat(x, delimiter). SqlFunctions.Postgres.array_agg
(SupportsArrayAgg) requires an array type and is therefore PostgreSQL-only. An array_agg
result is an array column:
var names = dataContext.From<IComplexEntity>()
.Select(e => SqlFunctions.Sql.string_agg(e.String, ","))
.First();
-- PostgreSQL / SQL Server
select string_agg(somestring, ',')
from complex_entity
-- ClickHouse
select arrayStringConcat(groupArray(somestring), ',')
from complex_entity
-- MySQL/MariaDB
select group_concat(somestring separator ',')
from complex_entity
-- SQLite
select group_concat(somestring, ',')
from complex_entity
| C# | SQL | Providers |
|---|---|---|
SqlFunctions.Sql.string_agg(x, delimiter) |
string_agg(x, delimiter) / arrayStringConcat(groupArray(x), delimiter) / group_concat(x separator delimiter) / group_concat(x, delimiter) |
PostgreSQL, SQL Server, ClickHouse, MySQL/MariaDB, SQLite |
SqlFunctions.Postgres.array_agg(x) |
array_agg(x) |
PostgreSQL |
SQL Server 2025: product
SqlFunctions.SqlServer.product(value) renders the SQL Server 2025 PRODUCT aggregate in an aggregate
context. The result keeps the input type (int? for an int column), NULL rows are ignored, and a
group with no non-null value has no product (SQL NULL):
var rows = dataContext.From<IComplexEntity>()
.GroupBy(e => new { e.Int })
.Select(e => new { e.Int, Product = SqlFunctions.SqlServer.product(e.Id) })
.ToList();
select nullableint, product(id) as [Product]
from complex_entity
group by nullableint
The aggregate is SQL Server 2025 (major version 17) only and follows the version-only
SupportsProductAggregate flag (which in turn
follows SupportsSqlServer2025Functions).
It is offered in the aggregate context only — no window/OVER expansion. A context declared for 16, an
unversioned context or an edition-only declaration refuses the call before SQL with The PRODUCT function requires SQL Server major version 17 or later; declared major version: 16. (or … unspecified.). An
edition never substitutes for the declared major version.
SQL Server: ANY_VALUE is refused
SqlFunctions.SqlServer.any_value(value) exists as a deterministic refusal, not as an aggregate.
On SQL Server build 17.0.5005.3 the engine rejected ANY_VALUE with Msg 195 in both the aggregate
and the OVER () forms, at compatibility levels 160 and 170 and with PREVIEW_FEATURES = ON, so no
supported SQL Server execution contract is established. Every invocation — direct or inside a query —
throws NotSupportedException with
SqlFunctions.SqlServer.any_value (ANY_VALUE) is refused: SQL Server build 17.0.5005.3 rejected this function with Msg 195, including with preview features enabled; no supported SQL Server execution contract is established. The refusal is raised before SQL is rendered and before any command is
created, and a sibling aggregate of the same query still executes.
This is an observed engine rejection, not a CLR/scope limitation and not a version gap a later
nextorm release can close. The portable CommonFunctions.any_agg is a different member: it stays gated
off on SQL Server (SupportsAnyValueAggregate = false) and is not replaced by this refusal; the
providers that enable it (MySQL/MariaDB via ANY_VALUE, ClickHouse via any) keep their dispatch
unchanged.
SQL Server aggregate family (2019 and 2022)
Five #282 members close the residual SQL Server
aggregate names. They are aggregate-context members: the two approximate percentiles are ordered-set
aggregates spelled WITHIN GROUP (ORDER BY …) with no window form, approx_count_distinct has no
OVER form in the engine at all, and checksum_agg — whose native OVER form the engine does accept —
is offered in the aggregate form only here. grouping_id is the analytic member of the family and needs
a grouped command.
| Member | SQL | CLR result | Empty group / all-NULL group | Requires |
|---|---|---|---|---|
approx_count_distinct(value) |
approx_count_distinct(value) |
long (bigint) |
0 / 0 |
major version 15 (SQL Server 2019) |
checksum_agg(value) |
checksum_agg(value) |
int? (int) |
NULL / NULL |
every declaration (no version floor) |
grouping_id(expressions...) |
grouping_id(a, b, …) |
int |
0 under a plain GROUP BY; the subtotal mask otherwise |
every declaration; grouped command only |
approx_percentile_cont(fraction, value) |
approx_percentile_cont(f) within group (order by value) |
double? (float(53)) |
NULL / NULL |
major version 16 (SQL Server 2022) |
approx_percentile_disc(fraction, value) |
approx_percentile_disc(f) within group (order by value) |
T? — the sort input type (int? for an int column) |
NULL / NULL |
major version 16 (SQL Server 2022) |
var rows = dataContext.From<IComplexEntity>()
.GroupBy(e => new { e.Int, e.Boolean })
.Select(e => new
{
e.Int,
e.Boolean,
Distinct = SqlFunctions.SqlServer.approx_count_distinct(e.Int),
Checksum = SqlFunctions.SqlServer.checksum_agg(e.Int),
Group = SqlFunctions.SqlServer.grouping_id(e.Int, e.Boolean),
Median = SqlFunctions.SqlServer.approx_percentile_cont(0.5, e.Int),
MedianValue = SqlFunctions.SqlServer.approx_percentile_disc(0.5, e.Int)
})
.ToList();
select nullableint, b,
approx_count_distinct(nullableint) as [Distinct],
checksum_agg(nullableint) as [Checksum],
grouping_id(nullableint, b) as [Group],
approx_percentile_cont(0.5) within group (order by nullableint) as [Median],
approx_percentile_disc(0.5) within group (order by nullableint) as [MedianValue]
from complex_entity
group by nullableint, b
approx_count_distinct, approx_percentile_cont and approx_percentile_disc follow the same
version-only contract as product: a lower declared major version, an
unversioned context or an edition-only declaration refuses the call before SQL, and an edition never
substitutes for a version.
The APPROX_COUNT_DISTINCT function requires SQL Server major version 15 or later; declared major version: 14.
The APPROX_PERCENTILE_CONT function requires SQL Server major version 16 or later; declared major version: 15.
approx_count_distinct
approx_count_distinct(value) returns long (bigint). NULL rows are not counted, and an empty group
and an all-NULL group both yield 0 — never SQL NULL. It is an estimate, not the exact
count(distinct …): keep SqlFunctions.Sql.count_distinct when exactness matters. A constant NULL
operand renders as CAST(NULL AS <type>), because a bare NULL operand is Msg 8117
(Operand data type NULL is invalid for approx_count_distinct operator.). The engine has no OVER
form (Msg 4113), and this surface offers the aggregate context only.
checksum_agg
checksum_agg(value) returns int? (int). NULL rows are skipped, and an empty or all-NULL group has
no checksum (NULL). The value is order-insensitive but is not a stable identity and not a hash for
equality: identical values cancel (checksum_agg over (5, 5) is 0, over (5) is 5, over
(5, 6) is 3), so it detects change rather than content. There is no version floor: every declared
SQL Server major version renders it, including an unversioned declaration. A constant NULL operand
renders as CAST(NULL AS int) (a bare NULL is Msg 8117, Operand data type NULL is invalid for checksum_agg operator.). The aggregate form is the only one offered, although the engine itself would
accept checksum_agg(...) over (...).
grouping_id
grouping_id(expressions...) renders grouping_id(a, b, …) and returns int. The params object?[]
arguments are flattened into one SQL argument per grouping expression; a captured array is refused. Two
obligations are enforced before SQL, with zero statements executed, instead of being left to the
server's Msg 8161:
- every argument must match a
GROUP BYexpression, otherwiseThe GROUPING_ID function requires every argument to match a GROUP BY expression.(expressions are compared with the query's expression-plan comparer, so an equivalent expression matches); - the call is legal only in the
SELECTlist,HAVINGandORDER BYof a command with aGROUP BY; a missing grouping context — or a call inWHERE, a joinONor theGROUP BYitself — is refused withThe GROUPING_ID function requires a GROUP BY context.
A plain GROUP BY reports 0 for every row. ROLLUP and GROUPING SETS report the subtotal mask: a
set bit per expression aggregated away, with the last argument as the least significant bit, so
grouping_id(grp, value) over rollup(grp, value) is 0 for a detail row, 1 for the grp subtotal and
3 for the grand total. There is no version floor.
approx_percentile_cont and approx_percentile_disc
approx_percentile_cont(fraction, value) returns double? (float(53)) and interpolates;
approx_percentile_disc(fraction, value) returns the sort input type (T?, for example int? for an
int column) and returns an input value. NULL rows are skipped, and an empty or all-NULL group yields
NULL for both. The spelling is WITHIN GROUP (ORDER BY value) and the endpoints 0 and 1 are exact.
The fraction is a double in [0, 1], never a 0–100 percentage, and it must be query-independent: a
literal or a captured scalar. A fraction that depends on a row ((double)e.Int) is refused when the SQL
is generated with The fraction for approx_percentile_disc on SQL Server must be a constant or a captured scalar., and a literal or captured value that is NaN, infinite or outside [0, 1] is refused with
The fraction for approx_percentile_cont on SQL Server must be finite and between 0 and 1 inclusive.
A runtime query-parameter fraction renders as a parameter; the engine then enforces the same range
(Msg 8727).
Neither member has an OVER overload: they are ordered-set aggregates, WITHIN GROUP only, so an
OVER form cannot be written through the public surface. If an OVER shape is constructed by other
means, the engine rejects it with Msg 4113 (The function 'approx_percentile_cont' is not a valid windowing function, and cannot be used with the OVER clause.).
The ORDER BY key must be a column, a parameter or a computed expression. SQL Server answers Msg 5309
(Windowed functions, aggregates and NEXT VALUE FOR functions do not support constants as ORDER BY clause expressions.) for a literal or a NULL key — a typed CAST(NULL AS int) is still a constant — so
such a call is refused when the SQL is generated, while a captured scalar becomes a parameter and
executes.
Approximate results and their caveat
approx_count_distinct and both approximate percentiles are statistical estimates: the vendor documents
a maximum rank error of about 1.33 % at 99 % confidence for the percentiles, and the distinct count is
likewise an estimate. That is a vendor statistical bound, not a deterministic tolerance this library
promises — consecutive runs may return different values. Tests therefore assert the CLR type, a value
inside the input bounds and the NULL/empty behaviour, never equality with one expected number.
checksum_agg and grouping_id are exact.
Aggregate FILTER
count/count_big/min/max/avg/sum and the string/array aggregates accept an extra
Expression<Func<bool>> argument that filters the rows the aggregate sees. The filter predicate is a
full query predicate and may reference columns and parameters. The spelling is selected by
AggregateFilterStyle — PostgreSQL and SQLite
render the ANSI filter (where ...) clause, ClickHouse renders its -If combinator
(countIf/sumIf/...) and MySQL/MariaDB and SQL Server reject the call. On PostgreSQL the ANSI clause
requires server version 9.4 or later; a dialect configured with an older Version rejects the call with
NotSupportedException, while an unset version assumes 9.4+ (see
PostgreSQL provider):
var rows = dataContext.From<IComplexEntity>()
.GroupBy(e => new { e.Int })
.Select(e => new
{
e.Int,
Big = SqlFunctions.Sql.count(() => e.Id > 10L),
Total = SqlFunctions.Sql.sum(e.Id, () => e.Boolean == true)
})
.ToList();
select nullableint, count(*) filter (where (id > 10)) as "Big", sum(id) filter (where (b = true)) as "Total"
from complex_entity group by nullableint
On ClickHouse the same query renders toInt32(countIf((id > 10))) and sumIf(id, (b = true)).
MariaDB explicitly refuses GROUPING in this catalogue; see the
MariaDB catalogue.
Median and percentiles
The four names are not portable and are deliberately not interchangeable: percentile_cont interpolates,
percentile_disc returns an input value, median is the fixed 0.5 case, and the generic percentile
member is refused everywhere instead of aliasing the continuous form.
| C# | SQL | Providers |
|---|---|---|
SqlFunctions.Postgres.percentile_cont(fraction, () => x) |
percentile_cont(f) within group (order by x) (double precision) |
PostgreSQL: ordered-set aggregate, one row per group; floating-point sort only |
SqlFunctions.Postgres.percentile_disc(fraction, () => x) |
percentile_disc(f) within group (order by x) (keeps the sort expression's type) |
PostgreSQL: ordered-set aggregate |
SqlFunctions.Postgres.median(() => x) |
percentile_cont(0.5) within group (order by x) |
PostgreSQL: aggregate, one row per group |
SqlFunctions.Sql.percentile_cont(fraction, x).Over(...), percentile_disc(fraction, x).Over(...) |
percentile_cont(f) within group (order by x) over (...) |
SQL Server (float(53)), MariaDB (double(18,10)): analytic, one row per input row, partition-only OVER |
SqlFunctions.SqlServer.median(x).Over(...), SqlFunctions.MySql.median(x).Over(...) |
percentile_cont(0.5) within group (order by x) over (...) |
SQL Server, MariaDB: analytic, one row per input row, partition-only OVER |
SqlFunctions.ClickHouse.median(x) |
toFloat64(median(x)) |
ClickHouse: aggregate over the quantile family (approximate) |
generic percentile(fraction, value) |
NotSupportedException |
every provider |
A fraction is a double in [0, 1], never a 0–100 percentage; it must be finite and constant (a literal or
a captured variable), and a row-dependent fraction is refused when the SQL is generated. SQLite refuses
median and every percentile (the shipped build has no median aggregate and reports
SQLITE_ENABLE_PERCENTILE=0, and nextorm provides no emulation), MySQL refuses all four (there is no
MEDIAN(…) and no PERCENTILE_*), and ClickHouse refuses the percentile names: its median is the native
quantile-family aggregate, and no quantile* alias is created for the portable spellings. The analytic
forms must be completed with Over(...), and a window ORDER BY or a frame is refused.
Set-returning helpers (PostgreSQL)
SqlFunctions.Postgres.generate_series and SqlFunctions.Postgres.unnest are pre-declared
TvfEntityBuilder<T> runtime factories, so no custom factory is needed:
var numbers = dataContext
.From(() => SqlFunctions.Postgres.generate_series(1L, 3L))
.Select(r => r.Value)
.ToList();
var elements = dataContext
.From(() => SqlFunctions.Postgres.unnest(SqlFunctions.Parameter<long[]>(0)))
.Select(r => r.Value)
.ToList(new long[] { 1, 2, 3 });
generate_series selects Value and unnest selects
Value; both map to the single column the function returns.
PostgreSQL aggregate additions (#281)
The members below extend the PostgreSQL aggregate surface. They are ordinary pg_proc aggregates — or,
for the SQL/JSON aggregates and grouping, parser-level constructs with no pg_proc row — gated per
name, and every other provider, including the in-memory provider, throws NotSupportedException at
translation.
json_agg, jsonb_agg, json_object_agg and jsonb_object_agg remain documented with the JSON
functions in JSON and JSONB.
any_value
| C# | SQL |
|---|---|
SqlFunctions.Postgres.any_value(x) |
any_value(x) |
any_value returns an arbitrary value of the group (anyelement → anyelement, PostgreSQL 16+) and both
the argument and the result keep the argument's own type (integer stays integer). It is the
PostgreSQL pg_proc aggregate, not the portable Sql.any_agg. Because the result is polymorphic,
nextorm requires a single-column CLR shape — a primitive, string, date/time, decimal, Guid or byte
array — and refuses any other T before SQL is emitted; project the value with an explicit conversion
when needed. A group whose values are all NULL yields NULL, and empty input still produces one NULL
row.
Regression sums (regr_sxx, regr_sxy, regr_syy)
| C# | SQL |
|---|---|
SqlFunctions.Postgres.regr_sxx(y, x) |
regr_sxx(y, x) |
SqlFunctions.Postgres.regr_sxy(y, x) |
regr_sxy(y, x) |
SqlFunctions.Postgres.regr_syy(y, x) |
regr_syy(y, x) |
Both operands are generic (TY?/TX?); PostgreSQL's aggregate overloads take double precision and
the result is double precision (double?). regr_sxx is
sum(X^2) - sum(X)^2/N, regr_syy is the same over Y, and regr_sxy is
sum(X*Y) - sum(X)*sum(Y)/N. A pair with a NULL operand is skipped; a group with a single non-NULL
pair yields 0 (measured), and an all-NULL group or empty input yields NULL — empty input still
produces one row.
xmlagg
| C# | SQL |
|---|---|
SqlFunctions.Postgres.xmlagg(x) |
xmlagg(cast(x as xml)) |
PostgreSQL has a single xmlagg(xml) → xml aggregate: xmlagg(text) and xmlagg(integer) are both
42883 "function … does not exist" (measured), so the operand is cast to xml and must hold XML
markup the xml type accepts. The result is the concatenation of the group's documents with no
separator, and Npgsql materialises the xml result as string (measured), which is what the member
declares. NULL operands are skipped, a group whose operands are all NULL yields NULL, and empty input
still produces one NULL row.
Strict and unique JSON aggregates
| C# | SQL |
|---|---|
SqlFunctions.Postgres.json_agg_strict(x) |
json_agg_strict(x) |
SqlFunctions.Postgres.jsonb_agg_strict(x) |
jsonb_agg_strict(x) |
SqlFunctions.Postgres.json_object_agg_unique(k, v) |
json_object_agg_unique(k, v) |
SqlFunctions.Postgres.json_object_agg_unique_strict(k, v) |
json_object_agg_unique_strict(k, v) |
PostgreSQL 16 aggregates. The two array forms ignore NULL values: a group whose values are all NULL
yields [] (not an array of nulls, measured), empty input still produces one NULL row, and the
json/jsonb result is read back as a string. json_object_agg_unique keeps a NULL value as JSON
null, while json_object_agg_unique_strict drops a NULL-valued pair; both raise 22030 on a
duplicate key and 22004 on a NULL key (measured on 17.11), and empty input yields one NULL row.
SQL/JSON aggregates (json_arrayagg, json_objectagg)
| C# | SQL |
|---|---|
SqlFunctions.Postgres.json_arrayagg(x) |
json_arrayagg(x) |
SqlFunctions.Postgres.jsonb_arrayagg(x) |
json_arrayagg(x returning jsonb) |
SqlFunctions.Postgres.json_objectagg(k, v) |
json_objectagg(k value v) |
SqlFunctions.Postgres.json_objectagg_unique(k, v) |
json_objectagg(k value v with unique keys) |
SqlFunctions.Postgres.jsonb_objectagg(k, v) |
json_objectagg(k value v returning jsonb) |
PostgreSQL 17 parser-level constructs with no pg_proc row. json_arrayagg defaults to
ABSENT ON NULL: NULL values are dropped, an all-NULL group yields [], and empty input yields NULL.
json_objectagg admits exactly one pair per call (key value value; the comma-separated two-pair
form is 42601) and defaults to NULL ON NULL with duplicate keys kept, while with unique keys
raises 22030; a NULL key raises 22004, and empty input yields NULL. The key is not cast — the
server coerces it to text (measured with an integer key). jsonb_arrayagg and jsonb_objectagg are
member names for the RETURNING jsonb clauses, since PostgreSQL has no functions of those names. The
builder has no clause slot for an aggregate's internal argument list, so ORDER BY …,
ABSENT|NULL ON NULL and DISTINCT are not exposed; the DISTINCT spelling is a syntax error on
17.11 (42601, measured). Every result is json/jsonb read back as a string.
grouping
| C# | SQL |
|---|---|
SqlFunctions.Postgres.grouping(column) |
grouping(column) |
One bit of the super-aggregate identity of the current row: 0 when the row groups the column, 1
when the row is a super-aggregate that aggregated it away. The argument must be an expression of the
query's own grouping list — anything else raises 42803 — and the function is only useful inside a
GROUP BY ROLLUP/CUBE/GROUPING SETS grouping; in a plain GROUP BY it is legal and always 0.
Nextorm's GroupByRollup(...) emits that grouping (as do GroupByCube and GroupByGroupingSets), so
grouping can be projected next to the aggregate columns. The zero-argument grouping() is a syntax
error (42601), and the result is integer (int?). Measured on 17.11. It returns 0 or 1 for
every row; there is no NULL or empty-input case.
ClickHouse aggregate catalogue (#286, T286b)
The residual ClickHouse aggregate catalogue is typed on ClickHouseFunctions and dispatched by
AdvancedAggregateTranslator over the ClickHouse-only IClickHouseAggregateRenderer capability (null on
every other provider, so a call fails closed before SQL). Parametric aggregates use the vendor
two-parenthesis grammar — the accuracy/level/limit parameters are the leading CLR arguments and the value
arguments form the second pair — and the result is normalised to the declared CLR shape (toInt64,
toInt32, toFloat64, or a composite cast for Array/Tuple results).
| Group | Members |
|---|---|
| Statistical | entropy, simple_linear_regression, delta_sum, delta_sum_timestamp, gini, skew_pop, skew_samp, kurt_pop, kurt_samp, rank_corr, contingency, cramers_v, cramers_v_bias_corrected, theils_u, welch_t_test, mann_whitney_u_test, kolmogorov_smirnov_test, mean_z_test, analysis_of_variance, student_t_test, student_t_test_one_sample |
| Quantiles | quantile_tdigest, quantile_gk, quantile_dd, quantile_bfloat16, quantile_prometheus_histogram, quantiles, quantiles_exact, quantiles_exact_low, quantiles_exact_high, quantiles_exact_weighted, quantiles_timing, quantiles_tdigest, quantiles_bfloat16, quantiles_dd, quantiles_gk, quantiles_prometheus_histogram, uniq_up_to |
| Top-K and maps | approx_top_k, approx_top_sum, any_heavy, max_map, min_map |
| Group-array family | group_array, group_array_moving_avg, group_array_moving_sum, group_array_sample, group_array_sorted, group_array_last, group_array_insert_at, group_array_intersect, group_concat |
| Counting and sums | sum_count, sum_kahan, histogram, sparkbar, group_format, max_intersections (+max_intersections_position) |
| Row picking | first_value, last_value (aggregate forms; the window forms stay on CommonFunctions) |
| Sequences | sequence_count, sequence_match_events, sequence_next_node |
| Time series | time_series_group_sum, time_series_group_rate_sum, time_series_instant_rate_to_grid, time_series_rate_to_grid, time_series_resample_to_grid_with_staleness; window-only exponential_time_decayed_sum/_max/_count/_avg (complete them with Over(...)) |
Version facts are pinned, not hidden: gini, student_t_test_one_sample, group_format and
quantile_prometheus_histogram were introduced after 25.8 and time_series_group_sum/
time_series_group_rate_sum were removed, so their arms render the vendor name and the live test pins
UNKNOWN_FUNCTION. sequence_next_node needs allow_experimental_funnel_functions and the time-series
grids need allow_experimental_time_series_aggregate_functions. Three members stay refused
(stochastic_linear_regression, stochastic_logistic_regression, exponential_moving_average: opaque
AggregateFunction states) — see
ClickHouse-specific SQL › Not yet supported.