12

我们的团队上周刚刚调试并试图找到许多 mysql 锁定超时和许多运行时间极长的查询的根源。最后,这个查询似乎是罪魁祸首。

mysql> explain 

SELECT categories.name AS cat_name, 
COUNT(distinct items.id) AS category_count 
FROM `items` 
INNER JOIN `categories` ON `categories`.`id` = `items`.`category_id` 
WHERE `items`.`state` IN ('listed', 'reserved') 
   AND (items.category_id IS NOT NULL) 
GROUP BY categories.name 
ORDER BY category_count DESC 
LIMIT 10\G

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: items
         type: range
possible_keys: index_items_on_category_id,index_items_on_state
          key: index_items_on_category_id
      key_len: 5
          ref: NULL
         rows: 119371
        Extra: Using where; Using temporary; Using filesort
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: categories
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: production_db.items.category_id
         rows: 1
        Extra: 
2 rows in set (0.00 sec)

我可以看到它正在执行令人讨厌的表扫描并创建一个临时表来运行。

为什么这个查询会导致数据库响应时间增加十倍,而一些通常需要 40-50 毫秒(项目表更新)的查询有时会爆炸到 50,000 毫秒甚至更高?

4

1 回答 1

5

如果没有更多信息,很难说

  1. 那是在事务中运行吗?
  2. 如果是这样,隔离级别是多少?
  3. 有多少个类别?
  4. 有多少项?

我的猜测是查询太慢并且它在事务中运行(这可能是因为你有这个问题)并且可能在 items 表上发出范围锁,它不允许写入继续,因此减慢更新直到他们可以锁在桌子上。

根据我从您的查询和执行计划中看到的内容,我有几条评论:

1)您的 items.state作为目录可能更好,而不是在 items 的每一行上都有字符串,这是为了空间效率,并且比较 ID 比比较字符串快得多(不管引擎可能做的任何优化)。

2)我猜 items.state 是一个基数低的列(很少有唯一值),因此该列中的索引可能对您的伤害比对您的帮助更大。每个索引在插入/删除/更新行时都会增加开销,因为索引必须被维护,这个特定的索引可能没有那么多值得使用。当然,我只是猜测,这取决于其余的查询。

SELECT
    ; Grouping by name, means comparing strings. 
    categories.name AS cat_name, 
    ; No need for distinct, the same item.id cannot belong to different categories
    COUNT(distinct items.id) AS category_count  
FROM `items` 
INNER JOIN `categories` ON `categories`.`id` = `items`.`category_id` 
WHERE `items`.`state` IN ('listed', 'reserved') 
   ; Not needed, the inner join gets rid of items with no category_id
   AND (items.category_id IS NOT NULL) 
GROUP BY categories.name 
ORDER BY category_count DESC 
LIMIT 10\G

这个查询的结构方式基本上是扫描整个 items 表,因为它使用 category_id 索引,然后通过 where 子句过滤,然后加入 category 表,这意味着在主键(categories.id ) 项目结果集中每个项目行的索引。然后按名称分组(使用字符串比较)进行计数,然后除掉 10 个结果之外的所有内容。

我会写这样的查询:

SELECT categories.name, counts.n
FROM (SELECT category_id, COUNT(id) n
      FROM items 
      WHERE state IN ('listed', 'reserved') AND category_id is not null
      GROUP BY category_id ORDER BY COUNT(id) DESC LIMIT 10) counts 
JOIN categories on counts.category_id = categories.id
ORDER BY counts.n desc          

(如果语法不完美,我很抱歉我没有运行 MySQL)

有了这个查询,引擎可能会做的是:

使用 items.state 索引获取“列出的”、“保留的”项目并按 category_id 比较数字而不是字符串,然后仅获取 10 个最高计数,然后加入类别以获取名称(但仅使用 10 个索引搜索) .

于 2012-09-27T00:32:40.047 回答