你可以拆分/爆炸MySQL查询中的字段吗?

时间:2009-01-23 04:53:26

标签: mysql

我必须创建一些关于某些学生完成情况的报告。每个学生都属于一个客户。以下是表格(此问题已简化)。

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”,并且注册的courseId2,那么该学生就在AB03中。

有没有办法可以在这些包含课程名称的表上进行单一选择?请记住,将有来自不同客户的学生(因此有不同的课程名称,并非所有课程名称都是连续的,例如:“NW01,NW03”)

基本上,如果我可以拆分该字段并从结果数组中返回单个元素,那将是我正在寻找的。这就是我在魔法伪代码中的意思:

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

18 个答案:

答案 0 :(得分:30)

到现在为止,我想在我的SQL数据库中保留这些以逗号分隔的列表 - 充分了解所有警告!

我一直认为它们比查找表(提供标准化数据库的方法)有好处。经过几天的拒绝,我见过光

  • 在一个字段中使用逗号分隔值时,使用查找表不会导致比那些丑陋的字符串操作更多的代码。
  • 查找表允许本机数字格式,因此不大于那些csv字段。但它更小。
  • 涉及的字符串操作在高级语言代码(SQL和PHP)中很小,但与使用整数数组相比价格昂贵。
  • 数据库并不意味着人类可读,并且由于其可读性/直接可编辑性而试图坚持结构是非常愚蠢的,就像我一样。

简而言之,MySQL中没有原生的SPLIT()函数是有原因的。

答案 1 :(得分:22)

看到这是一个相当受欢迎的问题 - 答案是肯定的。

对于包含所有逗号分隔值的表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转换为合理的数据库结构,而不是在代码中的任何位置使用。如果您必须在生产中使用它,请重新考虑您的数据库结构

答案 2 :(得分:12)

您可以为此创建一个功能:

/**
* 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...

答案 3 :(得分:7)

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',那么你可以count the delimiters using REPLACE然后使用IF()有条件地返回NULL

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)

答案 4 :(得分:7)

基于亚历克斯上面的答案(https://stackoverflow.com/a/11022431/1466341),我提出了更好的解决方案。解决方案不包含确切的一个记录ID。

假设逗号分隔列表在表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'),

答案 5 :(得分:4)

我用正则表达式模式解决了这种问题。它们往往比常规查询慢,但它是一种在逗号分隔的查询列中检索数据的简单方法

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

贪婪的问号有助于搜索字符串的开头或结尾。

希望将来对任何人都有帮助

答案 6 :(得分:4)

在Alwin Kesler的解决方案的基础上,这是一个更实际的现实世界的例子。

假设逗号分隔列表位于my_table.list中,并且它是my_other_table.id的ID列表,您可以执行以下操作:

SELECT 
    * 
FROM 
    my_other_table 
WHERE 
    (SELECT list FROM my_table WHERE id = '1234') REGEXP CONCAT(',?', my_other_table.id, ',?');

答案 7 :(得分:3)

可以在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

正如你所看到的,它可能但非常混乱。并且几乎没有机会使用索引,因此效率不高。此外,范围必须处理最大数量的分隔值,并通过排除大量重复项来工作;如果分隔值的最大数量非常大,那么这将大大减慢速度。总的来说,正确规范数据库通常要好得多。

答案 8 :(得分:2)

有一种更简单的方法,有一个链接表,即:

表1:客户,客户信息,等等等等。

表2:课程,课程信息,等等等等。

表3:clientid,courseid

然后做一个JOIN,然后你就参加了比赛。

答案 9 :(得分:2)

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

答案 10 :(得分:2)

如果你需要从带有分隔符的字符串中获取表:

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'

答案 11 :(得分:1)

我使用了上面的逻辑,但略微修改了它。 我的输入格式为: “apple:100 | pinapple:200 | orange:300”存储在变量@updtAdvanceKeyVal

这是功能块:

set @res = "";

set @i = 1;
set @updtAdvanceKeyVal = updtAdvanceKeyVal;

REPEAT


 -- set r =  replace(SUBSTRING(SUBSTRING_INDEX(@updtAdvanceKeyVal, "|", @i),
 --  LENGTH(SUBSTRING_INDEX(@updtAdvanceKeyVal, "|", @i -1)) + 1),"|","");

-- wrapping the function in "replace" function as above causes to cut off a character from
 -- the 2nd splitted value if the value is more than 3 characters. Writing it in 2 lines causes no such problem and the output is as expected
-- sample output by executing the above function :
-- orange:100
-- pi apple:200    !!!!!!!!strange output!!!!!!!!
-- tomato:500

      set @r =  SUBSTRING(SUBSTRING_INDEX(@updtAdvanceKeyVal, "|", @i),
                  LENGTH(SUBSTRING_INDEX(@updtAdvanceKeyVal, "|", @i -1)) + 1);

      set @r = replace(@r,"|","");

      if @r <> "" then

              set @key = SUBSTRING_INDEX(@r, ":",1);
              set @val = SUBSTRING_INDEX(@r, ":",-1);

              select @key, @val;
      end if;

      set @i = @i + 1;

     until @r = ""
END REPEAT;

答案 12 :(得分:1)

我刚刚遇到类似的问题,我用不同的方式解决了这个问题。我的用例需要将这些id放在以逗号分隔的列表中,以便在连接中使用。

我能够使用类似的方法解决它,但它变得更容易,因为除了逗号分隔符之外,还引用了ID,如下所示:

keys "1","2","6","12"

因此,我能够做一个喜欢

SELECT twwf.id, jtwi.id joined_id FROM table_with_weird_field twwf INNER JOIN join_table_with_ids jtwi ON twwf.delimited_field LIKE CONCAT("%\"", jtwi.id, "\"%")

这基本上只是查看您尝试加入的表中的ID是否出现在集合中,此时您可以轻松地加入它并返回您的记录。你也可以从这样的东西创建一个视图。

它适用于我的用例,我正在处理一个以所述方式管理关系的Wordpress插件。引号确实有帮助,因为否则你冒着部分匹配的风险(也就是 - 18中的id 1等)。

答案 13 :(得分:0)

在包含逗号分隔值的列中搜索

MySQL 有一个专用函数 FIND_IN_SET(),如果在包含逗号分隔值的字符串中找到该值,则该函数返回字段索引。

例如,以下语句返回字符串 C 中值 A,B,C,D 的从一开始的索引。

SELECT FIND_IN_SET('C', 'A,B,C,D') AS result;
+--------+
| result |
+--------+
|      3 |
+--------+

如果未找到给定值,FIND_IN_SET() 函数返回 0

SELECT FIND_IN_SET('Z', 'A,B,C,D') AS result;
+--------+
| result |
+--------+
|      0 |
+--------+

答案 14 :(得分:0)

您可以在最新的MySQL版本中使用JSON进行此操作。这是爆炸。我们将快速准备创建数字表。然后,首先创建一个中间表,以逗号分隔的字符串转换为json数组,然后使用json_extract将它们分开。我将字符串封装在引号中,仔细地转义了现有的引号,因为我用分号分隔了包含逗号的字符串。

因此,要创建数字表,希望您的客户比课程多,请选择一个足够大的表。

CREATE TABLE numbers (n int PRIMARY KEY);
INSERT INTO numbers 
SELECT @row := @row + 1
FROM clients JOIN (select @row:=0) t2;

如果知道只有50门课程,则增加LIMIT 50。现在,这很容易,不是吗?现在进入实际工作,说实话,引号使它更丑陋,但至少它更通用:

CREATE TABLE json_coursenames 
SELECT clientId,clientName,CONCAT('["', REPLACE(REPLACE(courseName,'"','\\"'), ',', '","'), '"]') AS a
FROM clients;

CREATE TABLE extracted
SELECT clientId,clientName,REPLACE(TRIM(TRIM('"' FROM JSON_EXTRACT(a, concat('$[', n, ']')))), '\\"', '"')
FROM json_coursenames
INNER JOIN numbers ON n < JSON_LENGTH(a);

哇!

这里有两种肉:CONCAT('["', REPLACE(coursename, ',', '","'), '"]')(我放下第二个REPLACE以使其更可见)会将foo,bar,bar转换为"foo","bar","baz"。另一个技巧是JSON_EXTRACT(a, concat('$[', n, ']')将变成JSON_EXTRACT(a, $[12]),这就是数组中的第13个元素,请参见JSON Path syntax

答案 15 :(得分:-1)

好吧,我没用过任何东西,所以我决定创建一个真正简单的分割功能,希望它有所帮助:

    DECLARE inipos INTEGER;
    DECLARE endpos INTEGER;
    DECLARE maxlen INTEGER;
    DECLARE item VARCHAR(100);
    DECLARE delim VARCHAR(1);

    SET delim = '|';
    SET inipos = 1;
    SET fullstr = CONCAT(fullstr, delim);
    SET maxlen = LENGTH(fullstr);

    REPEAT
        SET endpos = LOCATE(delim, fullstr, inipos);
        SET item =  SUBSTR(fullstr, inipos, endpos - inipos);

        IF item <> '' AND item IS NOT NULL THEN           
            USE_THE_ITEM_STRING;
        END IF;
        SET inipos = endpos + 1;
    UNTIL inipos >= maxlen END REPEAT;

答案 16 :(得分:-1)

这是我到目前为止所得到的内容(在Ben Alpert页面上找到):

SELECT REPLACE(
    SUBSTRING(
        SUBSTRING_INDEX(c.`courseNames`, ',', e.`courseId` + 1)
        , LENGTH(SUBSTRING_INDEX(c.`courseNames`, ',', e.`courseId`)
    ) + 1)
    , ','
    , ''
)
FROM `clients` c INNER JOIN `clientenrols` e USING (`clientId`)

答案 17 :(得分:-1)

以下是SQL Server的使用方法。其他人可以将其翻译成MySQL。 Parsing CSV Values Into Multiple Rows

SELECT Author, 
NullIf(SubString(',' + Phrase + ',' , ID , CharIndex(',' , ',' + Phrase + ',' , ID) - ID) , '') AS Word 
FROM Tally, Quotes 
WHERE ID <= Len(',' + Phrase + ',') AND SubString(',' + Phrase + ',' , ID - 1, 1) = ',' 
AND CharIndex(',' , ',' + Phrase + ',' , ID) - ID > 0

这个想法是交叉连接到预定义的表Tally,它包含整数1到8000(或任何足够大的数字)并运行SubString来找到正确的,单词,位置。