【问题标题】:SQL split values to multiple rowsSQL 将值拆分为多行
【发布时间】:2021-12-07 21:00:03
【问题描述】:

我有桌子:

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

我想要这样的输出:

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

【问题讨论】:

  • @GrahamGriffiths:我同意你的观点,至少这是学术知识所表明的。但是,在我的公司中有很多情况下他们在单个列中执行此类操作(以分隔的字符串方式存储多个值),并且他们声称它更有效(无连接,并且需要处理不贵)。老实说,我不知道应该首选哪一点。
  • 如果您将原始 json 存储在 JSON 数据类型中,您也会遇到这种情况。规范化结构更好,但它也有需要更多前期开发的缺点,并且如果响应发生变化,很容易被破坏,如果你决定从 json 中改变你想要的东西,你必须重新开发。
  • @GrahamGriffiths 请坚持回答问题而不是质疑需求。如果您要提供不相关的信息,请至少先回答问题。

标签: mysql sql delimiter csv


【解决方案1】:

如果您可以创建一个数字表,其中包含从 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

请看小提琴here

如果你不能创建表,那么解决方法可以是这样的:

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

一个示例小提琴是here

【讨论】:

  • @user2577038 你可以在没有数字表的情况下做到这一点,在这里查看sqlfiddle.com/#!2/a213e4/1
  • 需要注意的重要一点是,在第二个示例中,以逗号分隔的“字段”的最大数量为 5。您可以通过以下方法检查字符串中出现的次数: stackoverflow.com/questions/12344795/…。继续将 'select [number] union all' 子句添加到 'numbers' 内联视图,直到返回的行数停止增加。
  • 像往常一样,我一直在偶然发现你有用的代码。如果有人想要快速创建类似于此处显示的顶部块的表,这里是使用此例程here 的链接。该操作是针对单个字符串而不是它们的表。
  • 这个的 SQLite 版本会是什么样子?我收到以下错误:could not prepare statement (1 no such function: SUBSTRING_INDEX)
  • 不错的解决方案。但是如果有两列要拆分,ID 名称 name1 和值 1| 怎么办? a,b,c | x,y,z @fthiella
【解决方案2】:

如果 name 列是 JSON 数组(如 '["a","b","c"]'),那么您可以使用 JSON_TABLE() 提取/解压缩它(自 MySQL 8.0.4 起可用):

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    |

View on 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    |

View on DB Fiddle

【讨论】:

  • 我在 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
  • 怀疑需要升级到8.x。
  • @IanNastajus - 是的,你至少需要 MySQL 8.0.4
  • ...并确认。是的,升级数据库可能会很麻烦。 8.x 安装程序只是想将部件升级到最新的 5.7.y,所以我意识到要满足安装程序我必须先卸载 5.x 然后重新安装使用完全相同的 8.x 安装程序... 是的在这种情况下...
【解决方案3】:

I have take the reference from here with changed column name.

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 ;

【讨论】:

    【解决方案4】:

    这是我的尝试: 第一个选择将 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
    

    【讨论】:

    • 不要使用联合,联合将是 DISTINCT 值。 UNION ALL 会更好
    【解决方案5】:

    我的变体:以表名、字段名和分隔符作为参数的存储过程。灵感来自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;
    

    【讨论】:

    • 写得很漂亮。通过一些更改,我能够将其合并到我的数据库中,以确保它处于第一范式。谢谢。
    【解决方案6】:

    最初的问题是针对 MySQL 和 SQL 的。下面的示例适用于 MySQL 的新版本。不幸的是,不可能在任何 SQL 服务器上工作的通用查询。有些服务器不支持 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;
    

    【讨论】:

    • 虽然此解决方案有效,但它会使任何后续查询(即select count(1) from tablename)要么挂起,要么花费非常长的时间。我必须关闭 mysql 工作台并重新打开以使后续查询不再挂起。另外,我想使用此解决方案将结果插入到新表中。但是,如果您的逗号分隔值具有 NULL 值,则此解决方案将不起作用。我仍然会使用@fthiella 提供的解决方案,但仍然很高兴找到了这个解决方案。
    • 顺便说一句,我使用 MySQL 8.0.16 在具有近 6,000,000 条记录的表上运行此查询。
    【解决方案7】:
    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
    

    【讨论】:

      【解决方案8】:

      最佳实践。 结果:

      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   |
      

      【讨论】:

        【解决方案9】:

        这是我的解决方案

        -- 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
        

        【讨论】:

          【解决方案10】:
          SELECT id, unnest(string_to_array(name, ',')) AS names
          FROM datatable
          

          希望这会有所帮助:D

          【讨论】:

          • MySql中没有unnest()和string_to_array()。
          猜你喜欢
          • 1970-01-01
          • 2012-06-16
          • 2014-08-25
          • 2020-12-19
          • 2016-04-10
          • 2019-09-11
          • 1970-01-01
          相关资源
          最近更新 更多