Om user_resources
(t1) var en 'normaliserad tabell' med en rad för varje user => resource
kombination så skulle frågan för att få svaret vara så enkel som att bara joining
borden tillsammans.
Tyvärr är den denormalized
genom att ha resources
kolumn som en:'lista över resurs-id' separerad av ett ';' tecken.
Om vi kunde konvertera kolumnen "resurser" till rader försvinner många av svårigheterna eftersom tabellen blir enklare.
Frågan för att generera utdata som efterfrågas:
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
Utgången:
user data
---------- --------
sampleuser abcde
sampleuser azerty
sampleuser qwerty
stacky qwerty
testuser abcde
testuser azerty
Hur:
"Knepet" är att ha en tabell som innehåller siffrorna från 1 till någon gräns. Jag kallar det integerseries
. Den kan användas för att konvertera "horisontella" saker som:';' delimited strings
i rows
.
Sättet detta fungerar är att när du "ansluter" med integerseries
, gör du en cross join
, vilket är vad som händer "naturligt" med "inre sammanfogningar".
Varje rad dupliceras med ett annat "sekvensnummer" från integerseries
tabell som vi använder som ett 'index' för 'resursen' i listan som vi vill använda för den row
.
Tanken är att:
- räkna antalet objekt i listan.
- extrahera varje objekt baserat på dess position i listan.
- Använd
integerseries
att konvertera en rad till en uppsättning rader som extraherar det individuella "resurs-id" frånuser
.resources
allt eftersom.
Jag bestämde mig för att använda två funktioner:
-
funktion som givet en 'avgränsad stränglista' och ett 'index' kommer att returnera värdet på positionen i listan. Jag kallar det:
VALUE_IN_SET
. d.v.s. ges 'A;B;C' och ett 'index' på 2 så returnerar det 'B'. -
funktion som ger en 'avgränsad stränglista' returnerar antalet objekt i listan. Jag kallar det:
COUNT_IN_SET
. d.v.s. givet 'A;B;C' kommer att returnera 3
Det visar sig att dessa två funktioner och integerseries
bör tillhandahålla en allmän lösning på delimited items list in a column
.
Fungerar det?
Frågan för att skapa en 'normaliserad' tabell från en ';' delimited string in column
. Den visar alla kolumner, inklusive de genererade värdena på grund av "cross_join" (isequence.id
som 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
Den "normaliserade" tabellutgången:
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
Använder ovanstående "normaliserade" user_resources
tabell, är det en enkel koppling för att tillhandahålla den utdata som krävs:
De funktioner som behövs (detta är allmänna funktioner som kan användas var som helst )
notera:Namnen på dessa funktioner är relaterade till mysql FIND_IN_SET-funktionen . dvs de gör liknande saker när det gäller stränglistor?
COUNT_IN_SET
funktion:returnerar antalet character delimited items
i kolumnen.
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
funktion:behandlar den delimited list
som en one based array
och returnerar värdet vid det givna 'indexet'.
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 ;
Relaterad information:
-
Äntligen utarbetat hur man skaffar SQLFiddle - working code för att kompilera funktioner.
-
Det finns en version av detta som fungerar för
SQLite
databaser också SQLite- Normalisera ett sammanlänkade fält och ansluta till det?
Tabellerna (med data):
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);
Resurs:
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');