Als de user_resources
(t1) was een 'genormaliseerde tabel' met één rij voor elke user => resource
combinatie, dan is de vraag om het antwoord te krijgen zo simpel als gewoon joining
de tafels bij elkaar.
Helaas, het is denormalized
door de resources
kolom als een:'lijst met resource-ID' gescheiden door een ';' karakter.
Als we de kolom 'resources' in rijen zouden kunnen omzetten, verdwijnen veel van de problemen als het samenvoegen van de tabel eenvoudig wordt.
De vraag om de gevraagde uitvoer te genereren:
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
De uitvoer:
user data
---------- --------
sampleuser abcde
sampleuser azerty
sampleuser qwerty
stacky qwerty
testuser abcde
testuser azerty
Hoe:
De 'truc' is om een tabel te hebben die de getallen van 1 tot een bepaalde limiet bevat. Ik noem het integerseries
. Het kan worden gebruikt om 'horizontale' dingen te converteren, zoals:';' delimited strings
in rows
.
De manier waarop dit werkt, is dat wanneer je 'deelneemt' met integerseries
, je doet een cross join
, wat 'natuurlijk' gebeurt met 'inner joins'.
Elke rij wordt gedupliceerd met een ander 'volgnummer' uit de integerseries
tabel die we gebruiken als een 'index' van de 'resource' in de lijst die we willen gebruiken voor die row
.
Het idee is om:
- tel het aantal items in de lijst.
- extraheer elk item op basis van de positie in de lijst.
- Gebruik
integerseries
om één rij om te zetten in een reeks rijen door de individuele 'resource-ID' uituser
te extraheren .resources
terwijl we verder gaan.
Ik besloot om twee functies te gebruiken:
-
functie die gegeven een 'delimited string list' en een 'index' de waarde op de positie in de lijst zal retourneren. Ik noem het:
VALUE_IN_SET
. d.w.z. gegeven 'A;B;C' en een 'index' van 2 dan retourneert het 'B'. -
functie die gegeven een 'lijst met gescheiden tekenreeksen' de telling van het aantal items in de lijst zal retourneren. Ik noem het:
COUNT_IN_SET
. d.w.z. gegeven 'A;B;C' zal 3 teruggeven
Het blijkt dat die twee functies en integerseries
zou een algemene oplossing moeten bieden voor delimited items list in a column
.
Werkt het?
De query om een 'genormaliseerde' tabel te maken van een ';' delimited string in column
. Het toont alle kolommen, inclusief de gegenereerde waarden vanwege de 'cross_join' (isequence.id
als 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
De 'genormaliseerde' tabeluitvoer:
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
Met behulp van de bovenstaande 'genormaliseerde' user_resources
tabel, het is een simpele join om de vereiste output te leveren:
De benodigde functies (dit zijn algemene functies die overal kunnen worden gebruikt )
opmerking:de namen van deze functies zijn gerelateerd aan de mysql FIND_IN_SET functie . d.w.z. ze doen soortgelijke dingen met betrekking tot stringlijsten?
De COUNT_IN_SET
functie:retourneert het aantal character delimited items
in de kolom.
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 ;
De VALUE_IN_SET
functie:behandelt de delimited list
als een one based array
en retourneert de waarde bij de opgegeven 'index'.
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 ;
Verwante informatie:
-
Eindelijk uitgewerkt hoe je SQLFiddle - werkende code kunt krijgen om functies te compileren.
-
Er is een versie hiervan die werkt voor
SQLite
databases ook SQLite- Een aaneengeschakeld veld normaliseren en ermee samenvoegen?
De tabellen (met gegevens):
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);
Bron:
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');