Значення 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/…
Грем Гріффітс

4
@GrahamGriffiths: Я б погодився з вами, принаймні про це говорять академічні знання. Однак у моїй компанії є багато випадків, коли вони роблять подібні речі (зберігаючи декілька значень із розділеним рядком) в одному стовпці, і їх твердження полягає в тому, що він є більш ефективним (без об'єднання та необхідної обробки не є дорогим). Я, чесно кажучи, не знаю, якому пункту слід віддавати перевагу.
Веверке,

1
Ви також натрапите на це, якщо зберігаєте необроблений json у типі даних JSON. Нормалізована структура є кращою, але вона також має той недолік, що їй потрібно більше попереднього розробника, і вона чутлива до злому, якщо відповіді змінюються, і вам доведеться переробити, якщо ви вирішите змінити те, що ви хочете від json.
Кріс Стрікленд

Відповіді:


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/ ... . Продовжуйте додавати речення "select [number] union all" до вбудованого подання "numbers", поки кількість повернутих рядків не припинить збільшуватися.
Bret Weinraub,

1
Як завжди, я постійно натрапляю на ваш корисний код. Якщо хтось хоче швидкий спосіб створити таблицю, подібну до верхньої частини, показаної тут, ось посилання, використовуючи цю процедуру тут . Це було для одного рядка, а не таблиці з них.
Дрю

Як би виглядала версія SQLite цього? Я отримую таку помилку:could not prepare statement (1 no such function: SUBSTRING_INDEX)
Remi Sture

Гарне рішення. Але що, якщо є два стовпці, які потрібно розділити, ім’я імені 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

Якщо ви зберігаєте значення у простому форматі 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


я отримую цю помилку в DataGrip з MySQL 5.7.17, будь-які ідеї? Я також спробував дослівно скопіювати ідентичний код із скрипта DB, який виконується там, але не локально. [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.
Ян Настажус

1
@IanNastajus - Так, вам потрібен принаймні MySQL 8.0.4
Пол Шпігель

... і підтверджено. Так, оновлення бази даних може бути такою проблемою. інсталятор 8.x просто хотів оновити деталі до останньої версії 5.7.y, тому я зрозумів, щоб задовольнити інсталятор, мені доведеться спочатку видалити 5.x, а потім перевстановити за допомогою того самого інсталятора 8.x ... yeesh: eye -roll: ... на щастя, це спрацювало чудово, і це
стосувалося

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- це не працюватиме. Старі версії не підтримують Common Table Expression (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, але все одно радий, що знайшов це рішення.
kimbaudi

До речі, я здійснив цей запит за допомогою MySQL 8.0.16 у таблиці з майже 6 000 000 записів.
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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.