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

我必须创build一个学生完成报告。 每个学生都属于一个客户。 这里是表格(这个问题简化了)。

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字段中的课程名称的索引 。 因此,如果客户的课程名称是“AB01,AB02,AB03”,并且注册的courseId2 ,则该学生在AB03中。

有没有一种方法可以在这些包含课程名称的表格上进行单一select? 请记住,会有来自不同客户的学生(因此有不同的课程名称,并非全部是连续的,例如:“NW01,NW03”)

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

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

到现在为止,我想保留这些逗号分隔的列表在我的SQL数据库 – 深知所有的警告!

我一直认为它们比查找表(它提供了一个规范化的数据库的方法)有好处。 经过几天的拒绝, 我看到了光

  • 在一个字段中使用逗号分隔值时,使用查找表不会导致比那些难看的string操作更多的代码。
  • 查找表允许本地数字格式,因此不会比那些csv字段大。 虽然是小的。
  • 所涉及的string操作在高级语言代码(SQL和PHP)中很苗条,但与使用整数数组相比,代价较高。
  • 数据库并不是人类可读的,因为它的可读性/直接可编辑性,试图坚持结构是愚蠢的,就像我一样。

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

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

对于包含所有逗号分隔值的table中的列column

 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存储在您的数据库中; 这个function是为了将CSV转换成合理的数据库结构,而不是在代码中的任何地方使用。 如果您必须在生产中使用它,请重新考虑您的数据库结构

你可以为此创build一个函数:

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

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

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

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

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

贪婪的问号有助于searchstring的开始或结尾。

希望对未来的人有所帮助

基于Alwin Kesler的解决scheme,这里有一个更实际的现实世界的例子。

假设逗号分隔列表位于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, ',?'); 

可以在MySQL SELECT语句中分解一个string。

首先产生一系列的数字,直到你想要爆炸的最大分隔值。 无论是从一个整数表,或通过工会号码在一起。 以下生成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 

正如你所看到的,这里有一个小问题,最后的分隔值重复了很多次。 要摆脱这一点,你需要根据有多less个分隔符来限制数字的范围。 这可以通过获取分隔字段的长度并将分隔字段的长度与分隔符的长度比较来完成(将其删除)。 从这里你可以得到分隔符的数量:

 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 

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

MySQL唯一的string分割函数是SUBSTRING_INDEX(str, delim, count) 。 你可以使用这个,例如:

  • 返回string中的第一个分隔符之前的项目:

     mysql> SELECT SUBSTRING_INDEX('foo#bar#baz#qux', '#', 1); +--------------------------------------------+ | SUBSTRING_INDEX('foo#bar#baz#qux', '#', 1) | +--------------------------------------------+ | foo | +--------------------------------------------+ 1 row in set (0.00 sec) 
  • 返回string中最后一个分隔符后的项目:

     mysql> SELECT SUBSTRING_INDEX('foo#bar#baz#qux', '#', -1); +---------------------------------------------+ | SUBSTRING_INDEX('foo#bar#baz#qux', '#', -1) | +---------------------------------------------+ | qux | +---------------------------------------------+ 1 row in set (0.00 sec) 
  • 返回string中第三个分隔符之前的所有内容:

     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) 
  • 通过链接两个调用返回string中的第二个项目:

     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) 

一般来说,获得#分隔string的第n个元素(假设你知道它至less有n个元素)的一个简单方法是:

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

内部的SUBSTRING_INDEX调用放弃了第n个分隔符和其后的所有内容,然后外部的SUBSTRING_INDEX调用放弃除了最后一个元素之外的所有内容。

如果你想要一个更强大的解决scheme,如果你要求一个不存在的元素(例如,询问'a#b#c#d'的第五个元素),返回NULL ,那么你可以使用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) ); 

然后你可以使用这样的function:

 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) 

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

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

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

表3:clientid,courseid

然后做一个join,你是为了比赛。

 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 

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

 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' 

我使用了上面的逻辑,但稍微修改了一下。 我的input格式是:“apple:100 | pinapple:200 | orange:300”存储在variables中@updtAdvanceKeyVal

这里是function块:

 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; 

我刚刚遇到类似的问题,就像我用另一种方法解决的问题。 我的用例是需要在逗号分隔列表中使用这些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, "\"%")

这基本上只是看看你试图join的表中的id是否出现在集合中,并且在那一点上你可以很容易地join并返回你的logging。 你也可以创build一个像这样的视图。

对于我在使用Wordpress插件来处理关系的情况下,它的运行效果很好。 这些报价真的有帮助,因为否则你会冒着部分匹配的风险(也就是18以内的id 1等)。

这里是你如何做SQL Server的。 别人可以把它翻译成MySQL。 将CSV值parsing为多行 。

 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来find正确的单词,位置。

这是我到目前为止(在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`) 

那么,我没有用过,所以我决定创build一个真正简单的拆分function,希望它可以帮助:

  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;