SQL 분할 값을 여러 행으로


79

나는 테이블이있다 :

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

다음과 같은 출력을 원합니다.

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

5
일반적으로 동일한 데이터베이스 열에 여러 값을 저장하는 것은 나쁜 습관으로 간주됩니다. 이 모범 사례 (정규화)는 일반적으로 나중에 데이터베이스가 더 잘 작동하도록합니다. 여기에 설명되어 있습니다 (또는 '정규화'에 대해 읽어보십시오) : stackoverflow.com/questions/2331838/…
Graham Griffiths

4
@GrahamGriffiths : 적어도 이것은 학문적 지식이 말하는 것입니다. 그러나 우리 회사에는 단일 열에서 이러한 종류의 작업 (구분 된 문자열 방식으로 여러 값 저장)을 수행하는 경우가 많이 있으며, 그들의 주장은 더 효율적이라는 것입니다 (조인이 필요없고 처리가 필요하다는 것입니다). 비용이 많이 들지 않습니다). 솔직히 어느 점을 선호해야할지 모르겠습니다.
Veverke

1
JSON 데이터 유형에 원시 json을 저장하는 경우에도이 문제가 발생합니다. 정규화 된 구조가 더 좋지만 더 많은 선행 개발이 필요하다는 단점이 있으며 응답이 변경되면 중단되기 쉬우 며 json에서 원하는 것을 변경하기로 결정하면 재개발해야합니다.
Chris Strickland

답변:


128

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

여기에 예제 바이올린이 있습니다 .


15
@ user2577038 당신은 여기를 참조 번호 테이블없이 그것을 할 수 sqlfiddle.com/#!2/a213e4/1
fthiella

1
두 번째 예에서 쉼표로 구분 된 최대 "필드"수는 5입니다. 다음과 같은 방법을 통해 문자열에서 발생 횟수를 확인할 수 있습니다. stackoverflow.com/questions/12344795/ … . 반환 된 행 수가 증가하지 않을 때까지 'numbers'인라인 뷰에 'select [number] union all'절을 계속 추가하십시오.
Bret Weinraub

1
평소처럼 유용한 코드에 계속 걸려 넘어집니다. 여기에 표시된 맨 위 청크와 유사한 테이블을 빠르게 만드는 방법을 원하는 사람이 있다면 여기에이 루틴을 사용하는 링크가 있습니다 . 그 작업은 테이블이 아닌 단일 문자열에 대한 것입니다.
드류

이것의 SQLite 버전은 어떻게 생겼습니까? 다음과 같은 오류가 발생합니다.could not prepare statement (1 no such function: SUBSTRING_INDEX)
Remi Sture

좋은 솔루션입니다. 그러나 분할 할 두 개의 열, ID 이름 name1 및 값 1 | a, b, c | X, Y, Z의 @fthiella
syncdm2012

8

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

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에서보기


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
이안 Nastajus

8.x로 업그레이드해야합니다.
이안 Nastajus

1
@IanNastajus은 - 예, 적어도 MySQL은 8.0.4 필요
폴 스피겔

... 확인했습니다. 예, 데이터베이스 업그레이드는 매우 번거로울 수 있습니다. 8.x 설치 프로그램은 부품을 최신 5.7.y로 업그레이드하려고했기 때문에 먼저 5.x 를 제거한 다음 정확히 동일한 8.x 설치 프로그램으로 다시 설치 해야합니다. 굴러라 : ... 감사하게도 그냥 괜찮 았는데, 이것은 단지 내 자신의 사이드 프로젝트이며,이 경우 대규모 생산 시스템에 대한 전체 DBA ... 역할을하지 않은
이안 Nastajus

6

여기에서 열 이름이 변경된 참조를 가져 왔습니다.

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

내 변형 : 테이블 이름, 필드 이름 및 구분 기호를 인수로 사용하는 저장 프로 시저. 게시물 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;

아름답게 쓰여졌습니다. 몇 가지 변경을 통해이를 데이터베이스에 통합하여 첫 번째 정상 형식이되도록 할 수있었습니다. 감사합니다.
raviabhiram

3

내 시도는 다음과 같습니다. 첫 번째 선택은 분할에 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

1
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

1

원래 질문은 일반적으로 MySQL과 SQL이었습니다. 아래 예는 새 버전의 MySQL에 대한 것입니다. 불행히도 모든 SQL 서버에서 작동하는 일반 쿼리는 불가능합니다. 일부 서버는 CTE를 지원하지 않고 다른 서버에는 substring_index가 없지만 다른 서버에는 문자열을 여러 행으로 분할하는 내장 함수가 있습니다.

--- 대답은 다음과 같습니다 ---

재귀 쿼리는 서버가 기본 제공 기능을 제공하지 않을 때 편리합니다. 병목 현상이 될 수도 있습니다.

다음 쿼리는 MySQL 버전 8.0.16에서 작성 및 테스트되었습니다. 버전 5.7-에서는 작동하지 않습니다. 이전 버전은 CTE (Common Table Expression) 및 재귀 쿼리를 지원하지 않습니다.

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에서 제공하는 솔루션을 계속 사용할 것이지만이 솔루션을 발견하게되어 기쁩니다.
kimbaudi

btw, 거의 6,000,000 개의 레코드가있는 테이블에서 MySQL 8.0.16을 사용하여이 쿼리를 실행했습니다.
kimbaudi

0

모범 사례. 결과:

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   |

-1

여기 내 해결책이 있습니다.

-- 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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.