À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.
À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 temGROUP_CONCATe o ClickHouse usa o pararrayStringConcat(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,orderse 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 sertext(ou tipos compatíveis). Se o seu valor não for uma string, faça a conversão explícita com::textouCAST: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
productscontendo "Mouse, Keyboard, Monitor". Como qualquer agregado,STRING_AGGignora os NULL: as linhas em queproduct_name IS NULLsã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 BYdentro 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
DISTINCTlogo antes da expressão. Um detalhe: comDISTINCT, você só pode usarORDER BYsobre 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áusulaSEPARATORe a ordenação usaORDER BYdentro 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;DISTINCTtambém funciona:GROUP_CONCAT(DISTINCT currency SEPARATOR ', '). O separador padrão é a vírgula, então você pode omitirSEPARATORse isso lhe convier.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_AGGdireto; em vez disso, você compõe duas funções. PrimeirogroupArray()coleta os valores de um grupo em um array, depoisarrayStringConcat()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, usegroupUniqArrayno lugar degroupArray:SELECT customer_country, arrayStringConcat(arraySort(groupUniqArray(currency)), ', ') AS currencies FROM orders GROUP BY customer_country;Observe que
arrayStringConcatsó lida com strings: converta os campos numéricos comtoString(...)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, eSTRING_AGGsemDISTINCTrepete os produtos várias vezes. Resolva isso comDISTINCT, 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_AGGe companhia ignoram osNULL— envolva os valores emCOALESCEse os dados ausentes forem significativos.ORDER BYdentro do agregado, a ordem não é garantida.group_concat_max_len.