1

我正在用 PHP/MySQL 编写一个网站,我想实现一个类似于 stackoverflow 标记引擎的功能。我在 DB 中有 3 个相关表:1. 项目 2. 标签 3. ItemTagMap(将标签映射到项目,n:n 映射)

现在,在搜索页面上,我想显示整个搜索结果(不仅仅是当前页面)的所有标签的不同列表,以便用户可以通过从该标签列表中添加/删除标签来“优化”他们的搜索。

问题是它是对数据库的一个相当繁重的查询,并且可能有大量的搜索请求导致不同的结果集和不同的标签集。

有谁知道如何有效地实施这一点?

4

3 回答 3

8

在我们进入过早优化模式之前,查看以下查询模板可能会很有用。如果不出意外,这可以用作衡量可能优化的有效性的基准。

SELECT T.Tagid, TagInfo.TagName,  COUNT(*)
FROM Items I
JOIN Tags TagInfo ON TagInfo.TagId = T.TagId
JOIN ItemTagMap T  ON I.ItemId = T.ItemId 
--JOIN ItemTagMap T1 ON I.ItemId = T1.ItemId
WHERE I.ItemId IN
  (
      SELECT ItemId 
      FROM Items
      WHERE   -- Some typical initial search criteria
         Title LIKE 'Bug Report%'   -- Or some fulltext filter instead...
         AND  ItemDate > '02/22/2008'
         AND  Status = 'C'
  )
--AND T1.TagId = 'MySql'
GROUP BY T.TagId, TagInfo.TagName
ORDER BY COUNT(*) DESC

子查询是“驱动查询”,即对应于最终用户的初始标准的查询。(see below for details on how this query, required multiple times may fit in an overall optimized flow) Commented is the JOIN on T1 (and possibly T2, T3, when several tags are selected), and, with the WHERE clause, the associated标准。当用户选择特定标签时需要这些,无论是作为初始搜索的一部分还是通过细化。(将这些连接和 where 子句放在子查询中可能更有效;下面将详细介绍)

讨论...... “驱动查询”或其变体需要用于两个不同的目的:

  • 1 提供枚举所有关联标签所需的 ItemId的完整列表。

  • 2提供前N个ItemId值(N为显示页面大小),用于在Item表中查找Item详细信息。

请注意,不需要对完整列表进行排序(或者它可能会受益于以不同的顺序排序),因此第二个列表需要根据用户的选择进行排序(例如按日期,降序或按标题,按字母顺序升序)。另请注意,如果需要任何排序顺序,查询的成本将意味着处理完整列表(SQL 本身的奇怪优化和/或一些非规范化,SQL 需要“查看”该列表中的最后一条记录,以防它们属于顶部,排序方式)。

后一个事实有利于为这两个目的使用相同的查询,相应的列表可以存储在临时表中。一般流程是快速查找前 N 项记录及其详细信息,并立即将其返回给应用程序。然后应用程序可以获取 ajax-fashion 的标签列表以进行细化。该列表将使用与上述类似的查询生成,其中子查询被“select * from temporaryTable”替换。SQL 优化器决定对这个列表进行排序(在某些情况下)的可能性很大,让我们让它这样做,而不是再次猜测它并显式排序。

要考虑的另一点是可能将 ItemTagMap 表上的连接带入“驱动查询”中,而不是如上所示。这样做可能是最好的,这既是为了性能,也是因为它会为#2 目的(显示一页项目)生成正确的列表。

上述查询/流程可能会很好地扩展,即使在相对适中的硬件上也是如此;暂时进入 1/2 百万+ 项,持续的用户搜索可能高达每秒 10 次。关键因素之一是初始搜索标准的选择性。

优化思路

  • [取决于典型的搜索案例和数据统计] 通过将某些项目的字段(实际上是复制)引入 ItemTagMap 表来进行非规范化可能是有意义的。特别是短字段可能在那里“受欢迎”。
  • 随着数据以百万以上的方式增长,我们可以利用一些标签通常具有很强的相关性(例如:在 SO 中,PHP 通常带有 MySql,顺便说一句,通常没有充分的理由......),并使用各种技巧。例如,“多标签”TagIds 的引入可能会使输入逻辑更加复杂,但也可以显着减小 Map 的大小。


——'不多说了!--
应根据实际需求和有效的数据统计概况选择合适的架构和优化...

于 2009-10-07T02:30:43.833 回答
0

假设:

  • 项目(id);
  • 带有名称索引的标签(id,名称);
  • ItemTag (item_id, tag_id)。

然后:

SELECT t.name
FROM Tag t
WHERE EXISTS (SELECT 1 FROM ItemTag WHERE item_id = 1234)
ORDER BY t.name

没有什么深入的。这很相似,但我的猜测是它会更慢:

SELECT t.name
FROM Tag t
WHERE t.id IN (SELECT tag_id FROM ItemTag WHERE item_id = 1234)
ORDER BY t.name

这也可以作为连接来完成:

SELECT DISTINCT t.name
FROM Tag t
JOIN ItemTag i WHERE i.tag_id = t.id
WHERE i.item_id = 1234
ORDER BY t.name

我认为第一个会更快,但与 SQL 一样,它值得测试(在足够大的数据集上)。

以上已经完成以列出单个项目的标签。您需要一组用于搜索结果的复合标签。从上面看这并不难,但这取决于您如何获得搜索结果。

于 2009-10-07T01:42:48.813 回答
0

您将希望尽量减少 DB 调用的数量,将繁重的工作放到 PHP 中。

首先,从数据库中选择所有项目:

select * from items where (conditions);

然后,从结果集中创建一个包含所有 id 的数组。

$ids = array();
foreach ($items as $item) {
    $ids[] = $item['id'];
}
$ids = implode(',' $ids);

然后为您之前检索的项目 ID 选择所有 ItemTagMap 和关联的标记数据。

select map.item_id, t.id, t.name from tags t, item_tag_maps map where t.id = map.tag_id and map.item_id in ($ids);

现在,当您遍历 $items 数组时,您可以从您执行的第二个 SQL 查询中找到所有匹配的标签,只要它具有匹配的 item_id 值。

于 2009-10-07T01:45:00.933 回答