SQL-Aufteilungswerte in mehrere Zeilen

78

Ich habe Tisch:

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

Ich möchte eine Ausgabe wie folgt:

id | name    
1  | a    
1  | b    
1  | c    
2  | b
AFD
quelle
5
Es wird allgemein als schlechte Praxis angesehen, mehrere Werte in derselben Datenbankspalte zu speichern. Diese bewährte Methode - Normalisierung - sorgt im Allgemeinen dafür, dass Ihre Datenbank in Zukunft besser funktioniert. Es wird hier erklärt (oder unter "Normalisierung" nachgelesen
Graham Griffiths
4
@GrahamGriffiths: Ich würde dir zustimmen, zumindest sagt dies akademisches Wissen. In meinem Unternehmen gibt es jedoch viele Fälle, in denen diese Art von Dingen (Speichern mehrerer Werte in einer durch Trennzeichen getrennten Zeichenfolge) in einer einzigen Spalte ausgeführt werden, und sie behaupten, dass dies effizienter ist (ohne Verknüpfung und die erforderliche Verarbeitung) ist nicht teuer). Ich weiß ehrlich gesagt nicht, welcher Punkt bevorzugt werden sollte.
Veverke
1
Dies tritt auch auf, wenn Sie Raw-Json in einem JSON-Datentyp speichern. Eine normalisierte Struktur ist besser, hat aber auch den Nachteil, dass mehr Vorabentwickler benötigt werden. Sie kann leicht brechen, wenn sich die Antworten ändern. Sie müssen sich neu entwickeln, wenn Sie sich entscheiden, das zu ändern, was Sie vom JSON erwarten.
Chris Strickland

Antworten:

125

Wenn Sie eine Zahlentabelle erstellen können, die Zahlen von 1 bis zu den maximal zu teilenden Feldern enthält, können Sie eine Lösung wie die folgende verwenden:

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

Bitte sehen Sie Geige hier .

Wenn Sie keine Tabelle erstellen können, kann dies folgende Lösung sein:

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

Ein Beispiel für eine Geige ist hier .

fthiella
quelle
15
@ user2577038 Sie könnten es ohne eine Zahlentabelle tun, sehen Sie es hier sqlfiddle.com/#!2/a213e4/1
fthiella
1
Im zweiten Beispiel ist zu beachten, dass die maximale Anzahl von durch Komma getrennten "Feldern" 5 beträgt. Sie können die Anzahl der Vorkommen in einer Zeichenfolge mithilfe einer Methode wie der folgenden überprüfen : stackoverflow.com/questions/12344795/ … . Fügen Sie der Inline-Ansicht "Zahlen" so lange Klauseln "select [number] union all" hinzu, bis die Anzahl der zurückgegebenen Zeilen nicht mehr zunimmt.
Bret Weinraub
1
Wie immer stolpere ich immer wieder über Ihren nützlichen Code. Wenn jemand die schnelle Art und Weise will eine Tabelle ähnlich der oben Brocken erstellen hier gezeigt, hier ist ein Link , um diese Routine hier . Diese Operation war für eine einzelne Zeichenfolge und nicht für eine Tabelle.
Drew
Wie würde eine SQLite-Version davon aussehen? Ich erhalte die folgende Fehlermeldung:could not prepare statement (1 no such function: SUBSTRING_INDEX)
Remi Sture
Schöne Lösung. Was aber, wenn zwei Spalten geteilt werden müssen, ID-Name Name1 und Werte 1 | a, b, c | x, y, z @fthiella
syncdm2012
7

Wenn die nameSpalte ein JSON-Array (wie '["a","b","c"]') wäre, könnten Sie sie mit JSON_TABLE () extrahieren / entpacken (verfügbar seit MySQL 8.0.4):

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

Ergebnis:

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

Blick auf DB Fiddle

Wenn Sie die Werte in einem einfachen CSV-Format speichern, müssen Sie sie zuerst in JSON konvertieren:

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

Ergebnis:

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

Blick auf DB Fiddle

Paul Spiegel
quelle
Ich bekomme diesen Fehler in DataGrip mit MySQL 5.7.17, irgendwelche Ideen? Ich habe auch versucht, den identischen Code aus der DB Fiddle wörtlich zu kopieren, der dort aber nicht lokal ausgeführt wird. [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
Ian Nastajus
Verdächtige müssen auf 8.x upgraden.
Ian Nastajus
1
@IanNastajus - Ja, Sie benötigen mindestens MySQL 8.0.4
Paul Spiegel
... und bestätigt. Ja, das Aktualisieren einer Datenbank kann so problematisch sein. die 8.x Installateur wollte nur Teile auf die neueste 5.7.y aktualisieren, so erkannte ich das Installationsprogramm erfüllen muss ich würde deinstallieren dann zuerst 5.x Neuinstallation mit der exakt gleichen 8.x Installer ... Yeesh: Auge -roll: ... zum Glück hat es gut funktioniert, und dies war nur für meine eigenen Nebenprojekte und fungierte in diesem Fall nicht als vollständiger DBA für ein großes Produktionssystem ...
Ian Nastajus
6

Ich habe die Referenz von hier mit geändertem Spaltennamen genommen.

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 ;
Prahalad Gaggar
quelle
4

Meine Variante: gespeicherte Prozedur, die Tabellennamen, Feldnamen und Trennzeichen als Argumente verwendet. Inspiriert von der 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 ;

Anwendungsbeispiel (Normalisierung):

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;
Andrey
quelle
Wunderschön geschrieben. Mit ein paar Änderungen konnte ich dies in meine Datenbank aufnehmen, um sicherzustellen, dass es in der 1. normalen Form vorliegt. Vielen Dank.
Raviabhiram
3

Hier ist mein Versuch: Die erste Auswahl präsentiert das CSV-Feld dem Split. Mit rekursivem CTE können wir eine Liste von Zahlen erstellen, die auf die Anzahl der Begriffe im CSV-Feld beschränkt sind. Die Anzahl der Terme ist nur der Unterschied in der Länge des CSV-Felds und selbst, wobei alle Trennzeichen entfernt wurden. Wenn Sie dann diese Zahlen verbinden, extrahiert substring_index diesen Begriff.

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
Harry Marx
quelle
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
Imanez
quelle
1

Die ursprüngliche Frage betraf MySQL und SQL im Allgemeinen. Das folgende Beispiel gilt für die neuen Versionen von MySQL. Leider ist eine generische Abfrage, die auf jedem SQL Server funktionieren würde, nicht möglich. Einige Server unterstützen CTE nicht, andere haben keinen substring_index, andere verfügen über integrierte Funktionen zum Aufteilen einer Zeichenfolge in mehrere Zeilen.

--- die Antwort folgt ---

Rekursive Abfragen sind praktisch, wenn der Server keine integrierten Funktionen bietet. Sie können auch der Engpass sein.

Die folgende Abfrage wurde unter MySQL Version 8.0.16 geschrieben und getestet. Es wird nicht auf Version 5.7- funktionieren. Die alten Versionen unterstützen keinen Common Table Expression (CTE) und damit rekursive Abfragen.

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;
user9526573
quelle
Obwohl diese Lösung funktioniert, select count(1) from tablenamehängen nachfolgende Abfragen (dh ) entweder an oder dauern unglaublich lange. Ich muss die MySQL-Workbench schließen und erneut öffnen, damit nachfolgende Abfragen nicht mehr hängen bleiben. Außerdem wollte ich diese Lösung verwenden, um das Ergebnis in eine neue Tabelle einzufügen. Diese Lösung funktioniert jedoch nicht, wenn Sie NULL-Werte für Ihre durch Kommas getrennten Werte haben. Ich würde immer noch die von @fthiella bereitgestellte Lösung verwenden, bin aber trotzdem froh, diese Lösung gefunden zu haben.
Kimbaudi
Übrigens habe ich diese Abfrage mit MySQL 8.0.16 in einer Tabelle mit fast 6.000.000 Datensätzen ausgeführt.
Kimbaudi
0

Beste Übung. Ergebnis:

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

Erstellen Sie zunächst eine Zahlentabelle:

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   |

Zweitens teilen Sie einfach die 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   |
qupc
quelle
-1

Hier ist meine Lösung

-- 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
Tawonga Donnell Msiska
quelle