MySQL GROUP_CONCAT() → PostgreSQL STRING_AGG()
Converting GROUP_CONCAT()'s separator, ordering, and DISTINCT clauses to string_agg(), which places the same pieces in different argument positions.
MEDIUM
Quick answer
string_agg(expr, separator) is PostgreSQL's equivalent of GROUP_CONCAT(expr SEPARATOR separator), but ORDER BY moves inside the aggregate call and DISTINCT moves before the expression — the pieces aren't in the same order as MySQL's syntax.
Why it breaks
GROUP_CONCAT(expr ORDER BY col SEPARATOR sep) packs the expression, an optional DISTINCT, an optional ORDER BY, and the separator into one MySQL-specific clause layout. PostgreSQL's string_agg(expr, sep) takes the separator as a plain second argument (not a trailing keyword clause), and its ORDER BY belongs inside the aggregate call itself: string_agg(expr ORDER BY col, sep).
Real examples
Basic concatenation
SELECT GROUP_CONCAT(sku SEPARATOR ', ') FROM products; SELECT string_agg(sku, ', ') FROM products; With explicit ordering
SELECT GROUP_CONCAT(name ORDER BY created_at) FROM tags; SELECT string_agg(name, ',' ORDER BY created_at) FROM tags; Safe migration options
- Basic case: GROUP_CONCAT(col) → string_agg(col::text, ',') — note the default separator in MySQL is a comma with no space, so match it explicitly rather than relying on any PostgreSQL default (string_agg has none — the separator is required).
- With ordering: GROUP_CONCAT(col ORDER BY other_col) → string_agg(col::text, ',' ORDER BY other_col) — the ORDER BY moves inside the call, after the expression, before the separator conceptually but written after it syntactically.
- With DISTINCT: GROUP_CONCAT(DISTINCT col) → string_agg(DISTINCT col::text, ',') — DISTINCT stays right before the expression in both, this part translates directly.
PostgreSQL solution
string_agg(expression, separator [ORDER BY sort_expression])
Validation
Before cutover
Run both queries against a copy of the same data (or a representative sample) and diff the concatenated output row-by-row — since ordering and separator placement are easy to get subtly wrong, a byte-for-byte comparison catches drift that eyeballing the SQL will not.
Scan your own schema
This page documents detector rule GROUP_CONCAT in the DevEquiv analyzer — paste or
upload your real MySQL/MariaDB schema to find every occurrence of this (and 19 other
documented issues) automatically, with a prioritized readiness report.
References
- MySQL Manual — GROUP_CONCAT() (v8.0) — checked 2026-07-30
- PostgreSQL Manual — Aggregate Functions (v17) — checked 2026-07-30
Last verified 2026-07-30.