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

标签 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|               |
|               |                     |               |
+---------------+---------------------+---------------+

我想针对三个表运行一个或多个标签的查询,以返回与所有标签匹配的项目。

因此,例如,查询“pet”将返回项目 (1)spaniel、(2)tabby 和 (4)goldfish,因为它们都被标记为“pet”。同时查询“便宜”和“宠物”将返回 (1)spaniel 和 (4)goldfish,因为它们都被标记为“便宜”和“宠物”。虎斑猫不会被退回,因为它只被标记为“宠物”而不是“便宜”(在我的世界里,虎斑猫很贵 :P)

查询“cheap”、“pet”和“dog”只会返回 (1)Spaniel,因为它是唯一匹配所有三个标签的一只。

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

  1. Is this the best way to set up my tables for my intended purposes? I am still new to ideas of normalising databases, and am picking this up as I go along - any input on efficiency or even if this is an appropriate layout for my database would be much appreciated.

  2. Provided the above setup is workable, how could I structure a single MySQL query to achieve my intended purpose?* (that being, for a series of tags, returning ONLY the item(s) that match ALL the specified tags). I have tried doing a variety of JOINs/UNIONs but none of them are giving me the desired effect(usually return ALL the items that match ANY of the tags). I've spent some time looking through the MySQL manual online but I feel like I'm missing something conceptually.

*我说的是单一查询,因为我当然可以只运行一系列简单的 WHERE/JOIN 查询,每个标签一个,然后在 PHP 或事后对返回的项目进行组合/排序,但这似乎是愚蠢且低效的这样做的方式。我觉得有一种方法我应该能够通过一个 MySQL 查询来执行此操作,只要设置适当。

最佳答案

您的架构看起来相当不错。连接表中不需要 ID 列——只需从其他表的 ID 列创建一个主键(尽管请参阅 Marjan Venema 的评论和 Should I use composite primary keys or not? 以了解对此的替代观点)。以下示例展示了如何创建表、添加一些数据以及执行您请求的查询。

创建表,完成foreign key constraints .简而言之,外键约束有助于确保数据库的完整性。在此示例中,如果 itemtag 表中没有匹配项,它们会阻止将项插入连接表 (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 |
+----+----------+-----+

选择带有一个或多个标签的项目。请注意,这将返回具有标签 cheappet 的项目:

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 |
+----+----------+-------+

您可以通过添加 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)

GROUP BY 将所有具有相同 ID(或您指定的任何列)的项目组合到一行中,从而有效地删除重复项。 HAVING COUNT 将结果限制为匹配分组行的计数等于二的结果。这可确保仅返回具有两个标签的项目 - 请注意,此值必须与 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 添加到 itemtag 列。这具有帮助加快依赖这些列的查询的额外好处。更新后的 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'

关于mysql - 这个(规范化的)数据库结构是否允许我按照我的意图通过标签进行搜索?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3192416/

相关文章:

python - web.py sql查询,为什么只能遍历第一遍的结果?

MySQL 选择记录集中的 WHERE

php - PayPal txn_id IPN 检查

mysql - 数据库建模 : Facebook like messages

mysql - 如何处理用户选项和mysql数据库

r - 如何(有效地)在 R 中连接具有多个主键的表?

java - 如何将 SQLite 数据从 Android 发送到 MySQL 服务器?

mysql - 决定数据库表架构

MySQL 在连接时丢失行

MySQL:根据已选行中的值选择行