Si les user_resources
(t1) était une 'table normalisée' avec une ligne pour chaque user => resource
combinaison alors la requête pour obtenir la réponse serait aussi simple que de simplement joining
les tables ensemble.
Hélas, il est denormalized
en ayant les resources
colonne sous la forme d'une :'liste d'ID de ressource' séparés par un ';' caractère.
Si nous pouvions convertir la colonne « ressources » en lignes, la plupart des difficultés disparaîtraient à mesure que les jointures de table deviendraient simples.
La requête pour générer la sortie demandée :
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
Le résultat :
user data
---------- --------
sampleuser abcde
sampleuser azerty
sampleuser qwerty
stacky qwerty
testuser abcde
testuser azerty
Comment :
Le "truc" est d'avoir une table qui contient les nombres de 1 à une certaine limite. Je l'appelle integerseries
. Il peut être utilisé pour convertir des éléments 'horizontaux' tels que :';' delimited strings
en rows
.
La façon dont cela fonctionne est que lorsque vous "rejoignez" avec integerseries
, vous faites une cross join
, ce qui se passe "naturellement" avec les "jointures internes".
Chaque ligne est dupliquée avec un "numéro de séquence" différent de la integerseries
table que nous utilisons comme 'index' de la 'ressource' dans la liste que nous voulons utiliser pour cette row
.
L'idée est de :
- comptez le nombre d'éléments dans la liste.
- extraire chaque élément en fonction de sa position dans la liste.
- Utilisez des
integerseries
pour convertir une ligne en un ensemble de lignes en extrayant l'identifiant de ressource individuel deuser
.resources
au fur et à mesure.
J'ai décidé d'utiliser deux fonctions :
-
fonction qui, étant donné une 'liste de chaînes délimitée' et un 'index', renverra la valeur à la position dans la liste. Je l'appelle :
VALUE_IN_SET
. c'est-à-dire qu'étant donné 'A;B;C' et un 'index' de 2, il renvoie 'B'. -
fonction qui, étant donné une "liste de chaînes délimitée", renverra le nombre d'éléments de la liste. Je l'appelle :
COUNT_IN_SET
. c'est-à-dire que 'A;B;C' renverra 3
Il s'avère que ces deux fonctions et integerseries
devrait fournir une solution générale à la delimited items list in a column
.
Ça marche ?
La requête pour créer une table 'normalisée' à partir d'un ';' delimited string in column
. Il affiche toutes les colonnes, y compris les valeurs générées en raison du 'cross_join' (isequence.id
comme 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
Le résultat du tableau "normalisé" :
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
Utilisation des user_resources
"normalisés" ci-dessus table, il s'agit d'une simple jointure pour fournir la sortie requise :
Les fonctions nécessaires (ce sont des fonctions générales qui peuvent être utilisées n'importe où )
note :Les noms de ces fonctions sont liés au mysql fonction FIND_IN_SET . c'est-à-dire qu'ils font des choses similaires en ce qui concerne les listes de chaînes ?
Le COUNT_IN_SET
fonction :renvoie le nombre d'éléments character delimited items
dans la colonne.
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 ;
Le VALUE_IN_SET
fonction :traite la delimited list
en tant que one based array
et renvoie la valeur à 'l'index' donné.
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 ;
Informations connexes :
-
Enfin trouvé comment obtenir SQLFiddle - code de travail pour compiler des fonctions.
-
Il existe une version de ceci qui fonctionne pour
SQLite
bases de données également SQLite- Normaliser un champ concaténé et le joindre ?
Les tableaux (avec données) :
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');