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

MySQL
SELECT GROUP_CONCAT(sku SEPARATOR ', ') FROM products;
PostgreSQL
SELECT string_agg(sku, ', ') FROM products;
The separator becomes a plain second argument instead of a trailing SEPARATOR keyword clause.

With explicit ordering

MySQL
SELECT GROUP_CONCAT(name ORDER BY created_at) FROM tags;
PostgreSQL
SELECT string_agg(name, ',' ORDER BY created_at) FROM tags;
ORDER BY moves inside the function call in PostgreSQL, appearing after the expression rather than as a separate clause on the whole GROUP_CONCAT() the way MySQL writes it.

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.

Analyze a schema free

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.