这个查询出了什么问题? EXPLAIN对我来说很好看

时间:2009-02-20 01:47:00

标签: mysql optimization performance

我正在浏览一个应用程序,并试图优化一些查询,我真的在努力解决其中的一些问题。这是一个例子:

SELECT `Item` . * , `Source` . * , `Keyword` . * , `Author` . *
FROM `items` AS `Item`
JOIN `sources` AS `Source` ON ( `Item`.`source_id` = `Source`.`id` )
JOIN `authors` AS `Author` ON ( `Item`.`author_id` = `Author`.`id` )
JOIN `items_keywords` AS `ItemsKeyword` ON ( `Item`.`id` = `ItemsKeyword`.`item_id` )
JOIN `keywords` AS `Keyword` ON ( `Keyword`.`id` = `ItemsKeyword`.`keyword_id` )
JOIN `keywords_profiles` AS `KeywordsProfile` ON ( `Keyword`.`id` = `KeywordsProfile`.`keyword_id` )
JOIN `profiles` AS `Profile` ON ( `Profile`.`id` = `KeywordsProfile`.`profile_id` )
WHERE `KeywordsProfile`.`profile_id` IN ( 17 )
GROUP BY `Item`.`id`
ORDER BY `Item`.`timestamp` DESC , `Item`.`id` DESC
LIMIT 0 , 20;

这个花了10-30秒......在引用的表中,大约有500k作者行,大约750k项和items_keywords行。其他一切都少于500行。

这是解释输出: http://img.skitch.com/20090220-fb52wd7jf58x41ikfxaws96xjn.jpg

EXPLAIN对我来说相对较新,但我逐行完成了这一切,看起来一切都很好。不知道我还能做什么,因为我已经掌握了所有内容的索引......我缺少什么?

这个服务器在slicehost上只有256个切片,但是没有其它的东西在它上运行,CPU在运行之前是0%。然而它仍然在这个查询上。有什么想法吗?

编辑:一些进一步的信息;使这真令人沮丧的一件事是,如果我反复运行此查询,则需要不到0.1秒。我假设这是由于查询缓存,但如果我在它之前运行RESET QUERY CACHE,它仍然运行得非常快。只有在我等待一段时间或运行其他一些查询后才会返回10-30秒。所有的表都是MyISAM ...这是否表明MySQL正在将内容加载到内存中,这就是为什么它运行一段时间这么快?

编辑2:非常感谢大家的帮助......更新......我把所有内容都归结为:

SELECT i.id
FROM items AS i
ORDER BY i.timestamp DESC, i.id DESC
LIMIT 0, 20;

尽管数据库中只有750k记录,但仍然持续了5-6秒。一旦我在ORDER BY子句上删除了第二列,它就很快了。显然有几件事情在这里发生,但当我把查询切换到这个时:

SELECT i.id
FROM items AS i
JOIN items_keywords AS ik ON ( i.id = ik.item_id )
JOIN keywords AS k ON ( k.id = ik.keyword_id )
JOIN keywords_profiles AS kp ON ( k.id = kp.keyword_id )
WHERE kp.profile_id IN (139)
ORDER BY i.timestamp DESC
LIMIT 20;

它还需要10秒以上......我还能做些什么?

小小的好奇心:在解释中,items_keywords的rows列总是1544,无论我在查询中使用什么profile_id。它不应该根据与该配置文件关联的项目数量而改变吗?

编辑3:好的,这太荒谬了:)。如果我完全删除ORDER BY子句,事情就会非常迅速,临时表/ filesort会从explain中消失。 item.timestamp列目前有一个索引,但是由于某些原因它没有被使用?我以为我记得有关mysql的东西只使用每个表或某个索引?我应该在这个查询引用的items表的所有列上创建一个多列索引(source_id,author_id,timestamp等)?

13 个答案:

答案 0 :(得分:3)

看起来没问题,explain中的每一行都使用某种索引。一个可能的担心是filesort位。尝试在没有order by子句的情况下运行查询,看看是否能改善它。

然后,我会做的是逐渐取出每个join,直到你(希望)获得大幅度的速度提升,然后专注于为什么会发生这种情况。

我提到filesort的原因是因为我无法在explain输出中的任何地方看到时间戳的提及(即使它是您的主要排序标准) - 它可能需要完整的非 - 索引排序。

更新#1:

基于编辑#2,查询:

SELECT i.id
    FROM items AS i
    ORDER BY i.timestamp DESC, i.id DESC
    LIMIT 0, 20;

需要5-6秒。这是令人憎恶的。尝试在TIMESTAMPID上创建综合索引,看看是否能改善它:

create index timestamp_id on items(timestamp,id);
select id from items order by timestamp desc,id desc limit 0,20;
select id from items order by timestamp,id limit 0,20;
select id from items order by timestamp desc,id desc;
select id from items order by timestamp,id;

在其中一个测试中,我已经离开了降序位(DB2 for one有时不使用索引,如果它们的顺序相反)。另一个变化是在影响它的情况下取消限制。

答案 1 :(得分:3)

试试这个并看看它是如何做的:

SELECT i.*, s.*, k.*, a.*
FROM items AS i
 JOIN sources AS s ON (i.source_id = s.id)
 JOIN authors AS a ON (i.author_id = a.id)
 JOIN items_keywords AS ik ON (i.id = ik.item_id)
 JOIN keywords AS k ON (k.id = ik.keyword_id)
WHERE k.id IN (SELECT kp.keyword_id
           FROM keywords_profiles AS kp
           WHERE kp.profile_id IN (17))
ORDER BY i.timestamp DESC, i.id DESC
LIMIT 0, 20;

我将一些连接分解为非相关子查询,因此您不必执行GROUP BY将结果映射到不同的行。

实际上,在我的示例中,您仍然可以获得每行i.id多行,具体取决于映射到给定项目的关键字数量以及profile_id 17。

您的EXPLAIN报告中报告的filesort可能是由于GROUP BYORDER BY使用不同字段的组合。

我同意@ʞɔıu的答案,即加速可能是因为关键缓存。

答案 2 :(得分:1)

要使查询快速运行,您需要:

  1. 创建索引:CREATE INDEX ix_timestamp_id ON items (timestamp, id)

    • 确保idsourcesauthors上的keywords为主键。
    • 强制MySQL将此索引用于项目,并为其他项目执行NESTED LOOP联接:

      EXPLAIN EXTENDED 选择Item。*,Source。 *,Keyword。 *,Author。 * FROM items AS Item FORCE INDEX FOR ORDER BY(ix_timestamp_id) 加入items_keywords AS ItemsKeyword FORCE INDEX(ix_item_keyword)ON(Itemid = ItemsKeyworditem_idItemsKeyword。{ {1}} IN   (   选择keyword_id   FROM keyword_id AS keywords_profiles FORCE INDEX(ix_keyword_profile)   在KeywordsProfileKeywordsProfile = 17   ) ) 加入profile_id AS sources FORCE INDEX(主要)ON(SourceItem = source_idSource) 加入id AS authors FORCE INDEX(主要)ON(AuthorItem = author_idAuthor) 加入id AS keywords FORCE INDEX(主要)ON(KeywordKeyword = idItemsKeyword) ORDER BY keyword_id。timestamp DESC,Item。id DESC LIMIT 0,20

  2. 如您所见,我们摆脱Item,将子查询推入GROUP BY条件并强制JOIN用于连接。

    这就是我们如何确保PRIMARY KEY NESTED LOOPS作为主要表格将用于所有联接。

    结果:

    1, 'PRIMARY', 'Item',         'index',  '', 'ix_timestamp_id', '12', '', 20, 2622845.00, ''
    1, 'PRIMARY', 'Author',       'eq_ref', 'PRIMARY', 'PRIMARY', '4', 'test.Item.author_id', 1, 100.00, ''
    1, 'PRIMARY', 'Source',       'eq_ref', 'PRIMARY', 'PRIMARY', '4', 'test.Item.source_id', 1, 100.00, ''
    1, 'PRIMARY', 'ItemsKeyword', 'ref', 'PRIMARY', 'PRIMARY', '4', 'test.Item.id', 1, 100.00, 'Using where; Using index'
    1, 'PRIMARY', 'Keyword',      'eq_ref', 'PRIMARY', 'PRIMARY', '4', 'test.ItemsKeyword.keyword_id', 1, 100.00, ''
    2, 'DEPENDENT SUBQUERY',      'KeywordsProfile', 'unique_subquery', 'PRIMARY', 'PRIMARY', '8', 'func,const', 1, 100.00, 'Using index; Using where'
    

    ,当我们运行时,我们得到

    items

    20 rows fetched in 0,0038s (0,0019s) 中有500kitems中有600kitems_keywords中的512值和keywords中的512值{1}}(全部包含个人资料keywords_profiles)。

答案 3 :(得分:0)

我建议您在查询上运行一个分析器,然后您可以看到每个子查询花了多长时间以及消耗时间的位置。如果你有phpmyadmin,这是一个简单的chekbox你需要检查以获得这个功能,但我猜你也可以从mysql终端应用程序手动获取它。我之前没有看过这个解释的事情,如果它实际上是我在phpmyadmin习惯的分析我为废话道歉。

答案 4 :(得分:0)

GROUP BY子句实现了什么? SELECT中没有聚合函数,因此GROUP BY应该是不必要的

答案 5 :(得分:0)

有些事情要尝试:

  1. 尽量不从所有表中选择所有列,并仅选择所需的列。这可能会妨碍使用覆盖索引(在额外列中查找使用索引),并且通常会吸收大量不必要的IO。
  2. 那个文件看起来有点令人不安。尝试删除顺序并将其替换为null by order - 在mysql中隐式排序,因此您必须按null排序以删除该隐式排序。
  3. 尝试在item(timestamp,id)或(id,timestamp)上添加索引。可能会对该文件进行一些操作(你永远不知道)。
  4. 为什么要按商品ID进行分组?而不是选择任何聚合列?如果按列分组,然后选择(更少排序)其他一些非聚合列,那么这些列的值将或多或少地被选中。除非,对于此查询,item id始终是唯一的,在这种情况下,group by将无法完成任何操作。
  5. 最后,根据我的经验,如果你给它太多的连接以试图优化,mysql有时会莫名其妙地吓坏了。尝试并弄清楚是否有某种方式你不必像这样做那么多连接,即如果你可以将它分成多个查询。
  6. 让这真令人沮丧的一件事是,如果我反复运行此查询,则需要不到0.1秒。我假设这是由于查询缓存 - 在SELECT关键字后添加SQL_NO_CACHE以禁用每个查询使用查询缓存
  7. 所有的表都是MyISAM ...这表明MySQL正在将内容加载到内存中,这就是为什么它运行得如此之快一段时间 - MyISAM使用密钥缓冲区并且只缓存索引数据内存,并依赖于操作系统,希望缓存非索引数据。与Innodb不同,后者将缓存池中的所有内容缓存。

答案 6 :(得分:0)

由于文件系统I / O,您是否可能遇到问题? EXPLAIN显示必须从ItemsKeyword表中提取1544行。如果你必须为每个那些转到磁盘,你将在运行时添加大约10-15秒的总时间(假设因为你在VM上,所以需要时间很长)。通常情况下,表会缓存在RAM中,或者数据存储在磁盘上足够近,可以组合读取。但是,您运行的是具有256MB RAM的VM,因此您可能没有可以缓存的内存,如果您的表文件足够碎片,您可能会使查询性能降低这么多。

您可以通过在服务器上的另一个shell中运行pidstat -d 1iostat 1之类的内容来了解​​查询期间I / O的情况。

编辑: 通过查看在(ItemsKeyworditem_id上添加索引的查询,ItemsKeywordkeyword_id)如果我的理论是正确的,那么它应该解决它对于ItemsKeyword表。

答案 7 :(得分:0)

MySQL将大量内容加载到不同的缓存中,包括索引和查询。此外,您的操作系统将保留文件系统缓存,以便在重复执行时加快查询速度。

要考虑的一件事是MySQL如何在此类查询期间创建临时表。正如您在解释中所看到的,正在创建一个临时表,可能用于排序结果。通常,MySQL会在内存中创建这些临时表,除了2个条件。首先,如果它们超过了MySQL设置中设置的最大大小(最大临时表大小或堆大小 - 请查看mysqlperformanceblogs.com以获取有关这些设置的更多信息)。第二个也是更重要的一个是:

  • 在查询中选择文本或blob表时,将始终在磁盘上创建临时表。

这可能会造成重大的性能损失,如果您的服务器正在采取任何措施,甚至会导致I / O瓶颈。

检查您的任何列是否属于此数据类型。如果是,您可以尝试重写查询,以便不创建临时表(我认为分组总是会导致它们),或者尝试不选择这些。另一种策略是将其分解为几个可能在很短的时间内执行的较小查询。

祝你好运!

答案 8 :(得分:0)

我可能完全错了,但是当你改变

时会发生什么
WHERE kp.profile_id IN (139)

WHERE kp.profile_id = 139

答案 9 :(得分:0)

试试这个:

SELECT i.id
FROM ((items AS i
   INNER JOIN items_keywords AS ik ON ( i.id = ik.item_id ))
   INNER JOIN keywords AS k ON ( k.id = ik.keyword_id ))
   INNER JOIN keywords_profiles AS kp ON ( k.id = kp.keyword_id AND kp.profile_id = 139)
ORDER BY i.timestamp DESC
LIMIT 20;

答案 10 :(得分:0)

查看问题评论中的pastie.org链接:

  • 您正在加入items.source_id int(4)加入sources.id int(16)
  • items.id int(16)itemskeywords.item_id int(11)

在这些情况下,我看不出两个字段有不同宽度的任何充分理由

我意识到这些只是显示宽度,列可以存储的实际数字范围仅由INT部分确定,但MySQL 6.0 reference manual表示:

  

请注意,如果存储较大的值   比整数中的显示宽度   专栏,您可能会遇到问题   当MySQL生成临时表时   对于一些复杂的连接,因为在   这些案例MySQL假设   数据适合原始列   宽度。

从您引用的粗略数字来看,您看起来并没有超出任何ID列的显示宽度。你也可以整理这些不一致的方法,只是为了消除另一个可能的错误。

如果您不需要显示宽度,也可以完全删除显示宽度

修改

我猜想数据库的原始作者可能认为int(4)意味着“一个最多4位数的整数”,而它实际上意味着“-2147483648和2147482647之间的整数显示为至少4如果需要“

,则用空格填充左边的字符

authors.refreshed int(20)items.timestamp int(30)这样的定义并没有多大意义,因为int只能有10个数字加上符号。即使bigint也不能超过20个字符。也许原作者认为int(4)类似于varchar(4)

答案 11 :(得分:0)

尝试表格的备份副本。之后将原始表重命名为其他表,将新表重命名为原始表,然后再次使用新旧命名的表...

或者您可以尝试修复表格,但这并不总是有帮助。

编辑:伙计,这是一个老问题......

答案 12 :(得分:-1)

问题似乎是它甚至在尝试执行where子句之前必须在每个表上进行完全连接。这可能导致每个表500k行,你正在查看它在内存中填充的数百万行。我会尝试将JOINS更改为LEFT JOINS USING()。