我正在尝试建立一个包含以下三个表的规范化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| |
| | | |
+---------------+---------------------+---------------+
我想针对三个表运行一个或多个标签的查询,以返回与所有标签匹配的项目。
因此,例如,查询“宠物”将返回项目(1)spaniel,(2)tabby和(4)金鱼,因为所有这些都被标记为“pet”。一起查询“便宜”和“宠物”将返回(1)西班牙猎犬和(4)金鱼,因为它们都被标记为“便宜”和“宠物”。 Tabby不会被退回,因为它只标记为“宠物”但不是“便宜”(在我的世界虎斑猫很贵:P)
查询“便宜”,“宠物”和“狗”只会返回(1)西班牙猎犬,因为它是唯一一个匹配所有三个标签。
无论如何,这是理想的行为。我有两个问题。
这是为我的预期用途设置表格的最佳方法吗?我是 对于正常化的想法仍然是新的 数据库,我正在挑选这个 同意 - 关于效率的任何意见或 即使这是一个合适的布局 对于我的数据库会很多 赞赏。
- 醇>
如果上述设置可行,我该如何构建一个 单个MySQL查询来实现我的 预期目的?*(就是说 系列标签,仅返回 匹配所有指定项目的项目 标签)。我尝试过做各种各样的事情 JOIN / UNIONs,但没有一个 给我预期的效果(通常 返回所有匹配任何项目的项目 标签)。我花了一些时间 浏览MySQL手册 网上但我觉得我很想念 概念上的东西。
*我说单个查询,因为我当然可以运行一系列简单的WHERE / JOIN查询,每个标签一个,然后在PHP之后对返回的项目进行组合/排序,但这看起来是愚蠢和低效的这样做的方式。考虑到适当的设置,我觉得有一种方法可以用一个MySQL查询来完成这个。
答案 0 :(得分:10)
您的架构看起来相当不错。您不需要在连接表中使用ID列 - 只需从其他表的ID列创建主键(尽管请参阅Marjan Venema的注释和Should I use composite primary keys or not?以获取其他视图)。以下示例显示了如何创建表,添加一些数据以及执行所请求的查询。
创建表格,并附上foreign key constraints。简而言之,外键约束有助于确保数据库的完整性。在此示例中,如果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;
插入一些测试数据:
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);
选择所有项目和所有标签:
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 |
+----+----------+-----------+
选择具有特定标记的项目:
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 |
+----+----------+-----+
选择包含一个或多个标签的项目。请注意,这将返回标记为 cheap 或 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 |
+----+----------+-------+
以上查询会生成您可能不需要的答案,如以下查询所突出显示的那样。在这种情况下,没有包含 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 |
+----+----------+-------+
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)
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 |
+----+----------+-----+
请注意,在上一个示例中,项目已组合在一起,因此您不会获得重复项。在这种情况下,不需要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 |
+----+----------+
您可以更进一步,并使用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 |
+----+----------+-----------+
上述架构设计的一个问题是可以输入重复的项目和标签。也就是说,您可以根据需要多次将 bird 插入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;
现在,如果您尝试插入重复值,MySQL将阻止您这样做:
INSERT INTO tag (tag) VALUES ('bird');
ERROR 1062 (23000): Duplicate entry 'bird' for key 'tag'
答案 1 :(得分:3)
是。这称为关系分裂。这里讨论了各种技术http://www.simple-talk.com/sql/t-sql-programming/divided-we-stand-the-sql-of-relational-division/
一种方法是使用双阴性。即。选择表1中的所有记录,其中“cheap”,“pet”列表中没有标记,表2中没有相关记录
SELECT t1.id, t1.item
FROM Table1 t1
WHERE NOT EXISTS
(
SELECT * FROM
table3 t3 WHERE tag IN ('cheap','pet')
AND NOT EXISTS (
SELECT * FROM table2 t2
WHERE t2.tag_id = t3.id
AND t1.id=t2.item_id
)
)
答案 2 :(得分:0)
这种映射表概念非常标准,在这里看起来很好实现。我唯一要改变的就是摆脱表2中的ID;你会用什么?只需在项目ID和标签ID上为表2创建一个联合密钥。
实际上,选择项目与所有标签匹配的位置很难。试试这个:
SELECT item_id,COUNT(tag_id)FROM Table2 WHERE tag_id IN(此处设置)GROUP BY item_id
如果计数等于您的集合中的标记ID数量,则表示您找到了匹配项。
答案 3 :(得分:0)
您可以尝试这样的事情:
select item, count(*) 'NrMatches'
from #table1 i
inner join #table2 l ON i.id = l.item_id
inner join #table3 t on l.tag_id = t.id
where t.tag IN ('cheap', 'pet', 'dog')
group by item
having count(*) = (select count(*) from #table3
where tag IN ('cheap', 'pet', 'dog'))
这意味着您的搜索字词两次,但它主要是您所追求的。
答案 4 :(得分:0)
不确定其他人可能已经提到过这一点,但第二个表中的id列是多余的。您只需创建一个连接主键:
PRIMARY KEY (item_id, tag_id)
否则,它是一个严格的标准m:n数据库方案,它应该可以正常工作。
答案 5 :(得分:0)
感谢大家的非常详细和有用的回复。关于使用“WHERE标记IN('tag_1'...'tag_x')”与COUNT一起选择与所有标记匹配的项目的这一点正是我之前所缺少的。
使用复合主键的输入也非常有用 - 我觉得没有必要在中间表上使用唯一的ID键,但从未意识到我可以使用复合键。
再次感谢你!你们真棒!