Bam*_*boo 8 mysql database-design join relational-database
我正在尝试建立一个包含以下三个表的规范化MySQL数据库.第一个表包含可由各种标签描述的项目列表.第三个表包含用于描述第一个表中的项目的各种标记.中间表将另外两个表相互关联.在每个表的情况下,id是一个自动递增的主键(并且每个用作中间表中的外键)
+---------------+---------------------+---------------+
| Table 1 | Table 2 | Table 3 |
+---------------+---------------------+---------------+
|id item |id item_id tag_id|id tag|
+---------------+---------------------+---------------+
| 1 spaniel| 1 1 4| 1 bird|
| 2 tabby| 2 1 23| 4 pet|
| 3 chicken| 3 1 41|23 dog|
| 4 goldfish| 4 2 4|24 cat|
| | 5 2 24|25 reptile|
| | 6 3 1|38 fish|
| | 7 3 40|40 delicious|
| | 8 4 4|41 cheap|
| | 9 4 38|42 expensive|
| |10 4 41| |
| | | |
+---------------+---------------------+---------------+
Run Code Online (Sandbox Code Playgroud)
我想针对三个表运行一个或多个标签的查询,以返回与所有标签匹配的项目.
因此,例如,查询"宠物"将返回项目(1)spaniel,(2)虎斑和(4)金鱼,因为它们都被标记为"宠物".一起查询"便宜"和"宠物"会返回(1)西班牙猎犬和(4)金鱼,因为它们都被标记为"便宜"和"宠物".Tabby不会被退回,因为它只被标记为"宠物"而不是"便宜"(在我的世界虎斑猫很贵:P)
查询"便宜","宠物"和"狗"只会返回(1)西班牙猎犬,因为它是唯一一个匹配所有三个标签.
无论如何,这是期望的行为.我有两个问题.
这是为我的预期目的设置表格的最佳方法吗?我仍然对数据库规范化的想法不熟悉,并且随着我的进展选择了这一点 - 关于效率的任何输入,或者即使这是我的数据库的适当布局也将非常感激.
如果上面的设置是可行的,我怎么能构建一个MySQL查询来实现我的预期目的?*(对于一系列标签,只返回与所有指定标签匹配的项目).我尝试过各种JOIN/UNION但它们都没有给我预期的效果(通常返回所有与任何标签匹配的项目).我花了一些时间在线查看MySQL手册,但我觉得我在概念上缺少一些东西.
*我说单个查询,因为我当然可以运行一系列简单的WHERE/JOIN查询,每个标签一个,然后在PHP之后对返回的项目进行组合/排序,但这似乎是一种愚蠢而低效的方式它.考虑到适当的设置,我觉得有一种方法可以用一个MySQL查询来完成.
Mik*_*ike 10
您的架构看起来相当不错.您不需要在连接表中使用ID列 - 只需从其他表的ID列创建主键(尽管请参阅Marjan Venema的注释,我是否应该使用复合主键?对于此处的替代视图).以下示例显示如何创建表,添加一些数据以及执行所请求的查询.
创建表,完成外键约束.简而言之,外键约束有助于确保数据库的完整性.在此示例中,item_tag如果item和tag表中没有匹配的项,它们会阻止项插入连接表():
CREATE TABLE IF NOT EXISTS `item` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`item` VARCHAR(255) NOT NULL ,
PRIMARY KEY (`id`) )
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `tag` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`tag` VARCHAR(255) NOT NULL ,
PRIMARY KEY (`id`) )
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `item_tag` (
`item_id` INT UNSIGNED NOT NULL ,
`tag_id` INT UNSIGNED NOT NULL ,
PRIMARY KEY (`item_id`, `tag_id`) ,
INDEX `fk_item_tag_item` (`item_id` ASC) ,
INDEX `fk_item_tag_tag` (`tag_id` ASC) ,
CONSTRAINT `fk_item_tag_item`
FOREIGN KEY (`item_id` )
REFERENCES `item` (`id` )
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT `fk_item_tag_tag`
FOREIGN KEY (`tag_id` )
REFERENCES `tag` (`id` )
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB;
Run Code Online (Sandbox Code Playgroud)
插入一些测试数据:
INSERT INTO item (item) VALUES
('spaniel'),
('tabby'),
('chicken'),
('goldfish');
INSERT INTO tag (tag) VALUES
('bird'),
('pet'),
('dog'),
('cat'),
('reptile'),
('fish'),
('delicious'),
('cheap'),
('expensive');
INSERT INTO item_tag (item_id, tag_id) VALUES
(1,2),
(1,3),
(1,8),
(2,2),
(2,4),
(3,1),
(3,7),
(4,2),
(4,6),
(4,8);
Run Code Online (Sandbox Code Playgroud)
选择所有项目和所有标签:
SELECT item.id, item.item, tag.tag
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id;
+----+----------+-----------+
| id | item | tag |
+----+----------+-----------+
| 1 | spaniel | pet |
| 1 | spaniel | dog |
| 1 | spaniel | cheap |
| 2 | tabby | pet |
| 2 | tabby | cat |
| 3 | chicken | bird |
| 3 | chicken | delicious |
| 4 | goldfish | pet |
| 4 | goldfish | fish |
| 4 | goldfish | cheap |
+----+----------+-----------+
Run Code Online (Sandbox Code Playgroud)
选择具有特定标记的项目:
SELECT item.id, item.item, tag.tag
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag = 'pet';
+----+----------+-----+
| id | item | tag |
+----+----------+-----+
| 1 | spaniel | pet |
| 2 | tabby | pet |
| 4 | goldfish | pet |
+----+----------+-----+
Run Code Online (Sandbox Code Playgroud)
选择带有一个或多个标签的项目.请注意,这将返回标签便宜 OR pet的项目:
SELECT item.id, item.item, tag.tag
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag IN ('cheap', 'pet');
+----+----------+-------+
| id | item | tag |
+----+----------+-------+
| 1 | spaniel | pet |
| 1 | spaniel | cheap |
| 2 | tabby | pet |
| 4 | goldfish | pet |
| 4 | goldfish | cheap |
+----+----------+-------+
Run Code Online (Sandbox Code Playgroud)
上面的查询会生成您可能不需要的答案,如以下查询所突出显示的那样.在这种情况下,没有带有house标签的项目,但此查询仍返回一些行:
SELECT item.id, item.item, tag.tag
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag IN ('cheap', 'house');
+----+----------+-------+
| id | item | tag |
+----+----------+-------+
| 1 | spaniel | cheap |
| 4 | goldfish | cheap |
+----+----------+-------+
Run Code Online (Sandbox Code Playgroud)
SELECT item.id, item.item, tag.tag
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag IN ('cheap', 'house')
GROUP BY item.id HAVING COUNT(*) = 2;
Empty set (0.00 sec)
Run Code Online (Sandbox Code Playgroud)
GROUP BY导致具有相同id(或您指定的任何列)的所有项目组合在一起成为一行,从而有效地删除重复项. HAVING COUNT将结果限制为匹配的分组行的计数等于2的结果.这确保仅返回具有两个标记的项目 - 请注意,此值必须与IN子句中指定的标记数相匹配.这是一个产生一些东西的例子:
SELECT item.id, item.item, tag.tag
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag IN ('cheap', 'pet')
GROUP BY item.id HAVING COUNT(*) = 2;
+----+----------+-----+
| id | item | tag |
+----+----------+-----+
| 1 | spaniel | pet |
| 4 | goldfish | pet |
+----+----------+-----+
Run Code Online (Sandbox Code Playgroud)
请注意,在上一个示例中,项目已组合在一起,因此您不会获得重复项.在这种情况下,不需要tag列,因为这只会混淆结果 - 您已经知道有哪些标记,因为您已经请求具有这些标记的项目.因此,您可以通过tag从查询中删除列来简化一些事情:
SELECT item.id, item.item
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag IN ('cheap', 'pet')
GROUP BY item.id HAVING COUNT(*) = 2;
+----+----------+
| id | item |
+----+----------+
| 1 | spaniel |
| 4 | goldfish |
+----+----------+
Run Code Online (Sandbox Code Playgroud)
您可以更进一步,并用于GROUP_CONCAT提供匹配标记的列表.如果您想要一个包含一个或多个指定标记的项目列表,但这些项目列表不一定全部,这可能很方便:
SELECT item.id, item.item, GROUP_CONCAT(tag.tag) AS tags
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag IN ('cheap', 'pet', 'bird', 'cat')
GROUP BY id;
+----+----------+-----------+
| id | item | tags |
+----+----------+-----------+
| 1 | spaniel | pet,cheap |
| 2 | tabby | pet,cat |
| 3 | chicken | bird |
| 4 | goldfish | pet,cheap |
+----+----------+-----------+
Run Code Online (Sandbox Code Playgroud)
上述模式设计的一个问题是可以输入重复的项目和标签.也就是说,您可以根据需要多次将鸟插入tag表中,这并不好.解决这个问题的一种方法是UNIQUE INDEX在item和tag列中添加一个.这有助于加快依赖这些列的查询的额外好处.更新后的CREATE TABLE命令现在如下所示:
CREATE TABLE IF NOT EXISTS `item` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`item` VARCHAR(255) NOT NULL ,
UNIQUE INDEX `item` (`item`) ,
PRIMARY KEY (`id`) )
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `tag` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`tag` VARCHAR(255) NOT NULL ,
UNIQUE INDEX `tag` (`tag`) ,
PRIMARY KEY (`id`) )
ENGINE = InnoDB;
Run Code Online (Sandbox Code Playgroud)
现在,如果您尝试插入重复值,MySQL将阻止您这样做:
INSERT INTO tag (tag) VALUES ('bird');
ERROR 1062 (23000): Duplicate entry 'bird' for key 'tag'
Run Code Online (Sandbox Code Playgroud)
这个映射表概念非常标准,并且看起来在这里实现得很好。我唯一要更改的是删除表 2 中的 ID;你会用它做什么?只需在项目 ID 和标签 ID 上为表 2 创建一个联合密钥即可。
实际上,选择一个项目与所有标签匹配的位置是很困难的。尝试这个:
SELECT item_id,COUNT(tag_id) FROM Table2 WHERE tag_id IN (您在此处设置) GROUP BY item_id
如果计数等于集合中标签 ID 的数量,则表明您找到了匹配项。