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

时间:2010-07-07 06:31:09

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

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

因此,例如,查询“宠物”将返回项目(1)spaniel,(2)tabby和(4)金鱼,因为所有这些都被标记为“pet”。一起查询“便宜”和“宠物”将返回(1)西班牙猎犬和(4)金鱼,因为它们都被标记为“便宜”和“宠物”。 Tabby不会被退回,因为它只标记为“宠物”但不是“便宜”(在我的世界虎斑猫很贵:P)

查询“便宜”,“宠物”和“狗”只会返回(1)西班牙猎犬,因为它是唯一一个匹配所有三个标签。

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

  
      
  1. 这是为我的预期用途设置表格的最佳方法吗?我是   对于正常化的想法仍然是新的   数据库,我正在挑选这个   同意 - 关于效率的任何意见或   即使这是一个合适的布局   对于我的数据库会很多   赞赏。

  2.   
  3. 如果上述设置可行,我该如何构建一个   单个MySQL查询来实现我的   预期目的?*(就是说   系列标签,仅返回   匹配所有指定项目的项目   标签)。我尝试过做各种各样的事情   JOIN / UNIONs,但没有一个   给我预期的效果(通常   返回所有匹配任何项目的项目   标签)。我花了一些时间   浏览MySQL手册   网上但我觉得我很想念   概念上的东西。

  4.   

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

6 个答案:

答案 0 :(得分:10)

您的架构看起来相当不错。您不需要在连接表中使用ID列 - 只需从其他表的ID列创建主键(尽管请参阅Marjan Venema的注释和Should I use composite primary keys or not?以获取其他视图)。以下示例显示了如何创建表,添加一些数据以及执行所请求的查询。

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

您可以通过添加GROUP BYHAVING来解决此问题:

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 INDEXitem列中添加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)

  1. 这种映射表概念非常标准,在这里看起来很好实现。我唯一要改变的就是摆脱表2中的ID;你会用什么?只需在项目ID和标签ID上为表2创建一个联合密钥。

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

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

  3. 如果计数等于您的集合中的标记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键,但从未意识到我可以使用复合键。

再次感谢你!你们真棒!