Можете ли вы разделить / взорвать поле в запросе MySQL? - PullRequest
42 голосов
/ 23 января 2009

Мне нужно составить отчет о некоторых студенческих работах. Каждый из студентов принадлежит одному клиенту. Вот таблицы (упрощенно для этого вопроса).

CREATE TABLE  `clients` (
  `clientId` int(10) unsigned NOT NULL auto_increment,
  `clientName` varchar(100) NOT NULL default '',
  `courseNames` varchar(255) NOT NULL default ''
)

Поле courseNames содержит разделенную запятыми строку названий курсов, например, "AB01, AB02, AB03"

CREATE TABLE  `clientenrols` (
  `clientEnrolId` int(10) unsigned NOT NULL auto_increment,
  `studentId` int(10) unsigned NOT NULL default '0',
  `courseId` tinyint(3) unsigned NOT NULL default '0'
)

Поле courseId здесь - индекс названия курса в поле clients.courseNames . Таким образом, если courseNames клиента - "AB01, AB02, AB03", а courseId зачисления - 2, то студент находится в AB03.

Есть ли способ, которым я могу сделать один выбор для этих таблиц, который включает название курса? Помните, что будут студенты из разных клиентов (и, следовательно, у них будут разные названия курсов, не все из которых являются последовательными, например: «NW01, NW03»)

В принципе, если бы я мог разделить это поле и вернуть один элемент из полученного массива, это было бы тем, что я искал. Вот что я имею в виду в магическом псевдокоде:

SELECT e.`studentId`, SPLIT(",", c.`courseNames`)[e.`courseId`]
FROM ...

Ответы [ 16 ]

30 голосов
/ 21 декабря 2011

До сих пор я хотел хранить эти разделенные запятыми списки в своей базе данных SQL - хорошо помня обо всех предупреждениях!

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

  • Использование таблиц поиска НЕ ​​приводит к большему количеству кода, чем эти уродливые строковые операции, при использовании значений через запятую в одном поле.
  • Таблица поиска допускает форматы собственных чисел и, следовательно, НЕ больше, чем эти поля CSV. Это меньше, хотя.
  • В строковом коде высокого уровня (SQL и PHP) задействованные строковые операции невелики, но дороги по сравнению с использованием массивов целых чисел.
  • Базы данных не предназначены для чтения человеком, и в большинстве случаев глупо пытаться придерживаться структур из-за их читабельности / прямого редактирования, как я сделал.

Короче говоря, есть причина, по которой в MySQL нет встроенной функции SPLIT ().

22 голосов
/ 11 июня 2013

Видя, что это довольно популярный вопрос - ответ ДА.

Для столбца column в таблице table, содержащего все ваши значения, разделенные запятой:

CREATE TEMPORARY TABLE temp (val CHAR(255));
SET @S1 = CONCAT("INSERT INTO temp (val) VALUES ('",REPLACE((SELECT GROUP_CONCAT( DISTINCT  `column`) AS data FROM `table`), ",", "'),('"),"');");
PREPARE stmt1 FROM @s1;
EXECUTE stmt1;
SELECT DISTINCT(val) FROM temp;

Пожалуйста, помните, однако, что не храните CSV в вашей БД


Per @Mark Amery - поскольку это переводит значения, разделенные запятой, в оператор INSERT, будьте осторожны при выполнении его на неанимированных данных


Просто повторим, пожалуйста, не храните CSV в своей БД; эта функция предназначена для перевода CSV в разумную структуру БД и не должна использоваться нигде в вашем коде. Если вам нужно использовать его в производстве, переосмыслите структуру вашей БД

12 голосов
/ 31 декабря 2012

Вы можете создать функцию для этого:

/**
* Split a string by string (Similar to the php function explode())
*
* @param VARCHAR(12) delim The boundary string (delimiter).
* @param VARCHAR(255) str The input string.
* @param INT pos The index of the string to return
* @return VARCHAR(255) The (pos)th substring
* @return VARCHAR(255) Returns the [pos]th string created by splitting the str parameter on boundaries formed by the delimiter.
* @{@example
*     SELECT SPLIT_STRING('|', 'one|two|three|four', 1);
*     This query
* }
*/
DROP FUNCTION IF EXISTS SPLIT_STRING;
CREATE FUNCTION SPLIT_STRING(delim VARCHAR(12), str VARCHAR(255), pos INT)
RETURNS VARCHAR(255) DETERMINISTIC
RETURN
    REPLACE(
        SUBSTRING(
            SUBSTRING_INDEX(str, delim, pos),
            LENGTH(SUBSTRING_INDEX(str, delim, pos-1)) + 1
        ),
        delim, ''
    );

Преобразовав волшебный псевдокод, чтобы использовать это, вы получите:

SELECT e.`studentId`, SPLIT_STRING(',', c.`courseNames`, e.`courseId`)
FROM...
7 голосов
/ 01 апреля 2017

Единственная функция разделения строк в MySQL - SUBSTRING_INDEX(str, delim, count). Вы можете использовать это, например, для:

  • Вернуть элемент перед первым разделителем в строке:

    mysql> SELECT SUBSTRING_INDEX('foo#bar#baz#qux', '#', 1);
    +--------------------------------------------+
    | SUBSTRING_INDEX('foo#bar#baz#qux', '#', 1) |
    +--------------------------------------------+
    | foo                                        |
    +--------------------------------------------+
    1 row in set (0.00 sec)
    
  • Возвращает элемент после последнего разделителя в строке:

    mysql> SELECT SUBSTRING_INDEX('foo#bar#baz#qux', '#', -1);
    +---------------------------------------------+
    | SUBSTRING_INDEX('foo#bar#baz#qux', '#', -1) |
    +---------------------------------------------+
    | qux                                         |
    +---------------------------------------------+
    1 row in set (0.00 sec)
    
  • Вернуть все до третьего разделителя в строке:

    mysql> SELECT SUBSTRING_INDEX('foo#bar#baz#qux', '#', 3);
    +--------------------------------------------+
    | SUBSTRING_INDEX('foo#bar#baz#qux', '#', 3) |
    +--------------------------------------------+
    | foo#bar#baz                                |
    +--------------------------------------------+
    1 row in set (0.00 sec)
    
  • Вернуть второй элемент в строке путем объединения двух вызовов:

    mysql> SELECT SUBSTRING_INDEX(SUBSTRING_INDEX('foo#bar#baz#qux', '#', 2), '#', -1);
    +----------------------------------------------------------------------+
    | SUBSTRING_INDEX(SUBSTRING_INDEX('foo#bar#baz#qux', '#', 2), '#', -1) |
    +----------------------------------------------------------------------+
    | bar                                                                  |
    +----------------------------------------------------------------------+
    1 row in set (0.00 sec)
    

В общем, простой способ получить n-й элемент из # -разделенной строки (при условии, что вы точно знаете, что она содержит хотя бы n элементов):

SUBSTRING_INDEX(SUBSTRING_INDEX(your_string, '#', n), '#', -1);

Внутренний вызов SUBSTRING_INDEX отбрасывает n-й разделитель и все после него, а затем внешний вызов SUBSTRING_INDEX отбрасывает все, кроме последнего элемента, который остается.

Если вам нужно более надежное решение, которое возвращает NULL, если вы запрашиваете элемент, который не существует (например, запрашивает 5-й элемент 'a#b#c#d'), тогда вы можете считать разделители используя REPLACE и затем условно вернуть NULL используя IF():

IF(
    LENGTH(your_string) - LENGTH(REPLACE(your_string, '#', '')) / LENGTH('#') < n - 1,
    NULL,
    SUBSTRING_INDEX(SUBSTRING_INDEX(your_string, '#', n), '#', -1)
)

Конечно, это довольно уродливо и трудно понять! Так что вы можете захотеть обернуть его в функцию:

CREATE FUNCTION split(string TEXT, delimiter TEXT, n INT)
RETURNS TEXT DETERMINISTIC
RETURN IF(
    (LENGTH(string) - LENGTH(REPLACE(string, delimiter, ''))) / LENGTH(delimiter) < n - 1,
    NULL,
    SUBSTRING_INDEX(SUBSTRING_INDEX(string, delimiter, n), delimiter, -1)
);

Затем вы можете использовать такую ​​функцию:

mysql> SELECT SPLIT('foo,bar,baz,qux', ',', 3);
+----------------------------------+
| SPLIT('foo,bar,baz,qux', ',', 3) |
+----------------------------------+
| baz                              |
+----------------------------------+
1 row in set (0.00 sec)

mysql> SELECT SPLIT('foo,bar,baz,qux', ',', 5);
+----------------------------------+
| SPLIT('foo,bar,baz,qux', ',', 5) |
+----------------------------------+
| NULL                             |
+----------------------------------+
1 row in set (0.00 sec)

mysql> SELECT SPLIT('foo###bar###baz###qux', '###', 2);
+------------------------------------------+
| SPLIT('foo###bar###baz###qux', '###', 2) |
+------------------------------------------+
| bar                                      |
+------------------------------------------+
1 row in set (0.00 sec)
7 голосов
/ 26 марта 2014

Основано на ответе Алекса выше (https://stackoverflow.com/a/11022431/1466341) Я нашел еще лучшее решение. Решение, которое не содержит точного идентификатора одной записи.

Предполагая, что разделенный запятыми список находится в таблице data.list и содержит список кодов из другой таблицы classification.code, вы можете сделать что-то вроде:

SELECT 
    d.id, d.list, c.code
FROM 
    classification c
    JOIN data d
        ON d.list REGEXP CONCAT('[[:<:]]', c.code, '[[:>:]]');

Итак, если у вас есть такие таблицы и данные:

CLASSIFICATION (code varchar(4) unique): ('A'), ('B'), ('C'), ('D')
MY_DATA (id int, list varchar(255)): (100, 'C,A,B'), (150, 'B,A,D'), (200,'B')

выше SELECT вернет

(100, 'C,A,B', 'A'),
(100, 'C,A,B', 'B'),
(100, 'C,A,B', 'C'),
(150, 'B,A,D', 'A'),
(150, 'B,A,D', 'B'),
(150, 'B,A,D', 'D'),
(200, 'B', 'B'),
4 голосов
/ 13 июня 2012

Опираясь на решение Алвина Кеслера, приведу более практичный пример из реального мира.

Предполагая, что разделенный запятыми список находится в my_table.list, и это список идентификаторов для my_other_table.id, вы можете сделать что-то вроде:

SELECT 
    * 
FROM 
    my_other_table 
WHERE 
    (SELECT list FROM my_table WHERE id = '1234') REGEXP CONCAT(',?', my_other_table.id, ',?');
4 голосов
/ 08 июня 2012

Я решил эту проблему с помощью шаблона регулярного выражения. Они, как правило, медленнее, чем обычные запросы, но это простой способ получить данные в столбце запроса, разделенном запятыми

SELECT * 
FROM `TABLE`
WHERE `field` REGEXP ',?[SEARCHED-VALUE],?';

жадный вопросительный знак помогает искать в начале или в конце строки.

Надеюсь, что это поможет любому в будущем

3 голосов
/ 15 сентября 2015

Возможно взорвать строку в операторе MySQL SELECT.

Сначала сгенерируйте серию чисел до наибольшего числа значений с разделителями, которые вы хотите взорвать. Либо из таблицы целых чисел, либо путем объединения чисел вместе. Следующее генерирует 100 строк, дающих значения от 1 до 100. Его можно легко расширить, чтобы получить большие диапазоны (добавьте еще один подзапрос, дающий значения от 0 до 9 для сотен - следовательно, от 0 до 999 и т. Д.).

SELECT 1 + units.i + tens.i * 10 AS aNum
FROM (SELECT 0 AS i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) units
CROSS JOIN (SELECT 0 AS i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) tens

Это может быть перекрестно соединено с вашей таблицей, чтобы дать вам значения. Обратите внимание, что вы используете SUBSTRING_INDEX, чтобы получить значение с разделителями до определенного значения, а затем используете SUBSTRING_INDEX, чтобы получить это значение, исключая предыдущие.

SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(clients.courseNames, ',', sub0.aNum), ',', -1) AS a_course_name
FROM clients
CROSS JOIN
(
    SELECT 1 + units.i + tens.i * 10 AS aNum, units.i + tens.i * 10 AS aSubscript
    FROM (SELECT 0 AS i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) units
    CROSS JOIN (SELECT 0 AS i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) tens
) sub0

Как видите, здесь есть небольшая проблема, заключающаяся в том, что последнее значение с разделителями повторяется много раз. Чтобы избавиться от этого, вам нужно ограничить диапазон чисел в зависимости от количества разделителей. Это можно сделать, взяв длину поля с разделителями и сравнив его с длиной поля с разделителями, в котором разделители были изменены на '' (чтобы удалить их). Отсюда вы можете получить количество разделителей: -

SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(clients.courseNames, ',', sub0.aNum), ',', -1) AS a_course_name
FROM clients
INNER JOIN
(
    SELECT 1 + units.i + tens.i * 10 AS aNum
    FROM (SELECT 0 AS i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) units
    CROSS JOIN (SELECT 0 AS i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) tens
) sub0
ON (1 + LENGTH(clients.courseNames) - LENGTH(REPLACE(clients.courseNames, ',', ''))) >= sub0.aNum

В исходном поле для примера вы можете (например) подсчитать количество студентов на каждом курсе, основываясь на этом. Обратите внимание, что я изменил подзапрос, который получает диапазон чисел, чтобы вернуть 2 числа, 1 используется для определения названия курса (поскольку они основаны на 1), а другой получает нижний индекс (так как они основаны на начале в 0).

SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(clients.courseNames, ',', sub0.aNum), ',', -1) AS a_course_name, COUNT(clientenrols.studentId)
FROM clients
INNER JOIN
(
    SELECT 1 + units.i + tens.i * 10 AS aNum, units.i + tens.i * 10 AS aSubscript
    FROM (SELECT 0 AS i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) units
    CROSS JOIN (SELECT 0 AS i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) tens
) sub0
ON (1 + LENGTH(clients.courseNames) - LENGTH(REPLACE(clients.courseNames, ',', ''))) >= sub0.aNum
LEFT OUTER JOIN clientenrols
ON clientenrols.courseId = sub0.aSubscript
GROUP BY a_course_name

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

2 голосов
/ 14 апреля 2015

Если вам нужно получить таблицу из строки с разделителями:

SET @str = 'function1;function2;function3;function4;aaa;bbbb;nnnnn';
SET @delimeter = ';';
SET @sql_statement = CONCAT('SELECT '''
                ,REPLACE(@str, @delimeter, ''' UNION ALL SELECT ''')
                ,'''');
SELECT @sql_statement;
SELECT 'function1' UNION ALL SELECT 'function2' UNION ALL SELECT 'function3' UNION ALL SELECT 'function4' UNION ALL SELECT 'aaa' UNION ALL SELECT 'bbbb' UNION ALL SELECT 'nnnnn'
2 голосов
/ 07 марта 2012
SELECT
  tab1.std_name, tab1.stdCode, tab1.payment,
  SUBSTRING_INDEX(tab1.payment, '|', 1) as rupees,
  SUBSTRING(tab1.payment, LENGTH(SUBSTRING_INDEX(tab1.payment, '|', 1)) + 2,LENGTH(SUBSTRING_INDEX(tab1.payment, '|', 2))) as date
FROM (
  SELECT DISTINCT
    si.std_name, hfc.stdCode,
    if(isnull(hfc.payDate), concat(hfc.coutionMoneyIn,'|', year(hfc.startDtae), '-',  monthname(hfc.startDtae)), concat(hfc.payMoney, '|', monthname(hfc.payDate), '-', year(hfc.payDate))) AS payment
  FROM hostelfeescollection hfc
  INNER JOIN hostelfeecollectmode hfm ON hfc.tranId = hfm.tranId
  INNER JOIN student_info_1 si ON si.std_code = hfc.stdCode
  WHERE hfc.tranId = 'TRAN-AZZZY69454'
) AS tab1
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...