GROUP_CONCAT( ) в Mysql.

04.10.2008
сайт автора: http://webi.ru
публикация данной статьи разрешена только со ссылкой на сайт автора статьи

GROUP_CONCAT( ) в Mysql.
Сегодня хочу рассказать об интересной функции в Mysql.
Как оказалось, знают ее очень не многие программисты, уж не знаю почему ей мало кто пользуется, но я опросил десятка два своих знакомых и меньше половины знали о GROUP_CONCAT.
Эта функция работает примерно как CONCAT_WS(Объединяет строки с разделителем), только в отличии от CONCAT_WS способна объеденить результаты выборки из таблицы.

Синтаксис
GROUP_CONCAT([DISTINCT] expr [,expr ...]
[ORDER BY {unsigned_integer | col_name | expr}
[ASC | DESC] [,col_name ...]]
[SEPARATOR str_val])

Приведу обычный пример с домашними животными
Например есть таблица, в которой каждому человеку присваивется жвивотное, которое у него есть.

CREATE TABLE `test` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(250) NOT NULL, # это имя человека
`pet` varchar(250) NOT NULL, # это домашнее животное
PRIMARY KEY (`id`)
);

Данные в табличке вот такие
маша - попугай
света - кошка
маша - хомяк
юля - собака
света - собака
маша - рыбки

Из этой таблички видно, что
у Маши есть попугай, хомяк и рыбки,
у Светы кошка и собака,
а у Юли только собака.
Так вот есть задача вывести всех людей из таблицы и чтобы возле каждого показывалось, какие животные у него есть.

Делаем вот такой запрос
SELECT `name`, GROUP_CONCAT(`pet`) as `pet`
FROM `test`
GROUP BY `name`

Результат получается такой
маша - попугай,хомяк,рыбки
света- кошка,собака
юля - собака

Получилось объединение всех животных каждого человека.
GROUP BY `name` в конце сгруппировывает одинаковые имена, можно сказать отбрасывает дубли.

Пример конечно не очень жизненый, не часто встречаются такие избыточные данные, обычно имена хранят в одной таблице, животных во второй, а связи в третьей таблице.
Вот как раз для такого случая и будет полезна GROUP_CONCAT( ).
Приведу пример с вложенным запросом.

SELECT DISTINCT `name` as `name1`,
(select GROUP_CONCAT(`pet`) as `pet` from `test` where `name` = `name1` )as `pet`
FROM `test`

Этот пример использует вложенный select для создания поля со списком животных...
DISTINCT отбрасывает одинаковые имена. Поле name переименовываем, чтобы можно было во вложенном запросе правильно обратиться.
Этот пример уже более похож на пример из жизни.
Выводятся имена, отбрасывая одинаковые, выдергивается список животных для каждого имени.
Все очень просто. Не нужно получать каждое имя, а затем выдергивать животных для каждого имени, все происходит одним запросом.

А теперь подробнее про эту хитрую GROUP_CONCAT.

Разделитель.
По умолчанию используется разделитель запятая.
Если есть необходимость, можно задать другой разделитель

SELECT DISTINCT `name` as `name1`,
(select GROUP_CONCAT(`pet` SEPARATOR '::') as `pet` from `test` where `name` = `name1` )as `pet`
FROM `test`

Сейчас животные будут разделены не запятой, а двойным двоеточием (::)

А остальные возможности этой функции понятны из описаного синтаксиса (сортировка и исключение одинаковых записей).

Несколько разочарований.

Ограничение 1024.
У этой функции есть ограничение на объем выводимых данных.
По умолчанию 1024 символа для каждого объединения - для каждой выводимой строки.
Если размер склееных данных больше, то он будет урезаться.
Чтобы расширить размер нужно выполнить команду SET group_concat_max_len =4096;
Если у вас есть привелегии, то вы расширите объем получаемых данных до 4096, можно и больше.
Но чаще всего на обычных хостингах таких привелегий нет.

Только текст.
Следущая особенность GROUP_CONCAT это работа только со строками.
Если вы захотите склеить числа, то у вас ничего не получится, нужно преобразовать число в текст.

Допустим вы хотите получить не животных, а список ID.
Обычный вариант работать не будет, нужно конвертировать число в текст, например вот так

SELECT DISTINCT `name` as `name1`,
(select CONVERT(GROUP_CONCAT(`id`) USING cp1251) from `test` where `name` = `name1` )as `pet_id`
FROM `test`

 

Вот такой вот обзор интересной функции GROUP_CONCAT().
Использовать ее лучше всего для информации, не стоит полагаться на то, что вы получите абсолютно все данные, так как данных может быть больше чем установлено в ограничении.
Например вы выводите на странице список товаров.
У каждого товара показывается цена, наличие, вес, краткое описание. И тут можно например для каждого товара показать еще в каких цветах имеется этот товар.
Так как на этой странице выводится много товаров, поэтому дополнительную информацию о товаре стоит показывать сжато, и вот тут и можно подсунуть информацию в каких цветах присутсвует товар. Вывести например 5 самых популярных цветов этого товара.
Совершенно очевидно, если выводятся 20 товаров на странице, то показывать для каждого товара информацию о цветах размером более 1024 символов вы никогда не будете, перегружать страницу нет смысла.
Вот в таких местах есть смысл использовать GROUP_CONCAT().




Комментарии

RSS комментарии


27.11.2008 Евгений
Не плохая заморочка



07.12.2008 Виола
Отличная возможность. Я знала о ней, но не знала, что там есть ограничение. Вот только непонятно, зачем преобразование в текст? Я получала как раз список ID и без преобразования.



07.12.2008 Вадим
Виола, наверное ваши ID хранились в текстовых полях.
Если поле в базе имеет числовой тип, то эта функция без преобразования не выведет ничего... Надо обязательно преобразовать в текст.



07.12.2008 Виола
Да нет, поле ID у меня было - int(11), автоинкрементное, первичный ключ. Возможно дело в том, что уже после выборки я делаю неявное преобразование в текст - цепляю результат к строке. Но в самой выборке ничего такого нет и она возвращает требуемый результат.



24.12.2008 Вадим
Ну тут сложно сказать в чем дело, возможно это связано как раз с вашими неявными преобразованиями...
Но я сам проверял на трех разных версиях mysql и конвертировать в текст обязательно нужно, иначе выдает не знаечение, а какую то информацию о поле.



24.02.2009 Сергио
Виола дело говорит, у меня тоже Int выводит нормально. Версия 5.1



24.04.2009 Гумер
СПАСИБО!!!

Очень понадобилось.



16.06.2009 Нигина
У меня ошибка, хотя все правилно скопировала;)



18.10.2009 tyman8992
mysql 5.0.51b, Таблица InnoDB - конвертирование INT(11) обязательно, только что проверил.



21.10.2009 Артур
Спасибо.
Получилось вывести результат в формате, аналогичном сводной таблице в Excel.



21.12.2009 Семён
По ходу есть еще одна особенность.
Результат объединения не может быть больше 255. Это печально :(



21.12.2009 админ
255 это совсем мало, может все таки 1024?



19.01.2010
mможно объеденять несколько полей и вставлять разделитель
например вполне работает
select msisdn, group_concat(week,'=',charging order by week) from y2009 group by msisdn
380xxxxxxxxxx 40=8,41=4,44=8,45=8,46=4,47=1,50=8,51=9,52=4,53=1




09.03.2010 Дмитрий
А у меня не получается вот запрос

SELECT product.id,title, catid, GROUP_CONCAT(`idSpecific`) AS `specific`
FROM `product`
LEFT JOIN `specification_items` ON product.id = idProduct
WHERE catid =46 GROUP BY title

Результат specific [BLOB - 5 Bytes]



09.03.2010 админ
эта функция работает только со строками.
а в вашем случае скорее всего идет обработка числа.
нужно конвертировать данные так
CONVERT(GROUP_CONCAT(`idSpecific`) USING cp1251)



24.03.2010 Дмитрий
Огромное спасибо переконвертировал :)



25.03.2010 AJ
а можно какнибудь избежать вложенных селектов? если в запросе нужно вывести несколько сотен (тысяч) записей, это же будет столько же вложенных запросов.



26.03.2010 виктор
а разве через JOIN не получается избежать вложенных селектов?
хотя join тут наверное не поможет, так как тут в одном запросе выбирается много строк...
наверное без вложенного селекта это не сделать.






24.05.2010 USvER
Хорошая функция! Помогла избежать изобретению велосипеда и собственной сериализации.

to AJ, А тут как не крути а подзапрос обязателен. Вопрос лиш в том кто будет выполнять этот подзапрос - клиент или сервер. Использовать термин "подзапрос" конечно не совсем коректно, я не уверен но скорее всего на сервере произойдет только одна выборка подзапроса сразу для всего внешнего запроса, что невозможно реализовать на клиенте.
Как жаль что есть ограничение. Оно вообще бессмысленно и обоснованно только как мера защиты отчего-то(Переполнение буфера?).





24.05.2010 USvER
Наверное неправильно выразился,
На сервере возможна оптимизация(в этом суть всех агрегатных функций).
Я точно не знаю насколько данная функция оптимизирована но она явно требует меньше ресурсов чем обработка данных в клиенте.

 

31.05.2010 Павел фром да Таганрог
Спасибо огромное за сию заметку !
В старой версии одного движка с которым мы работаем был атрибут представляющий строку склееную из нескольких значений другого атрибута..в новой версии его убрали...и функций для работы в старом стиле не оставили...
чуть голову не сломал...



03.08.2010 artoodetoo
Век живи - век учись. Спасибо, штучка может пригодиться!



09.08.2010 STEREOLOVER.CS
Народ бьюсь уже кучу времени. Есть запрос, объединяющий три таблицы. Пусть они будут называться car, box и load. Объёдинены они с использованием left join
см см см м3
car.name box.name box.height box.width box.depth load.name load.volume
жигули ящик 1 20 30 50 корова 0,5
жигули ящик 2 10 50 10 свинья 0,2
ламборгини ящик 3 15 15 20 картошка 0,1
ламборгини ящик 3 15 15 20 марковка 0,15
ламборгини ящик 4 10 10 10 null null



Там где нул как вы догадываетесь ящик без груза. Задача следующая. Нужно вычислить объём всех ящиков и объём всех грузов и группировать это всё по имени машины.

car.name box_volume load_volume
жигули 1,1 0,7
ламборгини 0,7 0,25


P.S. в box_volume не рассчитывал точно, а поставил цифры от балды. Дынные выше только для примера как должно выглядеть в итоге.

Сам запрос выглдядит примерно так:
select car.name

from car, box left join load on box.id=load.box_id
where....
group by car.name

Стал на, том что не могу посчитать объём всех ящиков, т.к. если просто указать SUM(box.height*box.width*box.depth)/1000000 у меня объём ящика 3 посчитается 2 раза, а если использовать вложенный запрос для выбора только подходящих строк из box между select и where, то GROUP BY car.name не группирует данные из подзапроса :( Подскажите как можно решить данную задачу?



10.08.2010 гросс
STEREOLOVER.CS, вот ты задачу загнул.
Даже интересно, сможет тебе кто-то ответить или нет...
Ведь тут только для того, чтобы вникнуть, надо час думать, а после этого еще решить данную задачу.
Я подобные задачи решал иначе,
делал дополнительную таблицу, в которой хранил подсчитанные результаты, в данном случае по каждой машине.
В итоге при выводе информации нагрузка на много меньше, чем создавать такие хитрые запросы как у тебя.



28.09.2010 Александр
Огромное спасибо автору за статью - давно искал такую функцию, уже отчаился найти, приходилось обходить через известное тёмное место :)
СПАСИБО!!!



19.10.2010 Искатель
Хорошая статья, спасибо! Подскажите пожалуйста:

Генерируется следующий запрос:

SELECT * FROM main_tbl WHERE caption LIKE '%слово_1%' OR caption LIKE 'слово_2' OR info LIKE '%слово_1%' OR info LIKE '%слово_2%'

Возможно ли каким-то образом склеить 2 поля в запросе для задания общего условия к ним.

Ну что-то наподобие такого:

SELECT * FROM main_tbl WHERE caption+info LIKE '%слово_1%' OR caption+info LIKE 'слово_2'

???

ps1.. Соответственно знак "+" я указал для понимания ... существует ли какая либо функция, или может быть GROUP_CONCAT() мне поможет, только каким образом это реализовать.

ps2.. На php механизм формирования запроса следующий (во избежании некоторых вопросов):

<?php
$search_words="слово_1 слово_2"; // слов может быть произвольно много, либо 1

$query = "SELECT * FROM main_tbl WHERE caption LIKE '%". str_replace(" ", "%' OR caption LIKE '%", $search_words). "%' OR info LIKE '%". str_replace(" ", "%' OR info LIKE '%", $search_words). "%'";
?>



19.10.2010 админ
Искатель, вариантов решения этого вопроса много, все зависит от объема и типа данных.
Конкретно функция которую вы просите вот:

CONCAT_WS(' ', `поле1`, `поле2 `, `поле3`, ...)
Эта функция склеивает данные. Первый аргумент, это разделитель который добавится между склееными данными, в данном примере пробел.

SELECT * FROM `main_tbl` WHERE CONCAT_WS(' ', `caption`, `info `) LIKE '%слово_1%' OR CONCAT_WS(' ', `caption`, `info `) LIKE 'слово_2'
Только не могу сказать на сколько это будет затратно по ресурсам, наверное склеивать каждый раз большие данные это не выгодно, но смотря какие данные у вас в ячейках, можно использовать полнотекстовый поиск, а можно и создавать временную таблицу и туда погружать данные и потом искать.
Все зависит уже от конкретной ситуации.



19.10.2010 Искатель
Админ, спасибо огромное! По настоящему выручили. Да нет, там размер ячеек 16+64 символа (правда в юникоде, но всё равно не много). Да и количество строк около 300.



10.06.2011 Nachtvolk
Статья аццки нужная, понятно изложена. Для жизненности не хватает только примера с JOIN`ами, придётся самому разгребать.



28.08.2011 dude125
Nachtvolk... вот с JOIN-ами для жизненности))

Стоит задача выбрать все записи из одной таблицы, где поле количество >0, объединив все артикулы из другой таблицы по Id записи в поле Articles, разделяя

значения запятой, а также наименования производителей по артикулу из третьей таблицы, объединив их в поле Mnf также разделяя запятой. Поскольку таких записей

более 100 000, поэтому ограничился 1-й записью для примера.

SELECT
p.`Id`,
GROUP_CONCAT(a.`Article` SEPARATOR ',') as Articles,
GROUP_CONCAT(m.`Name` SEPARATOR ',') as Mnf,
p.nameRu as Title,
p.ItemCount
from
(MnfsArts a inner join Manufacturers m on a.Mnf=m.Id)
inner join Product p on a.Product=p.Id
where
p.ItemCount > 0
and p.Id='10313'
GROUP BY p.Id
ORDER BY m.Name;

Все вроде бы нормально, но длина обединенных строк Articles и Mnf получается 21 байт, а должно быть гораздо больше. Потому как запросом без GROUP BY выводит

все записи с повторяющимся Id соответственно и если подсчитать количество записей, то видно что если объединить поля, длина должна быть больше чем 21 байт.
Немного поразмыслив, решил добавить конверт в строку. Типа:

SELECT
p.Id,
GROUP_CONCAT(CAST(a.Article as CHAR) SEPARATOR ',') as Articles,
GROUP_CONCAT(CAST(m.`Name` as CHAR) SEPARATOR ',') as Mnf,
p.nameRu as Title,
p.ItemCount
from
(MnfsArts a inner join Manufacturers m on a.Mnf=m.Id)
inner join Product p on a.Product=p.Id
where
p.ItemCount > 0
and p.Id='10313'
GROUP BY p.Id
ORDER BY m.Name;


теперь зараза обрубает после 64 символа.

Подскажите кто-нибудь как решить вопрос, может попробовать вместо CAST что-то другое.

Причем на локальной машине тоже самое.
(версия MySQL на локалке - 5.1.54-community, на хостинге - 5.0.26)



28.08.2011 dude125
на кавычки в названиях полей и таблиц не обращайте внимания, в запросе они есть, я просто сюда забыл везде добавить, сорри



28.08.2011 dude125
извините ввел в заблуждение, на локалке все работает нормально, т.е. получается что проблема в версии MySQL?

Тогда вопрос, как увеличить размер строки? или альтернативным запросом в соответствии с версией MySQL?



30.08.2011 Миха
dude125, в этой статье написано...
....
Ограничение 1024.
У этой функции есть ограничение на объем выводимых данных.
По умолчанию 1024 символа для каждого объединения - для каждой выводимой строки.
Если размер склееных данных больше, то он будет урезаться.
Чтобы расширить размер нужно выполнить команду SET group_concat_max_len =4096;
Если у вас есть привелегии, то вы расширите объем получаемых данных до 4096, можно и больше.
Но чаще всего на обычных хостингах таких привелегий нет.
....

У вас наверное стоит ограничение в 64 байта.



01.09.2011 Миха
Да, вы правы. На хостинге значение этой переменной было вообще 64)). Привилегий на изменение нет. Просил службу поддержки исправить хотя бы до 1024. Сейчас все отлично работает. Спасибо за комментарий.



16.11.2011 Сергей
Только текст.
Следущая особенность GROUP_CONCAT это работа только со строками.

проверил на версиях:
5.0.92
5.1.47
5.5.16
с числами работает так же как и со строками
Hosted by uCoz
193.109.247.229