SQL split values to multiple rows

前端 未结 9 1416
刺人心
刺人心 2020-11-21 05:28

I have table :

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

i want output like this :

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


        
相关标签:
9条回答
  • 2020-11-21 05:35

    My variant: stored procedure that takes table name, field names and delimiter as arguments. Inspired by post 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 ;
    

    Usage example (normalization):

    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;
    
    0 讨论(0)
  • 2020-11-21 05:37

    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 ;
    
    0 讨论(0)
  • 2020-11-21 05:40

    Here is my attempt: The first select presents the csv field to the split. Using recursive CTE, we can create a list of numbers that are limited to the number of terms in the csv field. The number of terms is just the difference in the length of the csv field and itself with all the delimiters removed. Then joining with this numbers, substring_index extracts that term.

    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 讨论(0)
  • 2020-11-21 05:42

    If the name column were a JSON array (like '["a","b","c"]'), then you could extract/unpack it with JSON_TABLE() (available since MySQL 8.0.4):

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

    Result:

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

    View on DB Fiddle

    If you store the values in a simple CSV format, then you would first need to convert it to JSON:

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

    Result:

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

    View on DB Fiddle

    0 讨论(0)
  • 2020-11-21 05:43

    If you can create a numbers table, that contains numbers from 1 to the maximum fields to split, you could use a solution like this:

    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
    

    Please see fiddle here.

    If you cannot create a table, then a solution can be this:

    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
    

    an example fiddle is here.

    0 讨论(0)
  • 2020-11-21 05:43

    Here is my solution

    -- 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 讨论(0)
提交回复
热议问题