MySQL 只有在使用 ORDER BY 字段 DESC 和 LIMIT 时查询才慢

MySQL query is slow only when using ORDER BY field DESC and LIMIT

概述

我是 运行 MySQL 5.7.30-33,我遇到了一个问题,似乎 MySQL 在 运行 时使用了错误的索引一个问题。我使用现有查询获得了 3 秒的查询时间。但是,仅通过更改 ORDER BY、删除 LIMIT 或强制使用 USE INDEX,我可以获得 0.01 秒的查询时间。不幸的是,我需要坚持使用我的原始查询(它已融入应用程序),所以如果这种差异可以在 schema/indexing.

中得到解决,那就太好了

设置/问题

我的table结构如下:

CREATE TABLE `referrals` (
  `__id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `systemcreated` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `referrerid` mediumtext COLLATE utf8mb4_unicode_ci,
  `referrersiteid` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  ... lots more mediumtext fields ...
  PRIMARY KEY (`__id`),
  KEY `systemcreated` (`systemcreated`,`referrersiteid`,`__id`)
) ENGINE=InnoDB AUTO_INCREMENT=53368 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED

table 只有 ~55k 行,但非常宽,因为一些字段包含巨大的 BLOB:

mysql> show table status like 'referrals'\G;
*************************** 1. row ***************************
           Name: referrals
         Engine: InnoDB
        Version: 10
     Row_format: Compressed
           Rows: 45641
 Avg_row_length: 767640
    Data_length: 35035897856
Max_data_length: 0
   Index_length: 3653632
      Data_free: 3670016
 Auto_increment: 54008
    Create_time: 2020-12-12 12:46:14
    Update_time: 2020-12-12 17:50:28
     Check_time: NULL
      Collation: utf8mb4_unicode_ci
       Checksum: NULL
 Create_options: row_format=COMPRESSED
        Comment: 
1 row in set (0.00 sec)

我客户的应用程序使用此查询 table,不幸的是,这不能轻易更改:

SELECT  *
    FROM  referrals
    WHERE  `systemcreated` LIKE 'XXXXXX%'
      AND  `referrersiteid` LIKE 'XXXXXXXXXXXX%'
    order by  __id desc
    limit  16;

这导致查询时间约为 3 秒。

解释看起来像这样:

+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table       | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | referrals   | NULL       | index | systemcreated | PRIMARY | 4       | NULL |   32 |     5.56 | Using where |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+

请注意,它使用主键进行查询而不是 systemcreated 索引。

实验 1

如果我将查询更改为使用 ASC 而不是 DESC:

SELECT  *
    FROM  referrals
    WHERE  `systemcreated` LIKE 'XXXXXX%'
      AND  `referrersiteid` LIKE 'XXXXXXXXXXXX%'
    order by  __id asc
    limit  16;

然后用了0.01秒,EXPLAIN看起来是一样的:

+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table       | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | referrals   | NULL       | index | systemcreated | PRIMARY | 4       | NULL |   32 |     5.56 | Using where |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+

实验 2

如果我更改查询以坚持使用 ORDER BY __id DESC,但删除 LIMIT:

SELECT  *
    FROM  referrals
    WHERE  `systemcreated` LIKE 'XXXXXX%'
      AND  `referrersiteid` LIKE 'XXXXXXXXXXXX%'
    order by  __id desc;

然后它也需要 0.01 秒,解释如下:

+----+-------------+-------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
| id | select_type | table       | partitions | type  | possible_keys | key           | key_len | ref  | rows | filtered | Extra                                 |
+----+-------------+-------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
|  1 | SIMPLE      | referrals   | NULL       | range | systemcreated | systemcreated | 406     | NULL | 2086 |    11.11 | Using index condition; Using filesort |
+----+-------------+-------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+

实验 3

或者,如果我强制原始查询使用 systemcreated 索引,那么它也会给出 0.01 秒的查询时间。这是解释:

mysql> explain     SELECT  *
    FROM  referrals USE INDEX (systemcreated)
    WHERE  `systemcreated` LIKE 'XXXXXX%'
      AND  `referrersiteid` LIKE 'XXXXXXXXXXXX%'
    order by  __id desc
    limit  16;

+----+-------------+--------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
| id | select_type | table        | partitions | type  | possible_keys | key           | key_len | ref  | rows | filtered | Extra                                 |
+----+-------------+--------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
|  1 | SIMPLE      | referrals    | NULL       | range | systemcreated | systemcreated | 406     | NULL | 2086 |    11.11 | Using index condition; Using filesort |
+----+-------------+--------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+

实验 4

最后,如果我使用原来的 ORDER BY __id DESC LIMIT 16 但 select 更少的字段,那么它也 returns 在 0.01 秒内!解释如下:

mysql> explain     SELECT  field1, field2, field3, field4, field5
    FROM  referrals
    WHERE  `systemcreated` LIKE 'XXXXXX%'
      AND  `referrersiteid` LIKE 'XXXXXXXXXXXX%'
    order by  __id desc
    limit  16;

+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table       | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | referrals   | NULL       | index | systemcreated | PRIMARY | 4       | NULL |   32 |     5.56 | Using where |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+

总结

所以唯一表现不佳的组合是 ORDER BY __id DESC LIMIT 16

认为 我的索引设置正确。我通过 systemcreatedreferrersiteid 字段查询,并按 __id 排序,所以我有一个定义为 (systemcreated, referrersiteid, __id) 的索引,但是 MySQL 似乎仍在使用 PRIMARY 键。

有什么建议吗?

  • "Avg_row_length: 767640";很多 MEDIUMTEXT。一行限制在8KB左右;溢出进入“非记录”块。读取这些块需要额外的磁盘命中。

  • SELECT * 将达到所有那些胖柱。总数将约为 50 次读取(每次 16KB)。这需要时间。

  • (Exp 4) SELECT a,b,c,d 运行 更快,因为它不需要每行获取所有 ~50 个块。

  • 你的二级索引,(systemcreated,referrersiteid,__id), --只有第一列有用。这是因为 systemcreated LIKE 'xxx%'。这是一个“运行ge”。一旦命中了一个运行ge,剩下的索引就失效了。除了...

  • “索引提示”(USE INDEX(...)) 今天可能有所帮助,但明天当数据分布发生变化时可能会使情况变得更糟。

  • 如果不能去掉LIKE中的通配符,推荐这两个索引:

      INDEX(systemcreated)
      INDEX(referrersiteid)
    
  • 真正的加速可以通过将查询翻转过来来实现。也就是说,首先找到 16 个 id,然后再去寻找所有那些笨重的列:

      SELECT  r2...   -- whatever you want
          FROM  
          (
              SELECT  __id
                  FROM  referrals
                  WHERE  `systemcreated` LIKE 'XXXXXX%'
                    AND  `referrersiteid` LIKE 'XXXXXXXXXXXX%'
                  order by  __id desc
                  limit  16 
          ) AS r1
          JOIN  referrals r2 USING(__id)
          ORDER BY  __id DESC   -- yes, this needs repeating 
    

并保留您拥有的 3 列二级索引。即使它必须扫描超过 16 行才能找到所需的 16 行,但它的体积要小得多。这意味着子查询(“derived table”)将适度快速。那么外部查询仍然会有 16 次查找——可能需要读取 16*50 个块。读取的总块数还是会少很多。

ASCDESCORDER BY 上几乎没有明显的区别。

为什么优化器选择PK而不是看似更好的二级索引? PK 可能 是最好的,特别是当 16 行位于 table 的 'end'(DESC)时。但如果它必须扫描整个 table 而找不到 16 行,那将是一个糟糕的选择。

同时,通配符测试使二级索引仅部分有用。优化器根据不充分的统计数据做出决定。有时感觉就像抛硬币。

如果你使用我的由内而外的重构,那么我推荐以下两个复合索引——优化器可以在它们之间做出半智能、半正确的选择,用于派生 table:

INDEX(systemcreated, referrersiteid, __id),
INDEX(referrersiteid, systemcreated, __id)

它会继续说“filesort”,但别担心;它只对 16 行进行排序。

而且,请记住,SELECT * 会影响性能。 (虽然你可能无法解决这个问题。)