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
integerseries
para 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 paraSQLite
bancos 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');