优化MySQL搜索查询

时间:2013-03-26 12:52:36

标签: mysql query-optimization

需要你帮助优化一个mysql查询。让我们以简单的表格为例。

CREATE TABLE `Modules` (
 `ID` int(11) NOT NULL AUTO_INCREMENT,
 `moduleName` varchar(100) NOT NULL,
 `menuName` varchar(255) NOT NULL,
PRIMARY KEY (`ID`),
KEY `moduleName` (`moduleName`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

让我们填写一些数据:

INSERT INTO  `Modules` (`moduleName` ,`menuName`)
VALUES 
    ('abc1',  'name1'), 
    ('abc',  'name2'), 
    ('ddf',  'name3'), 
    ('ccc',  'name4'), 
    ('fer',  'name5');

还有一些示例字符串。设为abc_def;

传统上我们试图找到包含搜索字符串的所有行。

相反,我的任务是找到输入字符串中包含moduleName的所有行。现在我有以下查询来获得所需的结果:

SELECT `moduleName` ,`menuName` 
FROM `Modules` 
WHERE 'abc_def' LIKE(CONCAT(`moduleName`,'%'))

这将返回

moduleName   | menuName 
---------------------------
abc          | name2

问题是,此查询未使用索引。

有没有办法强迫它使用一个?

12 个答案:

答案 0 :(得分:11)

您似乎误解了什么是索引以及它如何有助于加快查询速度。

让我们来看看你的moduleName索引是什么。它基本上是从moduleName到ID的映射的排序列表。你在选择什么?

SELECT moduleName, menuName 
FROM Modules
WHERE 'abc_def' LIKE CONCAT(moduleName,'%');

那就是你想要每行有两个字段,这些字段与moduleName字段的某种映射值有某种关系。怎么能索引帮助你?没有完全匹配,并且没有办法利用我们对moduleNames进行排序的事实。

您需要利用索引来检查条件是否完全匹配:

SELECT moduleName, menuName 
FROM Modules
WHERE moduleName = LEFT('abc_def', LENGTH(moduleName));

现在我们确实有一个完全匹配,但由于条件的正确部分也取决于moduleName,因此将检查每一行的这个条件。因为在他的情况下,MySQL无法预测将匹配多少行,但它可以预测它将需要randon磁盘访问来获取每个匹配行的menuNames,MySQL将不会使用该索引。

所以你基本上有两种方法:

  1. 如果您知道条件会显着缩小匹配行的数量,那么您可以强制使用索引
  2. 另一种选择是将索引扩展到覆盖复合索引(moduleName, menuName),然后查询的所有结果将直接从索引中获取(即从内存中获取)。
  3. 方法#2(参见SQLfiddle)将通过简单查询获得索引命中,并且应该在更大的表上提供更好的性能。在小桌子上,我(,即lserni - 见评论)并不认为这是值得的。

答案 1 :(得分:7)

你实际上正在对该领域进行正则表达式,因此没有密钥可以正常工作。但是,在您的示例中,您可以使其更有效,因为匹配的每个moduleName必须小于或等于'abc_def',因此您可以添加:

and moduleName <= 'abc_def'

我能想到的唯一另一种选择是:

where modleName in ('a','ab','abc','abc_','abc_d','abc_de','abc_def')

不漂亮。

答案 2 :(得分:4)

尝试在问题中添加index hint

SELECT `moduleName` ,`menuName` 
FROM `Modules` USE INDEX (col1_index,col2_index) 
WHERE 'abc_def' LIKE(CONCAT(`moduleName`,'%'))

答案 3 :(得分:4)

因为,你的dtabase引擎是“InnoDB”     InnoDB中默认的所有用户数据都存储在包含B树索引的页面中

B-tree are good for following lookups:
● Exact full value (= xxx)
● Range of values (BETWEEN xx AND yy)
● Column prefix (LIKE 'xx%')
● Leftmost prefix

因此,对于您的查询,而不是使用索引或某些内容进行优化,    我们可以想到加快查询

您可以通过创建覆盖索引来加快查询速度。

覆盖索引是指all fields selected in a query are covered by an index的情况,在这种情况下是InnoDB(不是MyISAM)will never read the data in the table, but only use the data in the indexsignificantly speeding up the select。 请注意,在InnoDB中,主键包含在所有二级索引中,因此所有二级索引都是复合索引。 这意味着如果您在InnoDB上运行以下查询:

SELECT `moduleName` ,`menuName` 
FROM `Modules1` 
WHERE 'abc_def' LIKE(CONCAT(`moduleName`,'%'))

MySQL will always use a covering index and will not access the actual table

To believe, go to **Explain**

What does Explain statement mean?

table:表示输出受影响的表。

type:向我们显示正在使用的联接类型。从最好到最差     类型是:system,const,eq_ref,ref,range,index,all

possible_keys:表示MySQL可以选择哪些索引来查找此表中的行

key:表示MySQL实际决定使用的密钥(索引)。如果MySQL决定使用其中一个possible_keys索引来查找行,那么该索引将被列为键值。

key_len:这是所用密钥的长度。越短越好。

ref:使用哪个列(或常量)

rows: MySQL认为必须检查以执行查询的行数。

extra Extra info:在这里看到的不好的是“使用临时”和“使用filesort”

我有1,990行。

我的实验:

我会推荐Isern的where子句

的解决方案
    case 1) no indexes
explain select `moduleName` ,`menuName`  FROM `Modules1` WHERE moduleName = SUBSTRING('abc_def', 1, LENGTH(moduleName));
+----+-------------+----------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table    | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+----------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | Modules | ALL  | NULL          | NULL | NULL    | NULL | 2156 | Using where |
+----+-------------+----------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

创建覆盖索引的方法

case 2) ALTER TABLE `test`.`Modules1` ADD index `mod_name` (`moduleName`)

explain select `moduleName` ,`menuName`  FROM `Modules1` WHERE moduleName = SUBSTRING('abc_def', 1, LENGTH(moduleName));
+----+-------------+----------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table    | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+----------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | Modules | ALL  | NULL          | NULL | NULL    | NULL | 2156 | Using where |
+----+-------------+----------+------+---------------+------+---------+------+------+-------------+

这里显示正在使用的索引。请参阅列:键,额外

case 3) ALTER TABLE  `test`.`Modules1` DROP INDEX  `mod_name` ,
ADD INDEX  `mod_name` (  `moduleName` ,  `menuName` )

  explain select `moduleName` ,`menuName`  FROM `Modules1` WHERE moduleName = SUBSTRING('abc_def', 1, LENGTH(moduleName));
+----+-------------+----------+-------+---------------+----------+---------+------+------+--------------------------+
| id | select_type | table    | type  | possible_keys | key      | key_len | ref  | rows | Extra                    |
+----+-------------+----------+-------+---------------+----------+---------+------+------+--------------------------+
|  1 | SIMPLE      | Modules | index | NULL          | mod_name | 1069    | NULL | 2066 | Using where; Using index |
+----+-------------+----------+-------+---------------+----------+---------+------+------+--------------------------+
1 row in set (0.00 sec)


case 4) ALTER TABLE  `test`.`Modules1` DROP INDEX  `mod_name` ,
ADD INDEX  `mod_name` (  `ID` ,  `moduleName` ,  `menuName` )

  explain select `moduleName` ,`menuName`  FROM `Modules1` WHERE moduleName = SUBSTRING('abc_def', 1, LENGTH(moduleName));
+----+-------------+----------+-------+---------------+----------+---------+------+------+--------------------------+
| id | select_type | table    | type  | possible_keys | key      | key_len | ref  | rows | Extra                    |
+----+-------------+----------+-------+---------------+----------+---------+------+------+--------------------------+
|  1 | SIMPLE      | Modules | index | NULL          | mod_name | 1073    | NULL | 2061 | Using where; Using index |
+----+-------------+----------+-------+---------------+----------+---------+------+------+--------------------------+
1 row in set (0.00 sec)

编辑:

use where moduleName regexp "^(a|ab|abc|abc_|abc_d|abc_de|abc_def)$";
in place  of substring()

答案 4 :(得分:3)

  

DECLARE @SEARCHING_TEXT AS VARCHAR(500)

     

SET @SEARCHING_TEXT ='ab'

     

SELECT'modumentName','menuName'FROM [MODULES] WHERE FREETEXT   (MODULENAME,@ SEARCHING_TEXT);

答案 5 :(得分:3)

我不确定这是一个不错的查询,但它使用了索引:

SELECT `moduleName` ,`menuName`
FROM `Modules` WHERE LEFT('abc_def', 7) = `moduleName`
UNION ALL
SELECT `moduleName` ,`menuName`
FROM `Modules` WHERE LEFT('abc_def', 6) = `moduleName`
UNION ALL
SELECT `moduleName` ,`menuName`
FROM `Modules` WHERE LEFT('abc_def', 5) = `moduleName`
UNION ALL
SELECT `moduleName` ,`menuName`
FROM `Modules` WHERE LEFT('abc_def', 4) = `moduleName`
UNION ALL
SELECT `moduleName` ,`menuName`
FROM `Modules` WHERE LEFT('abc_def', 3) = `moduleName`
UNION ALL
SELECT `moduleName` ,`menuName`
FROM `Modules` WHERE LEFT('abc_def', 2) = `moduleName`
UNION ALL
SELECT `moduleName` ,`menuName`
FROM `Modules` WHERE LEFT('abc_def', 1) = `moduleName`

一般解决方案

这是一个通用的解决方案,使用动态查询:

SET @search='abc_def';

SELECT
  CONCAT(
    'SELECT `moduleName` ,`menuName` FROM `Modules` WHERE ',
    GROUP_CONCAT(
      CONCAT(
        'moduleName=\'',
        LEFT(@search, ln),
        '\'') SEPARATOR ' OR ')
    )
FROM
  (SELECT DISTINCT LENGTH(moduleName) ln
   FROM Modules
   WHERE LENGTH(moduleName)<=LENGTH(@search)) s
INTO @sql;

这将创建一个带有条件WHERE moduleName='abc' OR moduleName='abc_' OR ...的SQL查询的字符串,它应该能够快速创建字符串(如果没有,可以使用临时索引表进行大量改进)数字从1到字符串的最大允许长度,例如小提琴中的例子)。然后你可以执行查询:

PREPARE stmt FROM @sql;
EXECUTE stmt;

请参阅小提琴here

答案 6 :(得分:3)

我的回答可能更复杂

alter table Modules add column name_index int
alter table Modules add index name_integer_index(name_index);

当您插入模块表时,您计算moduleName的int值,类似于select ascii('a')

运行查询时,只需运行

即可
SELECT `moduleName`, `menuName`
FROM   `Modules`
WHERE  name_index >
  (select ascii('a')) and name_index < (select ascii('abc_def'))

它将使用name_integr_index

答案 7 :(得分:3)

类似于fthiella的建议,但更灵活(因为它可以轻松应对更长的字符串): -

SELECT DISTINCT `moduleName` ,`menuName`
FROM `Modules`
CROSS JOIN (SELECT a.i + b.i * 10 + c.i * 100 + 1 AS anInt FROM integers a, integers b, integers c) Sub1
WHERE LEFT('abc_def', Sub1.anInt) = `moduleName`

这个(作为类型)处理长达1000个字符的字符串,但比fthiellas解决方案慢。可以很容易地减少长达100个字符的字符串,此时它似乎比fthiellas解决方案略快。

检查它的长度确实加快了一点: -

SELECT SQL_NO_CACHE  DISTINCT `moduleName` ,`menuName`
FROM `Modules`
INNER JOIN (SELECT a.i + b.i * 10 + c.i * 100 + 1 AS anInt FROM integers a, integers b, integers c ) Sub1
ON Sub1.anInt <= LENGTH('abc_def') AND Sub1.anInt <= LENGTH(`moduleName`)
WHERE LEFT('abc_def', Sub1.anInt) = `moduleName`

或稍作修改,以便从子选择中带回可能的子串: -

SELECT SQL_NO_CACHE  DISTINCT `moduleName` ,`menuName`
FROM `Modules`
CROSS JOIN (SELECT DISTINCT LEFT('abc_def', a.i + b.i * 10 + c.i * 100 + 1) AS aStart FROM integers a, integers b, integers c WHERE( a.i + b.i * 10 + c.i * 100 + 1) <= LENGTH('abc_def')) Sub1
WHERE aStart = `moduleName`

请注意,这些解决方案取决于具有单个列的整数表和值为0到9的行。

答案 8 :(得分:3)

类似查询不使用索引...但您也可以定义一个全文索引来搜索这样的字符串。但是innodb引擎不支持它,只有myisam支持它。

答案 9 :(得分:3)

将索引键添加到moduleName 检查http://dev.mysql.com/doc/refman/5.0/en/mysql-indexes.html B树索引特征更多信息

不确定为什么使用LIKE,最好避免使用LIKE。我的建议是将所有行保存在JSON中,然后对其执行AJAX搜索。

答案 10 :(得分:3)

(前一部分答案已删除 - 请参阅新手的答案,但答案相同,但更好)

newtover的方法#2(请参阅SQLfiddle )会通过简单的查询获得索引,并且应该在较长的表格上提供更好的表现:

SELECT `moduleName`, `menuName` 
FROM `Modules` 
WHERE moduleName = LEFT('abc_def', LENGTH(moduleName));

如果您需要来自众多专栏的数据(而不只是menuName),即如果Modules更大也更长,那么您可能会更好将moduleName移动到仅包含IDmoduleName及其长度的查找表中(以保存一个函数调用)。

所需的实际额外空间很小,如果moduleName的基数较低,即很少moduleName次重复menuName次,您实际上可能最终节省可观的空间

新架构将是:

moduleName_id    integer, keys to Lookup.id
...all the fields in Modules except moduleName...


Lookup table
   id            primary key
   moduleName    varchar
   moduleLength  integer

和查询:

SELECT `Lookup`.`moduleName`,`menuName` 
FROM `Modules` INNER JOIN `Lookup`
    ON (`Modules`.`moduleName_id` = Lookup.id)
WHERE `Lookup`.`moduleName` = LEFT('abc_def',
         `Lookup`.`moduleLength`);

SQLfiddle从您的架构开始并修改它以实现上述目标。速度和存储空间的改进很大程度上取决于您在表中添加的数据。我故意让自己处于最佳状态(模块中的许多短字段,每个menuName平均有一百moduleName个},并且能够节省大约30%的存储空间;搜索性能只有3倍左右,并且可能受到I / O缓存的影响,所以除非有人进行更彻底的测试,否则我会把它留在“可观的空间并节省时间”。

另一方面,在小而简单的桌子和相同数量的菜单和模块(即1:1)上,存在轻微的存储损失,没有明显的速度增益。在这种情况下,所涉及的空间和时间将非常小,因此,尽管增加了复杂性,上面的“规范化”形式可能仍然是可行的方式。

答案 11 :(得分:0)

我们可以用一个函数本身instead实现2个函数作为SUBSTRING('abcdef',1,LENGTH(moduleName))

where locate(moduleName, 'abc_def');