【发布时间】:2010-12-21 02:07:03
【问题描述】:
我想知道实现标签系统的最佳方法是什么,就像在 SO 上使用的那样。我正在考虑这个,但我想不出一个好的可扩展解决方案。
我正在考虑有一个基本的 3 表解决方案:有一个 tags 表、一个 articles 表和一个 tag_to_articles 表。
这是解决此问题的最佳方法,还是有替代方案?使用这种方法,表格会及时变得非常大,我认为搜索效率不是很高。另一方面,快速执行查询并不那么重要。
【问题讨论】:
我想知道实现标签系统的最佳方法是什么,就像在 SO 上使用的那样。我正在考虑这个,但我想不出一个好的可扩展解决方案。
我正在考虑有一个基本的 3 表解决方案:有一个 tags 表、一个 articles 表和一个 tag_to_articles 表。
这是解决此问题的最佳方法,还是有替代方案?使用这种方法,表格会及时变得非常大,我认为搜索效率不是很高。另一方面,快速执行查询并不那么重要。
【问题讨论】:
相信你会发现这篇博文很有趣:Tags: Database schemas
问题:您希望有一个数据库模式,您可以在其中标记一个 书签(或博客文章或其他任何内容),带有任意数量的标签。 稍后,您希望运行查询以将书签限制为 标签的联合或交集。您还想排除(例如:减号) 搜索结果中的一些标签。
在这个解决方案中,模式只有一个表,它是非规范化的。这种类型称为“MySQLicious 解决方案”,因为 MySQLicious 将 del.icio.us 数据导入到具有这种结构的表中。
交点 (AND) 查询“search+webservice+semweb”:
SELECT *
FROM `delicious`
WHERE tags LIKE "%search%"
AND tags LIKE "%webservice%"
AND tags LIKE "%semweb%"
联合 (OR) 查询“search|webservice|semweb”:
SELECT *
FROM `delicious`
WHERE tags LIKE "%search%"
OR tags LIKE "%webservice%"
OR tags LIKE "%semweb%"
减号 查询“search+webservice-semweb”
SELECT *
FROM `delicious`
WHERE tags LIKE "%search%"
AND tags LIKE "%webservice%"
AND tags NOT LIKE "%semweb%"
Scuttle 将其数据组织在两个表中。该表“scCategories”是“标记”表,并且有一个“书签”表的外键。
交点 (AND) 查询“书签+webservice+semweb”:
SELECT b.*
FROM scBookmarks b, scCategories c
WHERE c.bId = b.bId
AND (c.category IN ('bookmark', 'webservice', 'semweb'))
GROUP BY b.bId
HAVING COUNT( b.bId )=3
首先,搜索所有书签标签组合,其中标签为“bookmark”、“webservice”或“semweb”(c.category IN ('bookmark', 'webservice', 'semweb')),然后只将所有三个标签都搜索到的书签考虑在内(HAVING COUNT(b.bId)=3)。
联合 (OR) 查询“bookmark|webservice|semweb”: 只需省略 HAVING 子句,您就有了联合:
SELECT b.*
FROM scBookmarks b, scCategories c
WHERE c.bId = b.bId
AND (c.category IN ('bookmark', 'webservice', 'semweb'))
GROUP BY b.bId
减号(排除) 查询“bookmark+webservice-semweb”,即:bookmark AND webservice AND NOT semweb。
SELECT b. *
FROM scBookmarks b, scCategories c
WHERE b.bId = c.bId
AND (c.category IN ('bookmark', 'webservice'))
AND b.bId NOT
IN (SELECT b.bId FROM scBookmarks b, scCategories c WHERE b.bId = c.bId AND c.category = 'semweb')
GROUP BY b.bId
HAVING COUNT( b.bId ) =2
省略 HAVING COUNT 会导致查询“bookmark|webservice-semweb”。
Toxi 提出了一个三表结构。通过表“tagmap”,书签和标签是 n 到 m 相关的。每个标签可以与不同的书签一起使用,反之亦然。这个 DB-schema 也被 wordpress 使用。 查询与“scuttle”解决方案中的查询完全相同。
交点 (AND) 查询“书签+webservice+semweb”
SELECT b.*
FROM tagmap bt, bookmark b, tag t
WHERE bt.tag_id = t.tag_id
AND (t.name IN ('bookmark', 'webservice', 'semweb'))
AND b.id = bt.bookmark_id
GROUP BY b.id
HAVING COUNT( b.id )=3
联合 (OR) 查询“bookmark|webservice|semweb”
SELECT b.*
FROM tagmap bt, bookmark b, tag t
WHERE bt.tag_id = t.tag_id
AND (t.name IN ('bookmark', 'webservice', 'semweb'))
AND b.id = bt.bookmark_id
GROUP BY b.id
减号(排除) 查询“bookmark+webservice-semweb”,即:bookmark AND webservice AND NOT semweb。
SELECT b. *
FROM bookmark b, tagmap bt, tag t
WHERE b.id = bt.bookmark_id
AND bt.tag_id = t.tag_id
AND (t.name IN ('Programming', 'Algorithms'))
AND b.id NOT IN (SELECT b.id FROM bookmark b, tagmap bt, tag t WHERE b.id = bt.bookmark_id AND bt.tag_id = t.tag_id AND t.name = 'Python')
GROUP BY b.id
HAVING COUNT( b.id ) =2
省略 HAVING COUNT 会导致查询“bookmark|webservice-semweb”。
【讨论】:
CREATE TABLE Tags (
tag VARHAR(...) NOT NULL,
bid INT ... NOT NULL,
PRIMARY KEY(tag, bid),
INDEX(bid, tag)
)
注意事项:
AUTO_INCREMENT PK。因此,它比 Scuttle 更好。LIKE 带有 前导 通配符;子字符串的错误命中)相关讨论(针对 MySQL):
many:many mapping table optimization
ordered lists
【讨论】:
我想建议优化 MySQLicious 以获得更好的性能。 在此之前Toxi(3表)解决方案的缺点是
如果您有数百万个问题,并且每个问题有 5 个标签,那么 tagmap 表中将有 500 万个条目。因此,首先我们必须根据标签搜索过滤掉 10000 个 tagmap 条目,然后再次过滤掉这 10000 个匹配的问题。因此,如果艺术 id 是简单的数字,则过滤掉它是可以的,但如果它是一种 UUID(32 varchar),那么过滤掉需要更大的比较,尽管它已被索引。
我的解决方案:
每当创建新标签时,使用 counter++(以 10 为基数),并将该计数器转换为 base64。现在每个标签名称都将具有 base64 id。并将此 ID 与名称一起传递给 UI。 这样,您将拥有最多两个字符 ID,直到我们在系统中创建 4095 个标签。现在将这些多个标签连接到每个问题表标签列中。也添加分隔符并使其排序。
所以表格是这样的
查询时,查询 id 而不是真实的标签名称。
由于它是SORTED,标签上的and 条件将更有效(LIKE '%|a|%|c|%|f|%)。
请注意,单个空格分隔符是不够的,我们需要双分隔符来区分sql 和mysql 等标签,因为LIKE "%sql%" 也会返回mysql 结果。应该是LIKE "%|sql|%"
我知道搜索是非索引的,但您仍然可能已在与文章相关的其他列(如作者/日期时间)上建立索引,否则将导致全表扫描。
最后使用这个解决方案,不需要内部连接,在连接条件下必须将百万条记录与 500 万条记录进行比较。
【讨论】:
您提出的三表实现将适用于标记。
然而,堆栈溢出使用不同的实现。他们以纯文本形式将标签存储到帖子表中的 varchar 列,并使用全文索引来获取与标签匹配的帖子。例如posts.tags = "algorithm system tagging best-practices"。我确信 Jeff 曾在某处提到过这一点,但我忘记了在哪里。
【讨论】:
如果您的数据库支持可索引数组(例如 PostgreSQL),我会推荐一个完全非规范化的解决方案 - 将标签作为字符串数组存储在同一张表上。如果没有,将对象映射到标签的辅助表是最佳解决方案。如果您需要针对标签存储额外信息,您可以使用单独的标签表,但为每个标签查找引入第二个连接是没有意义的。
【讨论】:
您的三表解决方案没有问题。
另一种选择是限制可应用于文章的标签数量(如 SO 中的 5 个)并将其直接添加到您的文章表中。
规范化数据库有其优点和缺点,就像将事物硬连接到一个表中一样有优点和缺点。
没有什么说你不能两者兼得。重复信息与关系型数据库范式背道而驰,但如果目标是性能,您可能不得不打破这些范式。
【讨论】:
提出的解决方案是我能想到的解决标签和文章之间多对多关系的最佳方法——如果不是唯一可行的话。所以我的投票是“是的,它仍然是最好的”。不过,我会对任何替代品感兴趣。
【讨论】: