Hvis user_resources
(t1) var en 'normaliseret tabel' med én række for hver user => resource
kombination, så ville forespørgslen for at få svaret være så simpel som bare at joining
bordene sammen.
Ak, den er denormalized
ved at have resources
kolonne som en:'liste over ressource-id' adskilt af et ';' tegn.
Hvis vi kunne konvertere 'ressourcer'-kolonnen til rækker, så forsvinder mange af vanskelighederne, efterhånden som tabellen bliver enkel.
Forespørgslen til at generere det output, der blev bedt om:
SELECT user_resource.user,
resource.data
FROM user_resource
JOIN integerseries AS isequence
ON isequence.id <= COUNT_IN_SET(user_resource.resources, ';') /* normalize */
JOIN resource
ON resource.id = VALUE_IN_SET(user_resource.resources, ';', isequence.id)
ORDER BY
user_resource.user, resource.data
Udgangen:
user data
---------- --------
sampleuser abcde
sampleuser azerty
sampleuser qwerty
stacky qwerty
testuser abcde
testuser azerty
Hvordan:
'Tricket' er at have en tabel, der indeholder tallene fra 1 til en eller anden grænse. Jeg kalder det integerseries
. Det kan bruges til at konvertere 'vandrette' ting såsom:';' delimited strings
i rows
.
Måden dette fungerer på er, når du 'join' med integerseries
, laver du en cross join
, hvilket er det, der sker 'naturligt' med 'indre sammenføjninger'.
Hver række bliver duplikeret med et andet 'sekvensnummer' fra integerseries
tabel, som vi bruger som et 'indeks' af 'ressourcen' på listen, som vi ønsker at bruge til den row
.
Ideen er at:
- tæl antallet af elementer på listen.
- udtræk hvert element baseret på dets placering på listen.
- Brug
integerseries
at konvertere en række til et sæt rækker, der udtrækker det individuelle 'ressource-id' frauser
.resources
mens vi går.
Jeg besluttede at bruge to funktioner:
-
funktion, der givet en 'separeret strengliste' og et 'indeks' vil returnere værdien på positionen i listen. Jeg kalder det:
VALUE_IN_SET
. dvs. givet 'A;B;C' og et 'indeks' på 2, så returnerer det 'B'. -
funktion, der givet en 'separeret strengliste' vil returnere antallet af elementer på listen. Jeg kalder det:
COUNT_IN_SET
. dvs. givet 'A;B;C' vil returnere 3
Det viser sig, at disse to funktioner og integerseries
skal give en generel løsning på delimited items list in a column
.
Virker det?
Forespørgslen om at oprette en 'normaliseret' tabel fra en ';' delimited string in column
. Den viser alle kolonnerne, inklusive de genererede værdier på grund af 'cross_join' (isequence.id
som resources_index
):
SELECT user_resource.user,
user_resource.resources,
COUNT_IN_SET(user_resource.resources, ';') AS resources_count,
isequence.id AS resources_index,
VALUE_IN_SET(user_resource.resources, ';', isequence.id) AS resources_value
FROM
user_resource
JOIN integerseries AS isequence
ON isequence.id <= COUNT_IN_SET(user_resource.resources, ';')
ORDER BY
user_resource.user, isequence.id
Det 'normaliserede' tabeloutput:
user resources resources_count resources_index resources_value
---------- --------- --------------- --------------- -----------------
sampleuser 1;2;3 3 1 1
sampleuser 1;2;3 3 2 2
sampleuser 1;2;3 3 3 3
stacky 2 1 1 2
testuser 1;3 2 1 1
testuser 1;3 2 2 3
Ved at bruge ovenstående 'normaliserede' user_resources
tabel, er det en simpel join for at give det nødvendige output:
De nødvendige funktioner (disse er generelle funktioner, der kan bruges overalt )
bemærk:Navnene på disse funktioner er relateret til mysql FIND_IN_SET-funktion . dvs. de gør lignende ting med hensyn til strengelister?
COUNT_IN_SET
funktion:returnerer antallet af character delimited items
i kolonnen.
DELIMITER $$
DROP FUNCTION IF EXISTS `COUNT_IN_SET`$$
CREATE FUNCTION `COUNT_IN_SET`(haystack VARCHAR(1024),
delim CHAR(1)
) RETURNS INTEGER
BEGIN
RETURN CHAR_LENGTH(haystack) - CHAR_LENGTH( REPLACE(haystack, delim, '')) + 1;
END$$
DELIMITER ;
VALUE_IN_SET
funktion:behandler den delimited list
som en one based array
og returnerer værdien ved det givne 'indeks'.
DELIMITER $$
DROP FUNCTION IF EXISTS `VALUE_IN_SET`$$
CREATE FUNCTION `VALUE_IN_SET`(haystack VARCHAR(1024),
delim CHAR(1),
which INTEGER
) RETURNS VARCHAR(255) CHARSET utf8 COLLATE utf8_unicode_ci
BEGIN
RETURN SUBSTRING_INDEX(SUBSTRING_INDEX(haystack, delim, which),
delim,
-1);
END$$
DELIMITER ;
Relaterede oplysninger:
-
Endelig fundet ud af, hvordan du får SQLFiddle - arbejdskode at kompilere funktioner.
-
Der er en version af dette, der virker til
SQLite
databaser også SQLite- Normalisere et sammenkædet felt og slutte sig til det?
Tabellerne (med data):
CREATE TABLE `integerseries` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=500 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*Data for the table `integerseries` */
insert into `integerseries`(`id`) values (1);
insert into `integerseries`(`id`) values (2);
insert into `integerseries`(`id`) values (3);
insert into `integerseries`(`id`) values (4);
insert into `integerseries`(`id`) values (5);
insert into `integerseries`(`id`) values (6);
insert into `integerseries`(`id`) values (7);
insert into `integerseries`(`id`) values (8);
insert into `integerseries`(`id`) values (9);
insert into `integerseries`(`id`) values (10);
Ressource:
CREATE TABLE `resource` (
`id` int(11) NOT NULL,
`data` varchar(250) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*Data for the table `resource` */
insert into `resource`(`id`,`data`) values (1,'abcde');
insert into `resource`(`id`,`data`) values (2,'qwerty');
insert into `resource`(`id`,`data`) values (3,'azerty');
User_resource:
CREATE TABLE `user_resource` (
`user` varchar(50) COLLATE utf8_unicode_ci NOT NULL,
`resources` varchar(250) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*Data for the table `user_resource` */
insert into `user_resource`(`user`,`resources`) values ('sampleuser','1;2;3');
insert into `user_resource`(`user`,`resources`) values ('stacky','3');
insert into `user_resource`(`user`,`resources`) values ('testuser','1;3');