skip to content

What does LISTAGG do, and what replaces it in engines that do not have it?

level: juniorimportance: must knowfreq 55%

answer

  1. Collapses a group into one text value
  2. Separator sits between elements, not around
  3. The standard names it an ordered-set aggregate
  4. LISTAGG, STRING_AGG, GROUP_CONCAT are the same idea

basics

~20 s

LISTAGG concatenates a group's values into one delimited string, as in LISTAGG(tag, ', ') WITHIN GROUP (ORDER BY tag). Engines lacking it spell the same idea STRING_AGG or GROUP_CONCAT, and ARRAY_AGG collects the values into an array instead.

solid answer

~50 s

`LISTAGG` is the standard's string-aggregation function: it reduces a group to a single text value containing every input value joined by a separator, ordered by the `WITHIN GROUP (ORDER BY …)` clause. So `SELECT post_id, LISTAGG(tag, ', ') WITHIN GROUP (ORDER BY tag) FROM post_tags GROUP BY post_id` gives one row per post with its tags as a comma-separated list. The name is not portable: PostgreSQL and SQL Server spell it `STRING_AGG`, MySQL and SQLite spell it `GROUP_CONCAT`, Oracle and Db2 use `LISTAGG`. The ordering syntax differs too — `WITHIN GROUP (ORDER BY …)` in some engines, an `ORDER BY` inside the argument list in others. Where the engine has an array type, `ARRAY_AGG` collects the same values into an array rather than a string, which keeps element boundaries intact instead of relying on a delimiter.

code

sql · 5 lines
sql
SELECT post_id,
       LISTAGG(tag, ', ') WITHIN GROUP (ORDER BY tag) AS tags
FROM   post_tags
GROUP  BY post_id;
-- one row per post: 1 | 'joins, sql'

go deeper

for a junior

Be ready to write the query on the spot: GROUP BY the parent key and concatenate the child column with an explicit separator. Know that the function's name differs by engine.

for a middle

Explain that this is an ordered-set aggregate, that the separator sits only between elements, and that NULL inputs are skipped while an all-NULL group yields NULL.

for a senior

Show judgment about result shape: a delimited string for reports, an array or plain rows when the client will parse it, and awareness that engine length caps make unbounded lists risky.

for a principal

Own the boundary question: how much presentation logic belongs in the query at all, given that every engine spells this differently and a concatenated list is a shape the database cannot index or constrain.

## What string aggregation is for Ordinary aggregates collapse a group into a single scalar summary: `COUNT` gives a number, `SUM` gives a total, `MIN` gives one member. String aggregation is the aggregate that collapses a group without discarding its members — it returns one value per group that still contains *every* input value, joined together by a separator. It is how you turn ``` post_id | tag 1 | sql 1 | joins 2 | nulls ``` into one row per post whose `tags` column reads `joins, sql`. ## The standard: LISTAGG SQL:2016 defines `LISTAGG(<expression> [, <separator>]) WITHIN GROUP (ORDER BY <sort list>)`. Three parts matter: - **The expression** is evaluated once per row in the group; it is normally a character value or something implicitly cast to one. - **The separator** is a constant string placed *between* elements — never before the first or after the last. Omit it and the values are concatenated with nothing between them. - **`WITHIN GROUP (ORDER BY …)`** is the standard's syntax for an *ordered-set aggregate*: an aggregate whose result depends on the order in which the group's rows are fed to it. Rows in a group have no inherent order, so without this clause the element order would be arbitrary. ```sql SELECT post_id, LISTAGG(tag, ', ') WITHIN GROUP (ORDER BY tag) AS tags FROM post_tags GROUP BY post_id; ``` ## The dialect landscape This is one of the least portable corners of everyday SQL — the concept is universal, the spelling is not: - **Oracle, Db2** — `LISTAGG(expr, sep) WITHIN GROUP (ORDER BY …)`. - **SQL Server 2017 and later** — `STRING_AGG(expr, sep) WITHIN GROUP (ORDER BY …)`. - **PostgreSQL** — `string_agg(expr, sep ORDER BY …)`, with the ordering written inside the argument list rather than in a `WITHIN GROUP` clause. - **MySQL and MariaDB** — `GROUP_CONCAT(expr ORDER BY … SEPARATOR ', ')`; the separator is a keyword-introduced clause, and the default separator is a comma with no space. - **SQLite** — `group_concat(expr, sep)`, where the separator is simply the second argument. So a query that concatenates tags has to be rewritten when it moves between engines. If you need one statement that runs everywhere, the only fully portable fallback is to return one row per value and join them in the application. ## Arrays instead of strings Where the engine supports an array type, `ARRAY_AGG(expr [ORDER BY …])` collects the group's values into an array rather than a string. That is usually the better result shape when the consumer is code rather than a human report: elements keep their boundaries and their type, so a value that itself contains a comma cannot be mistaken for two values, and there is no parsing step on the client. A delimited string is the right choice when the output is a display column, an export, or a message; an array is the right choice when the caller will iterate the elements. Engines without an array type simply do not offer this, which is why the string form is the more commonly seen idiom. ## NULLs, empty groups, and delimiters Like other aggregates, the string forms **ignore NULL inputs**: a NULL contributes no element and no separator, so you never see an empty slot in the output. If you want NULLs represented, wrap the input: `STRING_AGG(COALESCE(city, 'unknown'), ', ')`. A group that contributes no non-null value at all yields **NULL**, not an empty string — so `COALESCE(STRING_AGG(...), '')` is the idiom when the caller wants `''` for the empty case. And because the separator sits only between elements, *n* values produce exactly *n − 1* separators. ## Where it is used and where it bites Typical uses are report columns ("tags per article", "roles per user"), building a human-readable audit line, and flattening a child table for an export. Two traps show up immediately in real work. The first is **order**: without an explicit ordering clause inside the aggregate, the element order is unspecified and can change between runs or plans. The second is **length**: every engine caps how long the aggregated result may be, and they disagree on whether exceeding the cap raises an error or silently truncates. A third, smaller one is **delimiter collision** — if a value can itself contain the delimiter, the string is ambiguous, and that is precisely the case where an array or plain rows is the sounder answer.

  • How many separators appear in the result, and what happens to the last element?
    Separators go strictly between elements, so n values produce n − 1 separators and the string never ends with one. A group with a single value returns just that value; a group with no non-null values returns NULL rather than an empty string.
  • When would you return ARRAY_AGG instead of a delimited string?
    When the consumer is code, not a person. An array keeps element boundaries and element types, so a value containing a comma cannot be misread as two values and the client does not parse anything. A delimited string is for display columns, exports, and messages.
  • What does the aggregate return if the group's values are numbers or dates rather than text?
    They are converted to text before being joined, using the engine's default conversion, so the formatting of dates and decimals is engine- and locale-dependent. Cast or format explicitly when the output shape matters, rather than relying on the implicit conversion.

saying these in an interview costs you the question

  • Thinks the delimiter is also appended after the last element
  • Says the result is one row per value, not per group
  • Assumes LISTAGG works in every engine unchanged
  • Expects an empty group to return an empty string
  • Confuses it with DISTINCT or with a simple string concatenation of two columns

context