SQL 将值拆分为多行

SQL split values to multiple rows

提问人:AFD 提问时间:7/30/2013 最后编辑:dreftymacAFD 更新时间:1/13/2023 访问量:289208

问:

我有表格:

id | name    
1  | a,b,c    
2  | b

我想要这样的输出:

id | name    
1  | a    
1  | b    
1  | c    
2  | b
MySQL SQL 分隔符 CSV

评论

4赞 Veverke 1/11/2017
@GrahamGriffiths:我同意你的看法,至少学术知识是这样说的。但是,在我的公司中,在很多情况下,他们在单个列中执行此类操作(以分隔字符串方式存储多个值),并且他们声称它更有效(无连接,并且所需的处理成本不高)。老实说,我不知道应该首选哪一点。
1赞 Chris Strickland 6/19/2019
如果将原始 json 存储在 JSON 数据类型中,也会遇到这种情况。规范化结构更好,但它也有需要更多前期开发的缺点,并且如果响应发生变化,很容易中断,如果你决定改变你想要的 json,你必须重新开发。
1赞 Michael Peterson 2/5/2021
@GrahamGriffiths 请坚持回答问题,而不是质疑需求。如果您要提供不相关的信息,请至少先回答问题。

答:

9赞 Prahalad Gaggar 7/30/2013 #1

我从这里获取了更改列名的参考。

DELIMITER $$

CREATE FUNCTION strSplit(x VARCHAR(65000), delim VARCHAR(12), pos INTEGER) 
RETURNS VARCHAR(65000)
BEGIN
  DECLARE output VARCHAR(65000);
  SET output = REPLACE(SUBSTRING(SUBSTRING_INDEX(x, delim, pos)
                 , LENGTH(SUBSTRING_INDEX(x, delim, pos - 1)) + 1)
                 , delim
                 , '');
  IF output = '' THEN SET output = null; END IF;
  RETURN output;
END $$


CREATE PROCEDURE BadTableToGoodTable()
BEGIN
  DECLARE i INTEGER;

  SET i = 1;
  REPEAT
    INSERT INTO GoodTable (id, name)
      SELECT id, strSplit(name, ',', i) FROM BadTable
      WHERE strSplit(name, ',', i) IS NOT NULL;
    SET i = i + 1;
    UNTIL ROW_COUNT() = 0
  END REPEAT;
END $$

DELIMITER ;
177赞 fthiella 7/30/2013 #2

如果可以创建一个数字表,其中包含从 1 到要拆分的最大字段的数字,则可以使用如下解决方案:

select
  tablename.id,
  SUBSTRING_INDEX(SUBSTRING_INDEX(tablename.name, ',', numbers.n), ',', -1) name
from
  numbers inner join tablename
  on CHAR_LENGTH(tablename.name)
     -CHAR_LENGTH(REPLACE(tablename.name, ',', ''))>=numbers.n-1
order by
  id, n

看小提琴 这里.

如果无法创建表,则解决方案可以是这样的:

select
  tablename.id,
  SUBSTRING_INDEX(SUBSTRING_INDEX(tablename.name, ',', numbers.n), ',', -1) name
from
  (select 1 n union all
   select 2 union all select 3 union all
   select 4 union all select 5) numbers INNER JOIN tablename
  on CHAR_LENGTH(tablename.name)
     -CHAR_LENGTH(REPLACE(tablename.name, ',', ''))>=numbers.n-1
order by
  id, n

这里有一个小提琴的例子。

评论

20赞 fthiella 7/30/2013
@user2577038您可以在没有数字表的情况下做到这一点,请在此处查看 sqlfiddle.com/#!2/a213e4/1
3赞 Bret Weinraub 7/28/2016
需要注意的重要一点是,在第二个示例中,用逗号分隔的“字段”的最大数量为 5。您可以通过以下方法检查字符串中出现的 # 个:stackoverflow.com/questions/12344795/...。继续向“数字”内联视图添加“select [number] union all”子句,直到返回的行数停止增加。
1赞 Drew 9/26/2016
像往常一样,我总是偶然发现你有用的代码。如果有人想要快速创建类似于此处所示的顶部块的表,请在此处使用此例程的链接。该操作是针对单个字符串的,而不是它们的表。
1赞 Remi Sture 11/22/2016
这个SQLite版本会是什么样子?我收到以下错误:could not prepare statement (1 no such function: SUBSTRING_INDEX)
0赞 syncdm2012 5/3/2018
不错的解决方案。但是,如果要拆分两列,ID 名称 name1 和值 1|甲,乙,丙 |x,y,z @fthiella
1赞 Imanez 10/15/2016 #3
CREATE PROCEDURE `getVal`()
BEGIN
        declare r_len integer;
        declare r_id integer;
        declare r_val varchar(20);
        declare i integer;
        DECLARE found_row int(10);
        DECLARE row CURSOR FOR select length(replace(val,"|","")),id,val from split;
        create table x(id int,name varchar(20));
      open row;
            select FOUND_ROWS() into found_row ;
            read_loop: LOOP
                IF found_row = 0 THEN
                         LEAVE read_loop;
                END IF;
            set i = 1;  
            FETCH row INTO r_len,r_id,r_val;
            label1: LOOP        
                IF i <= r_len THEN
                  insert into x values( r_id,SUBSTRING(replace(r_val,"|",""),i,1));
                  SET i = i + 1;
                  ITERATE label1;
                END IF;
                LEAVE label1;
            END LOOP label1;
            set found_row = found_row - 1;
            END LOOP;
        close row;
        select * from x;
        drop table x;
END
6赞 Andrey 2/5/2017 #4

我的变体:将表名、字段名和分隔符作为参数的存储过程。灵感来自后 http://www.marcogoncalves.com/2011/03/mysql-split-column-string-into-rows/

delimiter $$

DROP PROCEDURE IF EXISTS split_value_into_multiple_rows $$
CREATE PROCEDURE split_value_into_multiple_rows(tablename VARCHAR(20),
    id_column VARCHAR(20), value_column VARCHAR(20), delim CHAR(1))
  BEGIN
    DECLARE id INT DEFAULT 0;
    DECLARE value VARCHAR(255);
    DECLARE occurrences INT DEFAULT 0;
    DECLARE i INT DEFAULT 0;
    DECLARE splitted_value VARCHAR(255);
    DECLARE done INT DEFAULT 0;
    DECLARE cur CURSOR FOR SELECT tmp_table1.id, tmp_table1.value FROM 
        tmp_table1 WHERE tmp_table1.value IS NOT NULL AND tmp_table1.value != '';
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

    SET @expr = CONCAT('CREATE TEMPORARY TABLE tmp_table1 (id INT NOT NULL, value VARCHAR(255)) ENGINE=Memory SELECT ',
        id_column,' id, ', value_column,' value FROM ',tablename);
    PREPARE stmt FROM @expr;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;

    DROP TEMPORARY TABLE IF EXISTS tmp_table2;
    CREATE TEMPORARY TABLE tmp_table2 (id INT NOT NULL, value VARCHAR(255) NOT NULL) ENGINE=Memory;

    OPEN cur;
      read_loop: LOOP
        FETCH cur INTO id, value;
        IF done THEN
          LEAVE read_loop;
        END IF;

        SET occurrences = (SELECT CHAR_LENGTH(value) -
                           CHAR_LENGTH(REPLACE(value, delim, '')) + 1);
        SET i=1;
        WHILE i <= occurrences DO
          SET splitted_value = (SELECT TRIM(SUBSTRING_INDEX(
              SUBSTRING_INDEX(value, delim, i), delim, -1)));
          INSERT INTO tmp_table2 VALUES (id, splitted_value);
          SET i = i + 1;
        END WHILE;
      END LOOP;

      SELECT * FROM tmp_table2;
    CLOSE cur;
    DROP TEMPORARY TABLE tmp_table1;
  END; $$

delimiter ;

使用示例(规范化):

CALL split_value_into_multiple_rows('my_contacts', 'contact_id', 'interests', ',');

CREATE TABLE interests (
  interest_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  interest VARCHAR(30) NOT NULL
) SELECT DISTINCT value interest FROM tmp_table2;

CREATE TABLE contact_interest (
  contact_id INT NOT NULL,
  interest_id INT NOT NULL,
  CONSTRAINT fk_contact_interest_my_contacts_contact_id FOREIGN KEY (contact_id) REFERENCES my_contacts (contact_id),
  CONSTRAINT fk_contact_interest_interests_interest_id FOREIGN KEY (interest_id) REFERENCES interests (interest_id)
) SELECT my_contacts.contact_id, interests.interest_id
    FROM my_contacts, tmp_table2, interests
    WHERE my_contacts.contact_id = tmp_table2.id AND interests.interest = tmp_table2.value;

评论

1赞 raviabhiram 2/22/2019
写得很漂亮。通过一些更改,我能够将其合并到我的数据库中,以确保它处于第一正常形式。谢谢。
0赞 Tawonga Donnell Msiska 11/15/2018 #5

这是我的解决方案

-- Create the maximum number of words we want to pick (indexes in n)
with recursive n(i) as (
    select
        1 i
    union all
    select i+1 from n where i < 1000
)
select distinct
    s.id,
    s.oaddress,
    -- n.i,
    -- use the index to pick the nth word, the last words will always repeat. Remove the duplicates with distinct
    if(instr(reverse(trim(substring_index(s.oaddress,' ',n.i))),' ') > 0,
        reverse(substr(reverse(trim(substring_index(s.oaddress,' ',n.i))),1,
            instr(reverse(trim(substring_index(s.oaddress,' ',n.i))),' '))),
        trim(substring_index(s.oaddress,' ',n.i))) oth
from 
    app_schools s,
    n

评论

0赞 Kermit 4/14/2022
MySQL中没有CTE
0赞 Lexius 1/26/2023
自 2018 年发布 8.0 版以来,MySQL 中有 CTE
1赞 user9526573 5/30/2019 #6

最初的问题适用于MySQL和SQL。以下示例适用于新版本的 MySQL。遗憾的是,在任何 SQL Server 上都无法进行泛型查询。有些服务器不支持 CTE,有些服务器没有 substring_index,但有些服务器具有将字符串拆分为多行的内置函数。

---答案如下---

当服务器不提供内置功能时,递归查询很方便。它们也可能是瓶颈。

以下查询是在 MySQL 版本 8.0.16 上编写和测试的。它不适用于 5.7- 版本。旧版本不支持公用表表达式 (CTE),因此不支持递归查询。

with recursive
  input as (
        select 1 as id, 'a,b,c' as names
      union
        select 2, 'b'
    ),
  recurs as (
        select id, 1 as pos, names as remain, substring_index( names, ',', 1 ) as name
          from input
      union all
        select id, pos + 1, substring( remain, char_length( name ) + 2 ),
            substring_index( substring( remain, char_length( name ) + 2 ), ',', 1 )
          from recurs
          where char_length( remain ) > char_length( name )
    )
select id, name
  from recurs
  order by id, pos;

评论

0赞 kimbaudi 7/7/2019
尽管此解决方案有效,但它会使任何后续查询(即 )挂起或花费非常长的时间。我必须关闭mysql工作台并重新打开,以便后续查询不再挂起。此外,我想使用此解决方案将结果插入到新表中。但是,如果逗号分隔值的 NULL 值为 NULL 值,则此解决方案将不起作用。我仍然会使用@fthiella提供的解决方案,但仍然很高兴找到这个解决方案。select count(1) from tablename
0赞 kimbaudi 7/7/2019
顺便说一句,我使用 MySQL 8.0.16 在一个包含近 6,000,000 条记录的表上运行了这个查询。
10赞 Harry Marx 8/1/2019 #7

这是我的尝试: 第一个选择将 csv 字段显示给拆分。 使用递归 CTE,我们可以创建一个数字列表,这些数字仅限于 csv 字段中的项数。 项数只是删除所有分隔符后 csv 字段和其本身长度的差异。 然后结合这个数字,substring_index提取该术语。

with recursive
    T as ( select 'a,b,c,d,e,f' as items),
    N as ( select 1 as n union select n + 1 from N, T
        where n <= length(items) - length(replace(items, ',', '')))
    select distinct substring_index(substring_index(items, ',', n), ',', -1)
group_name from N, T

评论

0赞 Frank 9/2/2021
不要使用 union,union 将是 DISTINCT 值。UNION ALL会更好
0赞 Kermit 4/14/2022
MySQL中没有CTE
3赞 Faizan Akram Dar 8/8/2022
@Kermit MySQL 从 7 年开始支持 CTE。
35赞 Paul Spiegel 12/6/2019 #8

如果该列是JSON数组(如),则可以使用JSON_TABLE()(从MySQL 8.0.4开始可用)提取/解压缩它:name'["a","b","c"]'

select t.id, j.name
from mytable t
join json_table(
  t.name,
  '$[*]' columns (name varchar(50) path '$')
) j;

结果:

| id  | name |
| --- | ---- |
| 1   | a    |
| 1   | b    |
| 1   | c    |
| 2   | b    |

在 DB Fiddle 上查看

如果以简单的 CSV 格式存储值,则首先需要将其转换为 JSON:

select t.id, j.name
from mytable t
join json_table(
  replace(json_array(t.name), ',', '","'),
  '$[*]' columns (name varchar(50) path '$')
) j

结果:

| id  | name |
| --- | ---- |
| 1   | a    |
| 1   | b    |
| 1   | c    |
| 2   | b    |

在 DB Fiddle 上查看

评论

1赞 Ian Nastajus 3/8/2020
我在 MySQL 5.7.17 的 DataGrip 中收到此错误,有什么想法吗?我还尝试逐字复制粘贴 DB Fiddle 中的相同代码,该代码在那里执行,但不在本地执行。[42000][1064] You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '( concat('[', replace(json_quote(t.name), ',', '","'), ']'), '$[*]' column' at line 3
0赞 Ian Nastajus 3/8/2020
嫌疑人需要升级到 8.x。
1赞 Paul Spiegel 3/8/2020
@IanNastajus - 是的,您至少需要 MySQL 8.0.4
0赞 Ian Nastajus 3/10/2020
...并确认。是的,升级数据库可能很麻烦。8.x 安装程序只想将部分升级到最新的 5.7.y,所以我意识到要满足安装程序的要求,我必须先卸载 5.x,然后使用完全相同的 8.x 安装程序重新安装......Yeesh :翻白眼:......值得庆幸的是,它运行良好,这只是我自己的副业项目,在这种情况下,它并没有充当大型生产系统的完整 DBA......
0赞 qupc 8/18/2020 #9

最佳实践。 结果:

SELECT
SUBSTRING_INDEX(SUBSTRING_INDEX('ab,bc,cd',',',help_id+1),',',-1) AS oid
FROM
(
SELECT @xi:=@xi+1 as help_id from 
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) xc1,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) xc2,
(SELECT @xi:=-1) xc0
) a
WHERE 
help_id < LENGTH('ab,bc,cd')-LENGTH(REPLACE('ab,bc,cd',',',''))+1

首先,创建一个数字表:

SELECT @xi:=@xi+1 as help_id from 
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) xc1,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) xc2,
(SELECT @xi:=-1) xc0;
| help_id  |
| --- |
| 0   |
| 1   |
| 2   |
| 3   |
| ...   |
| 24   |

其次,只需拆分 str:

SELECT SUBSTRING_INDEX(SUBSTRING_INDEX('ab,bc,cd',',',help_id+1),',',-1) AS oid
FROM
numbers_table
WHERE
help_id < LENGTH('ab,bc,cd')-LENGTH(REPLACE('ab,bc,cd',',',''))+1
| oid  |
| --- |
| ab   |
| bc   |
| cd   |
-5赞 Trace Ashley 8/20/2021 #10
SELECT id, unnest(string_to_array(name, ',')) AS names
FROM datatable

希望这对:D有所帮助

评论

3赞 forpas 8/20/2021
MySql 中没有 unnest() 和 string_to_array()。
3赞 Leon Straathof 7/5/2022 #11

因为您必须在上面的示例中继续添加“select number union all”,如果您需要大量拆分,这可能是一个问题。

    select
  tablename.id,
  SUBSTRING_INDEX(SUBSTRING_INDEX(tablename.name, ',', numbers.n), ',', -1) name
from
  (select 1 n union all
   select 2 union all select 3 union all
   select 4 union all select 5) numbers INNER JOIN tablename
  on CHAR_LENGTH(tablename.name)
     -CHAR_LENGTH(REPLACE(tablename.name, ',', ''))>=numbers.n-1
order by
  id, n

我决定一个更好的方法是,只为每个数字添加一个数字行。下面的示例适用于1-1000,添加另一行使其适用于1-10000,依此类推。

    select
  tablename.id,
  SUBSTRING_INDEX(SUBSTRING_INDEX(tablename.name, ',', numbers.n), ',', -1) name
from(SELECT @row := @row + 1 AS n FROM 
(select 0 union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as t,
(select 0 union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as t2,
(select 0 union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as t3,  
(SELECT @row:=0) as numbers)as numbers INNER JOIN tablename
  on CHAR_LENGTH(tablename.name)
     -CHAR_LENGTH(REPLACE(tablename.name, ',', ''))>=numbers.n-1
order by
  id, n
2赞 Mamed Shahmaliyev 1/13/2023 #12

这是另一个技巧。数字 20 是逗号分隔列表中的最大值数。

我们使用单个查询,没有程序。

如果 tbl 的行数超过单个逗号分隔列表中的最大值数,则可以从查询中删除“inner join tbl a inner join tbl c”部分。我添加这个是因为只有 2 行。

CREATE TABLE tbl(id int NOT NULL,name varchar(50),PRIMARY KEY (`id`));
insert into tbl values(1, 'a,b,c'), (2, 'd');
select id ,SUBSTRING_INDEX(SUBSTRING_INDEX(name, ',', k.n), ',', -1) as name
from tbl
INNER JOIN (
    SELECT * 
      FROM (
         SELECT @n:=@n+1 AS n
           FROM tbl inner join tbl a inner join tbl c
           INNER JOIN (SELECT @n:=0) AS _a
      ) AS _a WHERE _a.n <= 20
)AS k ON k.n <= LENGTH(name) - LENGTH(replace(name, ',','')) + 1
order by id

这是在逗号分隔列表中提取第 n 个值的技巧:

SUBSTRING_INDEX(SUBSTRING_INDEX(name, ',', k.n), ',', -1)