AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

06 Aggregation / Lesson 5 of 6

GROUP_CONCAT — a group's values as one string

12 min · +10 XP · 2 exercises · 4 questions

Read the text12 min
Exercises0/2
Quiz4 q

Quick answer

What does GROUP_CONCAT do in MySQL?

GROUP_CONCAT is an aggregate function that joins the values of a group into one string, separated by commas by default. GROUP_CONCAT(DISTINCT product ORDER BY product SEPARATOR ', ') lists each group's different products in alphabetical order. It skips NULLs, and its result is cut at 1,024 characters unless group_concat_max_len is raised. PostgreSQL and SQL Server call it STRING_AGG.

Aggregates so far have returned numbers: a count, a total, an average. Sometimes the answer a report wants is the list itself: the products in each order, the skills of each candidate, the days a user was active, the tags on a post. GROUP_CONCAT is MySQL's aggregate for that — it joins a group's values into a single string. It is also behind a well-known interview question: for each date, the number of different products sold and their names in alphabetical order.

The basics

SELECT order_id,
       GROUP_CONCAT(product) AS products
FROM order_lines
GROUP BY order_id
ORDER BY order_id;
order_idproducts
1pen,ink,paper
2stapler

Like every aggregate, GROUP_CONCAT returns one value per group. The values are joined with a comma and no space by default, and their order inside the string is not guaranteed — pen,ink,paper is just the order the rows were read in.

The full form

GROUP_CONCAT([DISTINCT] expr [ORDER BY sort_expr [ASC|DESC]] [SEPARATOR 'text'])

All three options go inside the parentheses, in that order:

SELECT order_id,
       GROUP_CONCAT(DISTINCT product ORDER BY product SEPARATOR ', ') AS products
FROM order_lines
GROUP BY order_id
ORDER BY order_id;
order_idproducts
1ink, paper, pen
2stapler
  • DISTINCT drops repeated values within the group, so an order with two lines of pen lists it once.
  • ORDER BY sorts the values inside the string. It is independent of the query's own ORDER BY, which sorts the groups. Always write it when the output is compared or shown to people — the stored order is not an order.
  • SEPARATOR sets the text between values. SEPARATOR '' joins with nothing.

The ORDER BY inside can sort by a different column from the one listed — GROUP_CONCAT(product ORDER BY added_at) lists products in the order they were added — and by several keys, ORDER BY qty DESC, product.

The interview classic: products sold by date

Given a table of (sell_date, product) rows with repeats, return for each date the number of different products and their names, sorted, comma-separated:

SELECT sell_date,
       COUNT(DISTINCT product)                                   AS num_sold,
       GROUP_CONCAT(DISTINCT product ORDER BY product SEPARATOR ',') AS products
FROM activities
GROUP BY sell_date
ORDER BY sell_date;
sell_datenum_soldproducts
2020-05-303Basketball,Headphone,T-shirt
2020-06-012Bible,Pencil
2020-06-021Mask

Three things make it right: DISTINCT in both functions (a product sold twice on one day counts once), ORDER BY inside GROUP_CONCAT, and ORDER BY sell_date for the rows.

Concatenating expressions

The argument can be any expression, so each item can be built from several columns:

SELECT customer_id,
       GROUP_CONCAT(CONCAT(product, ' x', qty) ORDER BY product SEPARATOR '; ') AS basket
FROM order_lines
GROUP BY customer_id
ORDER BY customer_id;
customer_idbasket
7ink x2; pen x1

MySQL also accepts several arguments — GROUP_CONCAT(product, ':', qty) concatenates them per row — but the CONCAT form is clearer and behaves the same.

NULLs

GROUP_CONCAT skips NULL values, like the other aggregates. If every value in a group is NULL — or the group's item expression is NULL for every row, as happens when CONCAT meets a NULL column — the result is NULL rather than an empty string. A group of no rows never exists in GROUP BY, so that case only appears in a whole-table GROUP_CONCAT with a WHERE that matches nothing.

SELECT team, COALESCE(GROUP_CONCAT(nickname ORDER BY nickname), '(none)') AS nicknames
FROM players
GROUP BY team
ORDER BY team;

The length limit

The result of GROUP_CONCAT is capped at group_concat_max_len characters, 1,024 by default. Longer results are cut silently — MySQL only raises a warning that applications rarely read. A list of a thousand order ids will come back as roughly the first two hundred. The fix is a session setting before the query:

SET SESSION group_concat_max_len = 1000000;

That limit is also a hint about design: GROUP_CONCAT is for short, human-readable lists in reports. When a program needs the items, return them as rows and let the program group them.

Not a substitute for rows

A comma-separated list is convenient to read and awkward to use: you cannot join to it, index it or filter it reliably (WHERE products LIKE '%pen%' also matches pencil). Store data as rows, one value per row — the first rule of normalisation (Module 12) — and use GROUP_CONCAT only at the very end of a query, to present the result. The reverse operation, splitting a stored comma list back into rows, is much harder in MySQL than avoiding the list in the first place.

Other databases

DatabaseFunction
MySQL, MariaDB, SQLiteGROUP_CONCAT(x ORDER BY x SEPARATOR ', ') (SQLite: GROUP_CONCAT(x, ', '))
PostgreSQLSTRING_AGG(x, ', ' ORDER BY x)
SQL Server 2017+STRING_AGG(x, ', ') WITHIN GROUP (ORDER BY x)
OracleLISTAGG(x, ', ') WITHIN GROUP (ORDER BY x)

STRING_AGG has no default separator — it must be given — and has no DISTINCT in SQL Server, where you de-duplicate in a subquery first. PostgreSQL also has ARRAY_AGG, which builds a real array rather than a string.

Pitfalls

  • No ORDER BY inside the call, so the list's order changes between runs.
  • Putting SEPARATOR before ORDER BY — it must come last.
  • Forgetting DISTINCT when the same value appears on several rows.
  • Silent truncation at 1,024 characters.
  • Storing comma-separated lists instead of rows.

Key takeaways

  • GROUP_CONCAT joins a group's values into one string, comma-separated by default.
  • DISTINCT, ORDER BY and SEPARATOR go inside the call, in that order.
  • NULLs are skipped; an all-NULL group gives NULL.
  • Results are cut at group_concat_max_len (1,024) characters.
  • PostgreSQL and SQL Server spell it STRING_AGG; Oracle LISTAGG.

Common questions

How do I change the separator in GROUP_CONCAT?

Add SEPARATOR as the last part inside the parentheses: GROUP_CONCAT(name SEPARATOR '; '). The default is a comma with no space. SEPARATOR '' joins the values with nothing between them, which is handy for building codes or initials.

How do I sort the values inside GROUP_CONCAT?

Put ORDER BY inside the function call, before any SEPARATOR: GROUP_CONCAT(name ORDER BY name SEPARATOR ', '). Without it the order of the values is whatever order MySQL read the rows in, which can change. The query's outer ORDER BY sorts the groups, not the values within one string.

Why is my GROUP_CONCAT result cut off?

MySQL limits the result to group_concat_max_len characters, 1,024 by default, and silently truncates anything longer with only a warning. Raise it for the session with SET SESSION group_concat_max_len = 100000; before the query. Long lists are often better returned as rows anyway.

What is the equivalent of GROUP_CONCAT in PostgreSQL and SQL Server?

Both use STRING_AGG(value, separator). In PostgreSQL the order goes inside, STRING_AGG(name, ', ' ORDER BY name); SQL Server 2017 and later use STRING_AGG(name, ', ') WITHIN GROUP (ORDER BY name). Oracle's version is LISTAGG. None of them has a default separator, so it must always be given.

Mark as readSign in to keep your place.

Concatenate Grouped Values in MySQL: Separator and Order By

MySQL's group concat function joins a group's values into one string with DISTINCT, ORDER BY and SEPARATOR. The 1024-character limit, NULLs and other databases.

  • Course: SQL study plan
  • Module: Aggregation
  • Kind: Lesson
  • Reading time: 12 min
  • Runtime: MySQL 8 syntax on SQLite 3.49

What does GROUP_CONCAT do in MySQL?

GROUP_CONCAT is an aggregate function that joins the values of a group into one string, separated by commas by default. GROUP_CONCAT(DISTINCT product ORDER BY product SEPARATOR ', ') lists each group's different products in alphabetical order. It skips NULLs, and its result is cut at 1,024 characters unless group_concat_max_len is raised. PostgreSQL and SQL Server call it STRING_AGG.

Lesson

Aggregates so far have returned numbers: a count, a total, an average. Sometimes the answer a report wants is the list itself: the products in each order, the skills of each candidate, the days a user was active, the tags on a post. GROUP_CONCAT is MySQL's aggregate for that — it joins a group's values into a single string. It is also behind a well-known interview question: for each date, the number of different products sold and their names in alphabetical order.

The basics

SELECT order_id,
       GROUP_CONCAT(product) AS products
FROM order_lines
GROUP BY order_id
ORDER BY order_id;
order_idproducts
1pen,ink,paper
2stapler

Like every aggregate, GROUP_CONCAT returns one value per group. The values are joined with a comma and no space by default, and their order inside the string is not guaranteed — pen,ink,paper is just the order the rows were read in.

The full form

GROUP_CONCAT([DISTINCT] expr [ORDER BY sort_expr [ASC|DESC]] [SEPARATOR 'text'])

All three options go inside the parentheses, in that order:

SELECT order_id,
       GROUP_CONCAT(DISTINCT product ORDER BY product SEPARATOR ', ') AS products
FROM order_lines
GROUP BY order_id
ORDER BY order_id;
order_idproducts
1ink, paper, pen
2stapler
  • DISTINCT drops repeated values within the group, so an order with two lines of pen lists it once.
  • ORDER BY sorts the values inside the string. It is independent of the query's own ORDER BY, which sorts the groups. Always write it when the output is compared or shown to people — the stored order is not an order.
  • SEPARATOR sets the text between values. SEPARATOR '' joins with nothing.

The ORDER BY inside can sort by a different column from the one listed — GROUP_CONCAT(product ORDER BY added_at) lists products in the order they were added — and by several keys, ORDER BY qty DESC, product.

The interview classic: products sold by date

Given a table of (sell_date, product) rows with repeats, return for each date the number of different products and their names, sorted, comma-separated:

SELECT sell_date,
       COUNT(DISTINCT product)                                   AS num_sold,
       GROUP_CONCAT(DISTINCT product ORDER BY product SEPARATOR ',') AS products
FROM activities
GROUP BY sell_date
ORDER BY sell_date;
sell_datenum_soldproducts
2020-05-303Basketball,Headphone,T-shirt
2020-06-012Bible,Pencil
2020-06-021Mask

Three things make it right: DISTINCT in both functions (a product sold twice on one day counts once), ORDER BY inside GROUP_CONCAT, and ORDER BY sell_date for the rows.

Concatenating expressions

The argument can be any expression, so each item can be built from several columns:

SELECT customer_id,
       GROUP_CONCAT(CONCAT(product, ' x', qty) ORDER BY product SEPARATOR '; ') AS basket
FROM order_lines
GROUP BY customer_id
ORDER BY customer_id;
customer_idbasket
7ink x2; pen x1

MySQL also accepts several arguments — GROUP_CONCAT(product, ':', qty) concatenates them per row — but the CONCAT form is clearer and behaves the same.

NULLs

GROUP_CONCAT skips NULL values, like the other aggregates. If every value in a group is NULL — or the group's item expression is NULL for every row, as happens when CONCAT meets a NULL column — the result is NULL rather than an empty string. A group of no rows never exists in GROUP BY, so that case only appears in a whole-table GROUP_CONCAT with a WHERE that matches nothing.

SELECT team, COALESCE(GROUP_CONCAT(nickname ORDER BY nickname), '(none)') AS nicknames
FROM players
GROUP BY team
ORDER BY team;

The length limit

The result of GROUP_CONCAT is capped at group_concat_max_len characters, 1,024 by default. Longer results are cut silently — MySQL only raises a warning that applications rarely read. A list of a thousand order ids will come back as roughly the first two hundred. The fix is a session setting before the query:

SET SESSION group_concat_max_len = 1000000;

That limit is also a hint about design: GROUP_CONCAT is for short, human-readable lists in reports. When a program needs the items, return them as rows and let the program group them.

Not a substitute for rows

A comma-separated list is convenient to read and awkward to use: you cannot join to it, index it or filter it reliably (WHERE products LIKE '%pen%' also matches pencil). Store data as rows, one value per row — the first rule of normalisation (Module 12) — and use GROUP_CONCAT only at the very end of a query, to present the result. The reverse operation, splitting a stored comma list back into rows, is much harder in MySQL than avoiding the list in the first place.

Other databases

DatabaseFunction
MySQL, MariaDB, SQLiteGROUP_CONCAT(x ORDER BY x SEPARATOR ', ') (SQLite: GROUP_CONCAT(x, ', '))
PostgreSQLSTRING_AGG(x, ', ' ORDER BY x)
SQL Server 2017+STRING_AGG(x, ', ') WITHIN GROUP (ORDER BY x)
OracleLISTAGG(x, ', ') WITHIN GROUP (ORDER BY x)

STRING_AGG has no default separator — it must be given — and has no DISTINCT in SQL Server, where you de-duplicate in a subquery first. PostgreSQL also has ARRAY_AGG, which builds a real array rather than a string.

Pitfalls

  • No ORDER BY inside the call, so the list's order changes between runs.
  • Putting SEPARATOR before ORDER BY — it must come last.
  • Forgetting DISTINCT when the same value appears on several rows.
  • Silent truncation at 1,024 characters.
  • Storing comma-separated lists instead of rows.

Key takeaways

  • GROUP_CONCAT joins a group's values into one string, comma-separated by default.
  • DISTINCT, ORDER BY and SEPARATOR go inside the call, in that order.
  • NULLs are skipped; an all-NULL group gives NULL.
  • Results are cut at group_concat_max_len (1,024) characters.
  • PostgreSQL and SQL Server spell it STRING_AGG; Oracle LISTAGG.

Common questions

How do I change the separator in GROUP_CONCAT?

Add SEPARATOR as the last part inside the parentheses: GROUP_CONCAT(name SEPARATOR '; '). The default is a comma with no space. SEPARATOR '' joins the values with nothing between them, which is handy for building codes or initials.

How do I sort the values inside GROUP_CONCAT?

Put ORDER BY inside the function call, before any SEPARATOR: GROUP_CONCAT(name ORDER BY name SEPARATOR ', '). Without it the order of the values is whatever order MySQL read the rows in, which can change. The query's outer ORDER BY sorts the groups, not the values within one string.

Why is my GROUP_CONCAT result cut off?

MySQL limits the result to group_concat_max_len characters, 1,024 by default, and silently truncates anything longer with only a warning. Raise it for the session with SET SESSION group_concat_max_len = 100000; before the query. Long lists are often better returned as rows anyway.

What is the equivalent of GROUP_CONCAT in PostgreSQL and SQL Server?

Both use STRING_AGG(value, separator). In PostgreSQL the order goes inside, STRING_AGG(name, ', ' ORDER BY name); SQL Server 2017 and later use STRING_AGG(name, ', ') WITHIN GROUP (ORDER BY name). Oracle's version is LISTAGG. None of them has a default separator, so it must always be given.

Exercises

Products sold by date

The activities table records each sale of a product; the same product can be sold several times a day.

sell_dateproduct
2020-05-30Headphone
2020-06-01Pencil
2020-06-02Mask
2020-05-30Basketball
2020-06-01Bible
2020-06-02Mask
2020-05-30T-Shirt

For each date return sell_date, num_sold (the number of different products sold) and products (those products, each once, in alphabetical order, joined by a comma with no space). Order by sell_date.

sell_date | num_sold | products
2020-05-30 | 3 | Basketball,Headphone,T-Shirt
2020-06-01 | 2 | Bible,Pencil
2020-06-02 | 1 | Mask

Team rosters

Build one line per team from players. A player's position may be unknown (NULL).

teamnameposition
redAshaGK
redBenFW
blueCaraDF
redDevFW
blueEliFW

Return team, roster — every player as name (position), with ? for an unknown position, ordered by name and separated by ; — and positions — the team's different known positions in alphabetical order separated by / (NULL when none is known). Order by team.

team | roster | positions
blue | Cara (DF); Eli (FW) | DF/FW
red | Asha (GK); Ben (FW); Dev (FW) | FW/GK

In this module: Aggregation

← Conditional aggregation — SUM(CASE …) and pivots · Grouping pitfalls — ONLY_FULL_GROUP_BY, COUNT and averages →