简单的查询优化(WHERE + ORDER + LIMIT)

时间:2014-12-11 16:32:41

标签: mysql sql query-optimization

我的查询速度令人难以置信地慢了(4分钟):

SELECT * FROM `ad` WHERE `ad`.`user_id` = USER_ID ORDER BY `ad`.`id` desc LIMIT 20;

广告表大约有1000万行。

SELECT COUNT(*) FROM `ad` WHERE `ad`.`user_id` = USER_ID;

返回10k行。

表格包含以下内容:

  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`,`status`,`sorttime`),

EXPLAIN提供了这个:

           id: 1
  select_type: SIMPLE
        table: ad
         type: index
possible_keys: idx_user_id
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 4249
        Extra: Using where

我无法理解为什么需要这么长时间?此查询也是由ORM(分页)生成的,因此从外部优化它可能会很好(可能会添加一些额外的索引)。

BTW此查询可以快速运行:

select aa.*
from (select id from ad where user_id=USER_ID order by id desc limit 20) as a
join ad as aa on a.id = aa.id ;

编辑:我尝试了另一个用户,其行数(数十个)比原始行少得多。我想知道为什么原始查询不会使用idx_user_id

EXPLAIN SELECT * FROM `ad` WHERE `ad`.`user_id` = ANOTHER_ID ORDER BY `ad`.`id` desc LIMIT 20;

           id: 1
  select_type: SIMPLE
        table: ad
         type: ref
possible_keys: idx_user_id
          **key: idx_user_id**
      key_len: 3
          ref: const
         rows: 84
        Extra: Using where; Using filesort

Edit2:在Alexander的帮助下我决定尝试强制MySQL使用我想要的索引,并且跟随查询要快得多(1秒而不是4分钟):

SELECT * 
FROM `ad` USE INDEX (idx_user_id)
WHERE `ad`.`user_id` = 1884774
ORDER BY `ad`.`id` desc LIMIT 20; 

1 个答案:

答案 0 :(得分:3)

EXPLAIN输出中,您可以看到key值为PRIMARY。这意味着MySQL优化器决定扫描所有表记录(已经按id排序)并使用特定user_id值搜索前20条记录比使用idx_user_id密钥更快,优化器将其视为可能的密钥然后被拒绝。

在第二个查询中,优化器发现子查询中只需要id个值,并决定使用idx_user_id索引,因为该索引允许计算必要的{{1}列表没有碰到桌子本身。然后通过主键值直接搜索只检索到20条记录,这对于少量记录来说是非常快速的操作。

当您使用id节目查询时,MySQL错误的决定是基于先前ANOTHER_ID值的行数。这个数字非常大,以至于优化器猜测它只会通过查看表记录本身并跳过错误USER_ID值的记录来更快地找到具有此特定user_id的前20条记录。

如果通过索引访问表行,则需要随机访问操作。对于典型的HDD,随机访问操作比顺序扫描慢大约100倍。因此,为了使索引有用,它必须将行数减少到总行数的1%以下。如果特定user_id值的行占总行数的1%以上,那么如果我们想要检索所有这些行,则执行全表扫描而不是使用索引可能更有效。但MySQL优化器没有考虑到只检索20行的事实。所以它错误地决定不使用索引而是进行全表扫描。

为了快速查询任何USER_ID值,您可以再添加一个索引,以便以最快的方式执行查询:

user_id

该索引允许MySQL进行过滤和排序。为此,应首先放置用于过滤的列,并将用于排序的列放在第二位。 MySQL应该足够聪明以使用该索引,因为该索引允许搜索所有必要的记录而不会跳过任何记录。