我希望你们中的一些是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个子查询的查询,如果表变大,这可能会导致速度变慢。
有没有人对如何优化上述内容有任何建议?有没有更好的方法来实现我想要的?我在上面做了任何明显的错误吗?
提前感谢您的帮助。
答案 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的索引,那么它的性能会更差。所以问题的一部分特别取决于C
和S
的相对大小,我目前只能猜测它。如果E
为1000且C
和S
各自为2-3,那么您将扫描8-21000行。如果E
为100万,而C
为2-3且S
为10-15,则您将扫描26-57百万行。如果E
为1百万且C
或S
大约为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等缓存结果来减少数据库负载。