Ако user_resources
(t1) беше „нормализирана таблица“ с един ред за всеки user => resource
комбинация, тогава заявката за получаване на отговора би била толкова проста, колкото просто joining
таблиците заедно.
Уви, той е denormalized
като разполагате с resources
колона като:'списък с идентификатор на ресурс', разделен с ';' знак.
Ако можем да преобразуваме колоната „ресурси“ в редове, тогава много от трудностите изчезват, когато присъединяването на таблицата стане лесно.
Заявката за генериране на изхода поиска:
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
Изходът:
user data
---------- --------
sampleuser abcde
sampleuser azerty
sampleuser qwerty
stacky qwerty
testuser abcde
testuser azerty
Как:
„Тръкът“ е да имате таблица, която съдържа числата от 1 до някакъв лимит. Наричам го integerseries
. Може да се използва за преобразуване на "хоризонтални" неща като:';' delimited strings
в rows
.
Начинът, по който това работи, е, че когато се "присъедините" с integerseries
, правите cross join
, което се случва „естествено“ с „вътрешни съединения“.
Всеки ред се дублира с различен "пореден номер" от integerseries
таблица, която използваме като "индекс" на "ресурса" в списъка, който искаме да използваме за този row
.
Идеята е да:
- пребройте броя на елементите в списъка.
- извличане на всеки елемент въз основа на неговата позиция в списъка.
- Използвайте
integerseries
за да конвертирате един ред в набор от редове, извличайки отделния „идентификатор на ресурса“ отuser
.resources
докато вървим напред.
Реших да използвам две функции:
-
функция, която дава „списък с разделени низове“ и „индекс“ ще върне стойността на позицията в списъка. Наричам го:
VALUE_IN_SET
. т.е. ако има 'A;B;C' и 'индекс' от 2, тогава той връща 'B'. -
функция, която дава 'списък с разделени низове' ще върне броя на броя на елементите в списъка. Наричам го:
COUNT_IN_SET
. т.е. дадено 'A;B;C' ще върне 3
Оказва се, че тези две функции и integerseries
трябва да предостави общо решение за delimited items list in a column
.
Работи ли?
Заявката за създаване на "нормализирана" таблица от ';' delimited string in column
. Той показва всички колони, включително генерираните стойности поради 'cross_join' (isequence.id
като 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
Изходът на „нормализираната“ таблица:
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
Използване на горните "нормализирани" user_resources
таблица, това е просто присъединяване за осигуряване на необходимия изход:
Необходимите функции (това са общи функции, които могат да се използват навсякъде )
забележка:Имената на тези функции са свързани с mysql функция FIND_IN_SET . т.е. правят ли подобни неща по отношение на списъците с низове?
COUNT_IN_SET
функция:връща броя на character delimited items
в колоната.
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
функция:третира delimited list
като one based array
и връща стойността в дадения 'индекс'.
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 ;
Свързана информация:
-
Най-накрая разбрах как да получа SQLPiddle - работещ код за компилиране на функции.
-
Има версия на това, която работи за
SQLite
както и бази данни SQLite – Нормализиране на свързано поле и присъединяване към него?
Таблиците (с данни):
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);
Ресурс:
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');
Потребителски ресурс:
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');