sqlpostgresqlmysqlclickhouse

STRING_AGG no SQL: concatenar linhas agrupadas com um delimitador e ORDER BY

Como transformar muitas linhas em uma única string delimitada e na ordem certa, usando STRING_AGG do PostgreSQL, GROUP_CONCAT do MySQL e arrayStringConcat(groupArray()) do ClickHouse.

3 min de leituraReferênciasql · postgresql · mysql · clickhouse · aggregation

Às vezes você não quer uma linha por valor, mas sim uma única célula: uma lista de tags separadas por vírgula, os e-mails de um pedido, os nomes de todos os integrantes de um projeto. Isso é agregação de strings: pegar todos os valores dentro de um grupo e colá-los em uma única string com um delimitador. O PostgreSQL oferece STRING_AGG, o MySQL tem GROUP_CONCAT e o ClickHouse usa o par arrayStringConcat(groupArray(...)). Vamos cobrir o básico, a ordenação, a remoção de duplicatas e as armadilhas que pegam muita gente.

STRING_AGG básico no PostgreSQL

Considere um esquema típico de loja: users, orders e as linhas dos itens do pedido. O caso mais simples é juntar o e-mail de cada usuário em uma única string:

SELECT STRING_AGG(email, ', ') AS all_emails
FROM users;

STRING_AGG(expression, delimiter) recebe dois argumentos: o que concatenar e com o que separar. Ambos precisam ser text (ou tipos compatíveis). Se o seu valor não for uma string, faça a conversão explícita com ::text ou CAST:

SELECT STRING_AGG(id::text, ',') AS user_ids
FROM users;

Na maioria das vezes você o combina com GROUP BY. Por exemplo, para reunir os nomes dos produtos comprados em cada pedido:

SELECT
  o.id AS order_id,
  STRING_AGG(oi.product_name, ', ') AS products
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id;

O resultado é uma linha por pedido, com products contendo "Mouse, Keyboard, Monitor". Como qualquer agregado, STRING_AGG ignora os NULL: as linhas em que product_name IS NULL são descartadas, e nenhum delimitador solto aparece em volta delas. É prático, mas pode esconder silenciosamente lacunas nos seus dados.

ORDER BY dentro do agregado

Sem uma ordem explícita, o banco concatena os valores em uma ordem arbitrária que pode mudar entre execuções. Quando a ordem importa — e em relatórios quase sempre importa — adicione ORDER BY dentro dos parênteses do agregado:

SELECT
  o.id AS order_id,
  STRING_AGG(oi.product_name, ', ' ORDER BY oi.product_name) AS products
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id;

Você também pode ordenar por outra coluna, não necessariamente a que está sendo concatenada. Um padrão comum é listar os itens na ordem em que foram adicionados:

SELECT
  o.id AS order_id,
  STRING_AGG(oi.product_name, ', ' ORDER BY oi.added_at DESC) AS products
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id;

Para remover duplicatas, coloque DISTINCT logo antes da expressão. Um detalhe: com DISTINCT, você só pode usar ORDER BY sobre a própria expressão concatenada.

SELECT
  customer_country,
  STRING_AGG(DISTINCT currency, ', ' ORDER BY currency) AS currencies
FROM orders
GROUP BY customer_country;

MySQL: GROUP_CONCAT

O equivalente no MySQL é GROUP_CONCAT, com sua própria sintaxe: o delimitador vai em uma cláusula SEPARATOR e a ordenação usa ORDER BY dentro da função.

SELECT
  o.id AS order_id,
  GROUP_CONCAT(oi.product_name ORDER BY oi.product_name SEPARATOR ', ') AS products
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id;

DISTINCT também funciona: GROUP_CONCAT(DISTINCT currency SEPARATOR ', '). O separador padrão é a vírgula, então você pode omitir SEPARATOR se isso lhe convier.

  • A grande armadilha do MySQL: o resultado é truncado em group_concat_max_len (1024 bytes por padrão), e isso acontece de forma silenciosa — sem erro. Em listas longas você vai obter uma string cortada. Aumente o valor por sessão: SET SESSION group_concat_max_len = 1000000;.

ClickHouse: arrayStringConcat(groupArray())

O ClickHouse não tem um STRING_AGG direto; em vez disso, você compõe duas funções. Primeiro groupArray() coleta os valores de um grupo em um array, depois arrayStringConcat() junta esse array em uma string delimitada:

SELECT
  order_id,
  arrayStringConcat(groupArray(product_name), ', ') AS products
FROM order_items
GROUP BY order_id;

Para ordenar, envolva o array coletado em arraySort; para garantir unicidade, use groupUniqArray no lugar de groupArray:

SELECT
  customer_country,
  arrayStringConcat(arraySort(groupUniqArray(currency)), ', ') AS currencies
FROM orders
GROUP BY customer_country;

Observe que arrayStringConcat só lida com strings: converta os campos numéricos com toString(...) antes de coletá-los no array, ou você esbarrará em um erro de tipo.

Granularidade e armadilhas

A armadilha compartilhada mais comum é a duplicação induzida pelo JOIN. Se você também juntar uma tabela de pagamentos a orders, cada linha de pedido se multiplica pelo número de pagamentos, e STRING_AGG sem DISTINCT repete os produtos várias vezes. Resolva isso com DISTINCT, ou agregando em uma subconsulta antes do join:

SELECT
  o.id,
  pr.products,
  SUM(p.amount) AS paid
FROM orders o
JOIN payments p ON p.order_id = o.id
JOIN (
  SELECT order_id, STRING_AGG(product_name, ', ' ORDER BY product_name) AS products
  FROM order_items
  GROUP BY order_id
) pr ON pr.order_id = o.id
GROUP BY o.id, pr.products;

O essencial:

  • STRING_AGG e companhia ignoram os NULL — envolva os valores em COALESCE se os dados ausentes forem significativos.
  • Sem ORDER BY dentro do agregado, a ordem não é garantida.
  • O MySQL trunca silenciosamente em group_concat_max_len.
  • Fique atento à granularidade do seu JOIN, ou você terá duplicatas na string concatenada.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador