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

nic*_*ckf 42 mysql

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

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

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'
)
Run Code Online (Sandbox Code Playgroud)

courseId此处的字段是clients.courseNames字段中课程名称的索引.因此,如果客户courseNames是"AB01,AB02,AB03",并且courseId注册的是2,则学生在AB03.

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

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

SELECT e.`studentId`, SPLIT(",", c.`courseNames`)[e.`courseId`]
FROM ...
Run Code Online (Sandbox Code Playgroud)

小智 30

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

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

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

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

  • 是的,好的,**但是**:_my_想要一个SPLIT()去聚合函数的目的是规范化当前未规范化的数据,以便将来可以将它们保持为标准化格式!如果您的评论是正确的,那么MySQL缺少此功能会阻碍我在规范数据方面的进展!如果MySQL提供**GROUP_CONCAT()**函数,而不是相应的反向:SPLIT()函数,这是非常奇怪和不对称的!看起来我将不得不与SUBSTRING_INDEX()一起破解"解决方案"! (3认同)

eit*_*hed 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;
Run Code Online (Sandbox Code Playgroud)

但请记住,不要在您的数据库中存储CSV


Per @Mark Amery - 因为这会将昏迷分隔的值转换为INSERT语句,所以在未经过数据处理的数据上运行它时要小心


重申一下,请不要将CSV存储在数据库中; 此功能旨在将CSV转换为合理的数据库结构,而不是在代码中的任何位置使用.如果您必须在生产中使用它,请重新考虑您的数据库结构

  • -1; 好主,这种疯狂是什么?如果`table`中的值包含引号,这可能会失败,可能会让您接受SQL注入. (2认同)

Jos*_*ius 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, ''
    );
Run Code Online (Sandbox Code Playgroud)

将魔法伪代码转换为使用此代码,您将拥有:

SELECT e.`studentId`, SPLIT_STRING(',', c.`courseNames`, e.`courseId`)
FROM...
Run Code Online (Sandbox Code Playgroud)

  • 请注意,上述代码不适用于 UTF-8 等多字节字符。我认为使用 CHAR_LENGTH 而不是 LENGTH 应该可以解决问题。 (3认同)

Dar*_*ide 7

根据上面的Alex回答(/sf/answers/771570201/),我提出了更好的解决方案.解决方案不包含确切的一个记录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, '[[:>:]]');
Run Code Online (Sandbox Code Playgroud)

所以如果你有这样的表和数据:

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')
Run Code Online (Sandbox Code Playgroud)

以上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'),
Run Code Online (Sandbox Code Playgroud)

  • 在MySQL中,如果它是逗号分隔的字段,那么使用FIND_IN_SET而不是连接的正则表达式可能会更有效. (3认同)

Mar*_*ery 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)
    
    Run Code Online (Sandbox Code Playgroud)
  • 在字符串中的最后一个分隔符后返回该项:

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

    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)
    
    Run Code Online (Sandbox Code Playgroud)
  • 通过链接两个调用返回字符串中的第二项:

    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)
    
    Run Code Online (Sandbox Code Playgroud)

通常,获取#分离字符串的第n个元素的一种简单方法(假设您确定它至少具有n个元素)是:

SUBSTRING_INDEX(SUBSTRING_INDEX(your_string, '#', n), '#', -1);
Run Code Online (Sandbox Code Playgroud)

内部SUBSTRING_INDEX调用将丢弃第n个分隔符及其后的所有内容,然后外部SUBSTRING_INDEX调用将丢弃除剩余的最终元素之外的所有内容.

如果你想要一个更健壮的解决方案,NULL如果你要求一个不存在的元素(例如,要求第五个元素'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)
)
Run Code Online (Sandbox Code Playgroud)

当然,这非常难看,难以理解!所以你可能想把它包装在一个函数中:

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)
);
Run Code Online (Sandbox Code Playgroud)

然后你可以使用这样的函数:

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)
Run Code Online (Sandbox Code Playgroud)


小智 5

基于 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, ',?');
Run Code Online (Sandbox Code Playgroud)