Se os
user_resources (t1) era uma 'tabela normalizada' com uma linha para cada user => resource combinação, a consulta para obter a resposta seria tão simples quanto apenas joining as mesas juntas. Infelizmente, é
denormalized tendo os resources coluna como:'lista de id de recurso' separada por ';' personagem. Se pudéssemos converter a coluna 'resources' em linhas, muitas das dificuldades desapareceriam à medida que as junções da tabela se tornassem simples.
A consulta para gerar a saída solicitada:
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
A saída:
user data
---------- --------
sampleuser abcde
sampleuser azerty
sampleuser qwerty
stacky qwerty
testuser abcde
testuser azerty
Como:
O 'truque' é ter uma tabela que contenha os números de 1 até algum limite. Eu chamo de
integerseries . Ele pode ser usado para converter coisas 'horizontais' como:';' delimited strings em rows . A maneira como isso funciona é que quando você 'junta' com
integerseries , você está fazendo uma cross join , que é o que acontece 'naturalmente' com 'junções internas'. Cada linha é duplicada com um 'número de sequência' diferente da
integerseries tabela que usamos como um 'índice' do 'recurso' na lista que queremos usar para essa row . A ideia é:
- contar o número de itens na lista.
- extraia cada item com base em sua posição na lista.
- Usar
integerseriespara converter uma linha em um conjunto de linhas extraindo o 'id de recurso' individual deuser.resourcesà medida que avançamos.
Resolvi usar duas funções:
-
função que fornece uma 'lista de strings delimitada' e um 'índice' retornará o valor na posição na lista. Eu chamo de:VALUE_IN_SET. ou seja, dado 'A;B;C' e um 'índice' de 2, ele retorna 'B'.
-
função que dada uma 'lista de strings delimitada' retornará a contagem do número de itens na lista. Eu chamo de:COUNT_IN_SET. ou seja, dado 'A;B;C' retornará 3
Acontece que essas duas funções e
integerseries deve fornecer uma solução geral para delimited items list in a column . Funciona?
A consulta para criar uma tabela 'normalizada' de um
';' delimited string in column . Mostra todas as colunas, incluindo os valores gerados devido ao 'cross_join' (isequence.id como 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
A saída da tabela 'normalizada':
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
Usando os
user_resources 'normalizados' acima table, é uma junção simples para fornecer a saída necessária:As funções necessárias (estas são funções gerais que podem ser usadas em qualquer lugar )
nota:Os nomes dessas funções estão relacionados ao mysql função FIND_IN_SET . ou seja, eles fazem coisas semelhantes em relação às listas de strings?
O
COUNT_IN_SET função:retorna a contagem de character delimited items na coluna. 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 ;
O
VALUE_IN_SET função:trata a delimited list como um one based array e retorna o valor no 'índice' fornecido. 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 ;
Informações relacionadas:
-
Finalmente descobri como obter SQLFiddle - código de trabalho para compilar funções.
-
Existe uma versão disso que funciona paraSQLitebancos de dados também SQLite- Normalizando um campo concatenado e unindo-se a ele?
As tabelas (com dados):
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);
Recurso:
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');
Recurso_usuário:
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');