2016-05-18 4 views
3

Ich versuche, eine Tabelle zu normalisieren, die ein früherer Entwickler entworfen, um eine Spalte mit ID-Nummern, die mit anderen Zeilen in der gleichen Tabelle verknüpfen ID.MySQL - Wie normalize Spalte mit Delimiter-separated IDs

Kunden Tabelle

id | aliases (VARCHAR) 
---------------------------- 
1  | |4|58|76 
2  |  
3  | 
4  | |1|58|76 
... |  
58 | |1|4|76 
... | 
76 | |1|4|58 

So Kunde 1, 4, 58 und 76 sind alle "Aliase" voneinander. Kunden 2 und 3 haben keine Aliase, daher enthält das Feld eine leere Zeichenfolge.

Ich möchte auf das gesamte "Alias" -System verzichten und die Daten normalisieren, damit ich diese anderen Kunden alle auf den einen Datensatz abbilden kann. Also möchte ich verwandte Tabelle Daten für Kunden 1, 4, 58 und 76 alle nur auf Kunden 1 zugeordnet werden.

Ich dachte, ich würde eine neue Tabelle füllen, die später ich dann beitreten und Aktualisierungen auf anderen Tabellen durchführen kann.

Join-Tabelle

id | customer_id | alias_id 
------------------------------- 
1 | 1   | 4 
2 | 1   | 58 
3 | 1   | 76 

Wie kann ich die Daten aus dieser ersten Tabelle, in das obige Format erhalten? Wenn dies ein absoluter Albtraum in reinem SQL wird, schreibe ich einfach ein PHP-Skript, das versucht, dies zu tun und die Daten einzufügen.

+0

Sie eine temporäre Tabelle verwenden können, durch Komma getrennte Zeichenfolge in Zeilen wie unten –

Antwort

3

Als ich anfing, diese Frage zu beantworten, dachte ich, es wäre schnell und einfach, weil ich einmal etwas sehr ähnliches in SQL Server getan habe, aber das Konzept der Übersetzung in dieser vollen Lösung aufgegriffen.

Ein Vorbehalt, der aus Ihrer Frage nicht klar war, ist, ob Sie eine Bedingung für die Deklaration der primären ID gegenüber der Alias-ID haben. Diese Lösung ermöglicht beispielsweise 1 einen Alias ​​von 4 sowie 4 einen Alias ​​von 1, der mit den bereitgestellten Daten in Ihrer vereinfachten Beispielfrage konsistent ist.

die Daten für dieses Beispiel Zur Einstellung habe ich diese Struktur:

CREATE TABLE notnormal_customers (
    id INT NOT NULL PRIMARY KEY, 
    aliases VARCHAR(10) 
); 

INSERT INTO notnormal_customers (id,aliases) 
VALUES 
(1,'|4|58|76'), 
(2,''), 
(3,''), 
(4,'|1|58|76'), 
(58,'|1|4|76'), 
(76,'|1|4|58'); 

Erstens, um die Eins-zu-viele-Beziehung zu einem Kunden zu viel Aliase darzustellen, habe ich diese Tabelle :

CREATE FUNCTION SPLIT_STR(
    x VARCHAR(255), 
    delim VARCHAR(12), 
    pos INT 
) 
RETURNS VARCHAR(255) 
RETURN REPLACE(SUBSTRING(SUBSTRING_INDEX(x, delim, pos), 
     LENGTH(SUBSTRING_INDEX(x, delim, pos -1)) + 1), 
     delim, ''); 

Dann werden wir:

CREATE TABLE customer_aliases (
    primary_id INT NOT NULL, 
    alias_id INT NOT NULL, 
    FOREIGN KEY (primary_id) REFERENCES notnormal_customers(id), 
    FOREIGN KEY (alias_id) REFERENCES notnormal_customers(id), 
    /* clustered primary key prevents duplicates */ 
    PRIMARY KEY (primary_id,alias_id) 
) 

Am wichtigsten ist, werden wir eine custom SPLIT_STR function verwenden Erstellen Sie eine gespeicherte Prozedur, um die gesamte Arbeit auszuführen. Code ist mit Kommentaren zu Quellreferenzen versehen.

DELIMITER $$ 
CREATE PROCEDURE normalize_customers() 
BEGIN 

    DECLARE cust_id INT DEFAULT 0; 
    DECLARE al_id INT UNSIGNED DEFAULT 0; 
    DECLARE alias_str VARCHAR(10) DEFAULT ''; 
    /* set the value of the string delimiter */ 
    DECLARE string_delim CHAR(1) DEFAULT '|'; 
    DECLARE count_aliases INT DEFAULT 0; 
    DECLARE i INT DEFAULT 1; 

    /* 
    use cursor to iterate through all customer records 
    http://burnignorance.com/mysql-tips/how-to-loop-through-a-result-set-in-mysql-strored-procedure/ 
    */ 
    DECLARE done INT DEFAULT 0; 
    DECLARE cur CURSOR FOR 
     SELECT `id`, `aliases` 
     FROM `notnormal_customers`; 
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; 

    OPEN cur; 
    read_loop: LOOP 

    /* 
     Fetch one record from CURSOR and set to customer id and alias string. 
     If not found then `done` will be set to 1 by continue handler. 
    */ 
    FETCH cur INTO cust_id, alias_str; 
    IF done THEN 
     /* If done set to 1 then exit the loop, else continue. */ 
     LEAVE read_loop; 
    END IF; 

    /* skip to next record if no aliases */ 
    IF alias_str = '' THEN 
     ITERATE read_loop; 
    END IF; 

    /* 
     get number of aliases 
     https://pisceansheart.wordpress.com/2008/04/15/count-occurrence-of-character-in-a-string-using-mysql/ 
    */ 
    SET count_aliases = LENGTH(alias_str) - LENGTH(REPLACE(alias_str, string_delim, '')); 

    /* strip off the first pipe to make it compatible with our SPLIT_STR function */ 
    SET alias_str = SUBSTR(alias_str, 2); 

    /* 
     iterate and get each alias from custom split string function 
     https://stackoverflow.com/questions/18304857/split-delimited-string-value-into-rows 
    */ 
    WHILE i <= count_aliases DO 

     /* get the next alias id */ 
     SET al_id = CAST(SPLIT_STR(alias_str, string_delim, i) AS UNSIGNED); 
     /* REPLACE existing values instead of insert to prevent errors on primary key */ 
     REPLACE INTO customer_aliases (primary_id,alias_id) VALUES (cust_id,al_id); 
     SET i = i+1; 

    END WHILE; 
    SET i = 1; 

    END LOOP; 
    CLOSE cur; 

END$$ 
DELIMITER ; 

Schließlich können Sie es einfach laufen durch den Aufruf:

CALL normalize_customers(); 

Dann können Sie die Daten in der Konsole überprüfen:

mysql> select * from customer_aliases; 
+------------+----------+ 
| primary_id | alias_id | 
+------------+----------+ 
|   4 |  1 | 
|   58 |  1 | 
|   76 |  1 | 
|   1 |  4 | 
|   58 |  4 | 
|   76 |  4 | 
|   1 |  58 | 
|   4 |  58 | 
|   76 |  58 | 
|   1 |  76 | 
|   4 |  76 | 
|   58 |  76 | 
+------------+----------+ 
12 rows in set (0.00 sec) 
1

Update 2 (One-Abfrage-Lösung)

Unter der Annahme, dass die Aliase Liste immer sortiert, können Sie das Ergebnis mit nur einer Abfrage erreichen:

CREATE TABLE aliases (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 
    customer_id INT UNSIGNED NOT NULL, 
    alias_id INT UNSIGNED NOT NULL 
) AS 
    SELECT NULL AS id, c1.id AS customer_id, c2.id AS alias_id 
    FROM customers c1 
    JOIN customers c2 
    ON c2.aliases LIKE CONCAT('|', c1.id , '|%') -- c1.id is the first alias of c2.id 
    WHERE c1.id < (SUBSTRING(c1.aliases,2)+0) -- c1.id is smaller than the first alias of c2.id 

Es wird auch viel schneller sein, wenn die aliases Spalte indiziert ist, so dass die JOIN wird von einem Bereichssuche unterstützt werden .

sqlfiddle

Ursprüngliche Antwort

Wenn Sie die Rohre mit Komma ersetzen, können Sie die FIND_IN_SET-Funktion verwenden.

Ich würde eine temporäre Tabelle zunächst erstellen (muss nicht technicaly vorübergehend sein) zu speichern, durch Komma getrennte Alias ​​Listen:

CREATE TABLE tmp (`id` int, `aliases` varchar(50)); 
INSERT INTO tmp(`id`, `aliases`) 
    SELECT id, REPLACE(aliases, '|', ',') AS aliases 
    FROM customers; 

Dann füllen Sie Ihre normalisierte Tabelle mit FIND_IN_SET in der JOIN ON-Klausel:

CREATE TABLE aliases (`id` int, `customer_id` int, `alias_id` int) AS 
    SELECT t.id as customer_id, c.id AS alias_id 
    FROM tmp t 
    JOIN customers c ON find_in_set(c.id, t.aliases); 

bei Bedarf - löschen Duplikate mit höherer customer_id (nur halten niedrigsten):

DELETE FROM aliases 
WHERE customer_id IN (SELECT * FROM(
    SELECT DISTINCT a1.customer_id 
    FROM aliases a1 
    JOIN aliases a2 
    ON a2.customer_id = a1.alias_id 
    AND a1.customer_id = a2.alias_id 
    AND a1.customer_id > a1.alias_id 
)derived); 

Bei Bedarf - erstellen AUTO_INCREMENT-ID:

ALTER TABLE aliases ADD column id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; 

Die aliases Tabelle nun so aussehen:

| id | customer_id | alias_id | 
|----|-------------|----------| 
| 1 |   1 |  4 | 
| 2 |   1 |  58 | 
| 3 |   1 |  76 | 

sqlfiddle

nicht korrekten Indizes definieren Vergessen.

aktualisieren 1

Sie überspringen können eine temporäre Tabelle erstellen, und füllen Sie die aliases Tabelle mit LIKE statt FIND_IN_SET:

CREATE TABLE aliases (`customer_id` int, `alias_id` int) AS 
    SELECT c2.id as customer_id, c1.id AS alias_id 
    FROM customers c1 
    JOIN customers c2 
    ON CONCAT(c1.aliases, '|') LIKE CONCAT('%|', c2.id , '|%'); 

sqlfiddle

+0

„immer geordnet“ zu halten - was bedeutet das!?!? – Strawberry

+0

@Strawberry - ich meine die Liste in der Zeichenfolge. "| 1 | 4 | 76 |" ist sortiert. "| 4 | 76 | 1 |" ist nicht sortiert. Für die letzte Abfrage muss der kleinste Alias ​​immer zuerst kommen. –

+0

Ich mag die Kürze Ihrer Antwort, aber ich kann es nicht als Lösung umschließen. Zum Beispiel erstellt Ihre Antwort eine neue Tabelle und füllt sie auf, aber nur mit den Aliasen für die erste Zeile. Haben Sie eine Empfehlung, dies zu erweitern, um tatsächlich den ganzen Tisch zu machen? –

2

eine Tabelle von Zahlen verwenden (0- 9) - obwohl Sie das gleiche mit (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3...etc.) erreichen können ...

SELECT DISTINCT id old_id /* the technique below inevitably creates duplicates. */ 
          /* DISTINCT discards them. */ 
       , SUBSTRING_INDEX(
        SUBSTRING_INDEX(SUBSTR(aliases,2),'|',i+1) /* isolate text between */ 
         ,'|',-1) x       /* each pipe and the next */ 
      FROM customers 
       , ints  /* do this for the first 10 pipes in each string */ 
      ORDER 
      BY id,x+0 /* implicit CASTING */ 
+--------+------+ 
| old_id | x | 
+--------+------+ 
|  1 | 4 | 
|  1 | 58 | 
|  1 | 76 | 
|  2 | NULL | 
|  3 | NULL | 
|  4 | 1 | 
|  4 | 58 | 
|  4 | 76 | 
|  58 | 1 | 
|  58 | 4 | 
|  58 | 76 | 
|  76 | 1 | 
|  76 | 4 | 
|  76 | 58 | 
+--------+------+ 

(Edit: In Zeile hinzugefügt comments)

+0

wow das ist erstaunlich. Könntest du bitte etwas besser erklären warum/wie es funktioniert? –

+1

@JeffPuckettII In Zeile Kommentare hinzugefügt – Strawberry

Verwandte Themen