有什么我可以做的来优化这个mysql查询?

时间:2010-11-13 19:23:32

标签: mysql performance subquery left-join

我希望你们中的一些是mysql专家可以帮助我优化我的mysql搜索查询......

首先,一些背景知识:

我正在开发一个具有搜索功能的小型练习mysql应用程序。

数据库中的每个练习都可以属于任意数量的嵌套类别,每个练习也可以有任意数量的搜索标签。

这是我的数据结构(为便于阅读而简化)

TABLE exercises
  ID
  title

TABLE searchtags
  ID
  title

TABLE exerciseSearchtags
  exerciseID -> exercises.ID
  searchtagID -> searchtags.ID

TABLE categories
  ID
  parentID -> ID
  title

TABLE exerciseCategories
  exerciseID -> exercises.ID
  categoryID -> categories.ID

所有表格都是InnoDB(没有全文搜索)。

练习,搜索标签和类别的ID列已编入索引。

“exerciseSearchtags”和“exerciseCategories”是分别表达练习和搜索标签,练习和类别之间关系的多对多连接表。 exerciseID& searchtagID列已在exerciseSearchtags中编入索引,而exerciseID和categoryID列都已在exerciseCategories中编入索引。

以下是一些练习题,类别标题和搜索标题标题数据的示例。所有三种类型的标题中都可以有多个单词。

Exercises
    (ID - title)
    1 - Concentric Shoulder Internal Rotation in Prone
    2 - Straight Leg Raise Dural Mobility (Sural)
    3 - Push-Ups 

 Categories
    (ID - title)
    1 - Flexion
    2 - Muscles of Mastication
    3 - Lumbar Plexus

 Searchtags
    (ID - title)
    1 - Active Range of Motion
    2 - Overhead Press
    3 - Impingement

现在,转到搜索查询:

搜索引擎接受任意数量的用户输入关键字。

我想根据关键字/类别标题匹配,关键字/搜索标题匹配以及关键字/练习标题匹配的数量对搜索结果进行排名。

为实现此目的,我使用以下动态生成的SQL:

  SELECT 
   exercises.ID AS ID,
   exercises.title AS title, 
   (

    // for each keyword, the following 
    // 3 subqueries are generated

    (
     SELECT COUNT(1) 
     FROM categories 
     LEFT JOIN exerciseCategories 
     ON exerciseCategories.categoryID = categories.ID 
     WHERE categories.title RLIKE CONCAT('[[:<:]]',?) 
     AND exerciseCategories.exerciseID = exercises.ID
    ) + 

    (
     SELECT COUNT(1) 
     FROM searchtags 
     LEFT JOIN exerciseSearchtags 
     ON exerciseSearchtags.searchtagID = searchtags.ID 
     WHERE searchtags.title RLIKE CONCAT('[[:<:]]',?) 
     AND exerciseSearchtags.exerciseID = exercises.ID
    ) +

    (
     SELECT COUNT(1) 
     FROM exercises AS exercises2 
     WHERE exercises2.title RLIKE CONCAT('[[:<:]]',?) 
     AND exercises2.ID = exercises.ID
    )

    // end subqueries

    ) AS relevance

    FROM 
    exercises

    LEFT JOIN exerciseCategories
      ON exerciseCategories.exerciseID = exercises.ID 

    LEFT JOIN categories
     ON categories.ID = exerciseCategories.categoryID

    LEFT JOIN exerciseSearchtags
     ON exerciseSearchtags.exerciseID = exercises.ID 

    LEFT JOIN searchtags
     ON searchtags.ID = exerciseSearchtags.searchtagID

    WHERE

    // for each keyword, the following 
    // 3 conditions are generated

    categories.title RLIKE CONCAT('[[:<:]]',?) OR 
    exercises.title RLIKE CONCAT('[[:<:]]',?) OR 
    searchtags.title RLIKE CONCAT('[[:<:]]',?) 

    // end conditions

    GROUP BY 
     exercises.ID

    ORDER BY
     relevance DESC

    LIMIT 
       $start, $results 

所有这一切都很好。它根据用户输入返回相关的搜索结果。

但是,我担心我的解决方案可能无法很好地扩展。例如,如果用户输入七个关键字搜索字符串,则会导致相关性计算中包含21个子查询的查询,如果表变大,这可能会导致速度变慢。

有没有人对如何优化上述内容有任何建议?有没有更好的方法来实现我想要的?我在上面做了任何明显的错误吗?

提前感谢您的帮助。

2 个答案:

答案 0 :(得分:3)

如果您还提供了一些数据,特别是一些示例关键字和每个表中的title示例,我可能会提供更好的答案,这样我们就可以了解您要尝试的内容实际上匹配。但我会尝试回答你所提供的内容。

首先让我用英语说出我认为你的查询会做什么,然后我会分解原因以及修复方法。

Perform a full table scan of all instances of `exercises`
  For each row in `exercises`
    Find all categories attached via exerciseCategories
      For each combination of exercise and category
        Perform a full table scan of all instances of exerciseCategories
          Look up corresponding category
            Perform RLIKE match on title
        Perform a full table scan of all instances of exerciseSearchtags      
          Look up corresponding searchtag
            Perform RLIKE match on title
        Join back to exercises table to re-lookup self
            Perform RLIKE match on title

假设您至少有一些合理的索引,这将是E x C x (C + S + 1),其中E是练习的数量,C是a的平均分类数给定练习,S是给定练习的平均搜索标签数。如果您至少没有列出您列出的ID的索引,那么它的性能会更差。所以问题的一部分特别取决于CS的相对大小,我目前只能猜测它。如果E为1000且CS各自为2-3,那么您将扫描8-21000行。如果E为100万,而C为2-3且S为10-15,则您将扫描26-57百万行。如果E为1百万且CS大约为1000,那么您将扫描超过1万亿行。所以不,这根本不会很好地扩展。

1)忽略子查询中的LEFT JOIN,因为这些相同查询的WERE子句强制它们是正常的JOIN。这不会对性能产生太大影响,但会使您的意图模糊不清。

2)RLIKE(及其别名REGEXP)不会使用索引AFAIK,因此它们不会扩展。我只能在没有样本数据的情况下猜测,但我会说,如果您的搜索需要匹配字边界,那么您需要对数据进行规范化。即使你的标题看起来像是存储的自然字符串,搜索它们中的一部分意味着你真的将它们视为一组单词。因此,您应该使用mysql的全文搜索capabilities,否则您应该将标题分解为每行存储一个单词的单独表。每个单词一行显然会增加你的存储空间,但是你的查询几乎是微不足道的,因为你似乎只是在做整个单词匹配(而不是类似的单词,词根等)。

3)你所拥有的最后左联盟是导致我的公式的E x C部分的原因,你将为每次练习做同样的工作C次。现在,诚然,在大多数查询计划下,子查询将被缓存为每个类别,因此它实际上并不像我建议的那么糟糕,但在每种情况下都不会这样,所以我给你最糟糕的情况。即使你可以验证你有适当的索引并且查询优化器已经避免了所有这些额外的表扫描,你仍然会返回大量的冗余数据,因为你的结果看起来像这样:

Exercise 1 info
Exercise 1 info
Exercise 1 info
Exercise 2 info
Exercise 2 info
Exercise 2 info
etc

因为每个exercisecategory条目的每个练习行都是重复的,即使你没有从exercisecategory或类别返回任何内容(并且你的第一个子查询中的categories.ID实际上是引用在该子查询中加入的类别而不是从外部引用的类别查询)。

4)由于大多数搜索引擎使用分页返回结果,我猜你只需要第一个X结果。在查询中添加LIMIT X,或者更好的是LIMIT Y,X,其中Y是当前页面,X是每页返回的结果数,如果搜索关键字返回大量结果,将极大地帮助优化查询。

如果您可以向我们提供有关您数据的更多信息,我可以更新我的回答以反映这一点。

更新

根据您的回复,这是我建议的查询。遗憾的是,如果没有全文搜索或索引字词,如果您的类别表或搜索标记表非常大,仍会出现缩放问题。

 SELECT exercises.ID AS ID,
        exercises.title AS title,

        IF(exercises.title RLIKE CONCAT('[[:<:]]',?), 1, 0)
        +
        (SELECT COUNT(*)
           FROM categories
           JOIN exerciseCategories ON exerciseCategories.categoryID = categories.ID
          WHERE exerciseCategories.exerciseID = exercises.ID
            AND categories.title RLIKE CONCAT('[[:<:]]',?))
        +
        (SELECT COUNT(*)
           FROM searchtags
           JOIN exerciseSearchtags ON exerciseSearchtags.searchtagID = searchtags.ID
          WHERE exerciseSearchtags.exerciseID = exercises.ID
            AND searchtags.title RLIKE CONCAT('[[:<:]]',?))

   FROM exercises

ORDER BY related DESC      具有相关性&gt; 0       LIMIT $ start,$ results

我通常不会推荐一个HAVING条款,但它不会比你的RLIKE更糟糕......或者RLIKE ......等等。

这解决了我的问题#1,#3,#4但仍留下#2。鉴于您的示例数据,我认为每个表最多只有几十个条目。在这种情况下,RLIKE的低效率可能不足以值得每行一个单词的优化,但你确实询问了缩放。只有完全相等(title = ?)查询或以查询(title LIKE 'foo%')开头才能使用索引,如果要扩展任何表中的行,这些索引是绝对必要的。 RLIKE和REGEXP不符合这些标准,无论使用正则表达式(并且你的是'包含'类似查询,这是最坏的情况)。 (重要的是要注意title LIKE CONCAT(?, '%')不够好,因为mysql认为它必须计算某些东西并忽略它的索引。你需要在应用程序中添加'%'。)

答案 1 :(得分:1)

尝试运行查询的解释计划,并查看当前不使用索引的行。为这些行策略性地添加索引。

另外,如果可能的话,减少查询中的RLIKE调用次数,因为这些调用很昂贵。

考虑使用数据库前面的memcached等缓存结果来减少数据库负载。