这个(规范化的)数据库结构是否允许我按照我的意图按标签搜索?

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)西班牙猎犬,因为它是唯一一个匹配所有三个标签.

无论如何,这是期望的行为.我有两个问题.

  1. 这是为我的预期目的设置表格的最佳方法吗?我仍然对数据库规范化的想法不熟悉,并且随着我的进展选择了这一点 - 关于效率的任何输入,或者即使这是我的数据库的适当布局也将非常感激.

  2. 如果上面的设置是可行的,我怎么能构建一个MySQL查询来实现我的预期目的?*(对于一系列标签,只返回与所有指定标签匹配的项目).我尝试过各种JOIN/UNION但它们都没有给我预期的效果(通常返回所有与任何标签匹配的项目).我花了一些时间在线查看MySQL手册,但我觉得我在概念上缺少一些东西.

*我说单个查询,因为我当然可以运行一系列简单的WHERE/JOIN查询,每个标签一个,然后在PHP之后对返回的项目进行组合/排序,但这似乎是一种愚蠢而低效的方式它.考虑到适当的设置,我觉得有一种方法可以用一个MySQL查询来完成.

Mik*_*ike 10

您的架构看起来相当不错.您不需要在连接表中使用ID列 - 只需从其他表的ID列创建主键(尽管请参阅Marjan Venema的注释,我是否应该使用复合主键?对于此处的替代视图).以下示例显示如何创建表,添加一些数据以及执行所请求的查询.

创建表,完成外键约束.简而言之,外键约束有助于确保数据库的完整性.在此示例中,item_tag如果itemtag表中没有匹配的项,它们会阻止项插入连接表():

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)

你可以通过添加GROUP BY和修复它HAVING:

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 INDEXitemtag列中添加一个.这有助于加快依赖这些列的查询的额外好处.更新后的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)


Bor*_*lid 0

  1. 这个映射表概念非常标准,并且看起来在这里实现得很好。我唯一要更改的是删除表 2 中的 ID;你会用它做什么?只需在项目 ID 和标签 ID 上为表 2 创建一个联合密钥即可。

  2. 实际上,选择一个项目与所有标签匹配的位置是很困难的。尝试这个:

    SELECT item_id,COUNT(tag_id) FROM Table2 WHERE tag_id IN (您在此处设置) GROUP BY item_id

如果计数等于集合中标签 ID 的数量,则表明您找到了匹配项。