String aggregation (GROUP_CONCAT / STRING_AGG / LISTAGG / ClickHouse
array-concat), dispatched per-dialect. This is the single most
error-prone aggregate across dialects, so each branch below documents the
exact verified syntax.
MySQL / MariaDB:
GROUP_CONCAT([DISTINCT] expr [ORDER BY col [ASC|DESC]] SEPARATOR 'delim')
Clause order is fixed: DISTINCT, then the expression, then ORDER BY,
then SEPARATOR — SEPARATOR must come last and is a keyword, not a comma
argument.
SQLite: GROUP_CONCAT(expr, 'delim') (2-argument form; the
separator defaults to , if omitted). SQLite's GROUP_CONCAT does
not support an ORDER BY clause inside the function call at all —
if options.orderBy is passed for SQLite this throws, since silently
ignoring it would produce non-deterministic ordering that looks
intentional. Pre-sort via a subquery/CTE if ordering is required.
SQLite's DISTINCT variant (GROUP_CONCAT(DISTINCT expr)) is supported
but forces the default comma separator (a custom separator combined
with DISTINCT is not supported by SQLite) — this also throws if both
distinct and a non-default delimiter are requested together.
PostgreSQL / CockroachDB: STRING_AGG([DISTINCT] expr, 'delim' [ORDER BY col [ASC|DESC]])
— the ORDER BY clause sits inside the same parentheses as the
delimiter argument (no WITHIN GROUP).
MSSQL: STRING_AGG(expr, 'delim') WITHIN GROUP (ORDER BY col [ASC|DESC])
— note this is a genuinely different clause placement than Postgres:
the ORDER BY is a separate WITHIN GROUP (...) clause appended after
the function call, not inline inside the argument list. MSSQL's
STRING_AGG also does not support a DISTINCT argument at all
(unlike Postgres) — this throws if distinct: true is requested.
Oracle / Db2: LISTAGG([DISTINCT] expr, 'delim') WITHIN GROUP (ORDER BY col [ASC|DESC])
— same WITHIN GROUP placement as MSSQL. Oracle supports DISTINCT
inside LISTAGG since 19c; Db2 LUW's LISTAGG also supports DISTINCT.
Redshift: LISTAGG([DISTINCT] expr, 'delim') WITHIN GROUP (ORDER BY col [ASC|DESC])
— Redshift's aggregate string function has always been LISTAGG
(matching Oracle/Db2 syntax), not STRING_AGG. AWS did add a
Postgres-compatible STRING_AGG alias to Redshift more recently, but
LISTAGG is the long-standing, universally-available, and
ORDER-BY-capable form, so it's used here for correctness.
Snowflake: LISTAGG([DISTINCT] expr, 'delim') WITHIN GROUP (ORDER BY col [ASC|DESC])
— Snowflake's string aggregation function is LISTAGG (Oracle-style),
notSTRING_AGG — Snowflake has no STRING_AGG function.
ClickHouse: no native GROUP_CONCAT/STRING_AGG/LISTAGG. The
verified, version-portable idiom is
arrayStringConcat(groupArray(expr), 'delim') (distinct:
arrayStringConcat(groupUniqArray(expr), 'delim')). Newer ClickHouse
releases (24.x+) also ship a MySQL-compatible groupConcat(expr)
function, but arrayStringConcat(groupArray(...), ...) works on every
supported version so it's used as the default here. ClickHouse's
groupArray has no inline ORDER BY sub-clause (it preserves block
processing order, not a guaranteed sort) — options.orderBy therefore
throws for ClickHouse; pre-sort via a subquery if ordering matters.
String aggregation (
GROUP_CONCAT/STRING_AGG/LISTAGG/ ClickHouse array-concat), dispatched per-dialect. This is the single most error-prone aggregate across dialects, so each branch below documents the exact verified syntax.MySQL / MariaDB:
GROUP_CONCAT([DISTINCT] expr [ORDER BY col [ASC|DESC]] SEPARATOR 'delim')Clause order is fixed: DISTINCT, then the expression, then ORDER BY, then SEPARATOR — SEPARATOR must come last and is a keyword, not a comma argument.SQLite:
GROUP_CONCAT(expr, 'delim')(2-argument form; the separator defaults to,if omitted). SQLite's GROUP_CONCAT does not support anORDER BYclause inside the function call at all — ifoptions.orderByis passed for SQLite this throws, since silently ignoring it would produce non-deterministic ordering that looks intentional. Pre-sort via a subquery/CTE if ordering is required. SQLite's DISTINCT variant (GROUP_CONCAT(DISTINCT expr)) is supported but forces the default comma separator (a custom separator combined with DISTINCT is not supported by SQLite) — this also throws if bothdistinctand a non-default delimiter are requested together.PostgreSQL / CockroachDB:
STRING_AGG([DISTINCT] expr, 'delim' [ORDER BY col [ASC|DESC]])— the ORDER BY clause sits inside the same parentheses as the delimiter argument (no WITHIN GROUP).MSSQL:
STRING_AGG(expr, 'delim') WITHIN GROUP (ORDER BY col [ASC|DESC])— note this is a genuinely different clause placement than Postgres: the ORDER BY is a separateWITHIN GROUP (...)clause appended after the function call, not inline inside the argument list. MSSQL'sSTRING_AGGalso does not support aDISTINCTargument at all (unlike Postgres) — this throws ifdistinct: trueis requested.Oracle / Db2:
LISTAGG([DISTINCT] expr, 'delim') WITHIN GROUP (ORDER BY col [ASC|DESC])— same WITHIN GROUP placement as MSSQL. Oracle supportsDISTINCTinside LISTAGG since 19c; Db2 LUW's LISTAGG also supportsDISTINCT.Redshift:
LISTAGG([DISTINCT] expr, 'delim') WITHIN GROUP (ORDER BY col [ASC|DESC])— Redshift's aggregate string function has always beenLISTAGG(matching Oracle/Db2 syntax), notSTRING_AGG. AWS did add a Postgres-compatibleSTRING_AGGalias to Redshift more recently, butLISTAGGis the long-standing, universally-available, and ORDER-BY-capable form, so it's used here for correctness.Snowflake:
LISTAGG([DISTINCT] expr, 'delim') WITHIN GROUP (ORDER BY col [ASC|DESC])— Snowflake's string aggregation function isLISTAGG(Oracle-style), notSTRING_AGG— Snowflake has noSTRING_AGGfunction.ClickHouse: no native
GROUP_CONCAT/STRING_AGG/LISTAGG. The verified, version-portable idiom isarrayStringConcat(groupArray(expr), 'delim')(distinct:arrayStringConcat(groupUniqArray(expr), 'delim')). Newer ClickHouse releases (24.x+) also ship a MySQL-compatiblegroupConcat(expr)function, butarrayStringConcat(groupArray(...), ...)works on every supported version so it's used as the default here. ClickHouse'sgroupArrayhas no inlineORDER BYsub-clause (it preserves block processing order, not a guaranteed sort) —options.orderBytherefore throws for ClickHouse; pre-sort via a subquery if ordering matters.