MYSQL 优化器只是忽略我在复合索引中用于 ORDER BY 的最后一列

标签 mysql sql database optimization database-performance

我有一个表包含大约 300 万行,其结构如下:

CREATE TABLE `profiles3m` (
  `uid` int(10) unsigned NOT NULL,
  `birth_date` date NOT NULL,
  `gender` tinyint(4) NOT NULL DEFAULT '0',
  `country` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'ID',
  `city` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Makassar',
  `created_at` timestamp NULL DEFAULT NULL,
  `premium` tinyint(4) NOT NULL DEFAULT '0',
  `updated_at` timestamp NULL DEFAULT NULL,
  `latitude` double NOT NULL DEFAULT '0',
  `longitude` double NOT NULL DEFAULT '0',
  `orderid` int(11) NOT NULL,
  PRIMARY KEY (`uid`),
  KEY `idx_composites_latitude_longitude_gender_birth_date_created_at` (`latitude`,`longitude`,`country`,`city`,`gender`,`birth_date`) USING BTREE,
  KEY `idx_composites_country_city_gender_birth_date` (`country`,`city`,`gender`,`birth_date`,`orderid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

我未能告诉 MySQL 优化器使用复合索引定义中的所有列,似乎优化器只是忽略最后一列作为 orderid 进行排序,即您可能知道,InnoDB 表中的 uid 列的副本不能用于排序,因为它可能指示优化器使用 < strong>PRIMARY KEY 作为索引,而不是使用我们的复合索引,这就是创建 orderid 列的想法。

下面的SQL查询,以及Explain JSON,加上Show Index语句来显示表上的所有索引统计信息,可能有助于分析原因。

SELECT
    pro.uid 
FROM
    `profiles3m` AS pro 
WHERE
    pro.country = 'INDONESIA' 
    AND pro.city IN ( 'MAKASSAR' ) 
    AND pro.gender = 0 
    AND ( pro.birth_date BETWEEN ( NOW()- INTERVAL 35 YEAR ) AND ( NOW()- INTERVAL 25 YEAR ) ) 
    AND pro.orderid > 0 
ORDER BY
    pro.orderid
LIMIT 30

解释一下JSON如下:

{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "45278.73"
    },
    "ordering_operation": {
      "using_filesort": true,
      "cost_info": {
        "sort_cost": "19051.43"
      },
      "table": {
        "table_name": "pro",
        "access_type": "range",
        "possible_keys": [
          "idx_composites_country_city_gender_birth_date"
        ],
        "key": "idx_composites_country_city_gender_birth_date",
        "used_key_parts": [
          "country",
          "city",
          "gender",
          "birth_date"
        ],
        "key_length": "488",
        "rows_examined_per_scan": 57160,
        "rows_produced_per_join": 19051,
        "filtered": "33.33",
        "using_index": true,
        "cost_info": {
          "read_cost": "22417.02",
          "eval_cost": "3810.29",
          "prefix_cost": "26227.30",
          "data_read_per_join": "9M"
        },
        "used_columns": [
          "uid",
          "birth_date",
          "gender",
          "country",
          "city",
          "orderid"
        ],
        "attached_condition": "((`restful`.`pro`.`gender` = 0) and (`restful`.`pro`.`country` = 'INDONESIA') and (`restful`.`pro`.`city` = 'MAKASSAR') and (`restful`.`pro`.`birth_date` between <cache>((now() - interval 35 year)) and <cache>((now() - interval 25 year))) and (`restful`.`pro`.`orderid` > 0))"
      }
    }
  }
}

下面是显示索引语句:

+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| Non_unique | Key_name                                                       | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| 0          | PRIMARY                                                        | 1            | uid         | A         | 2984412     |          |        |      | BTREE      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| 1          | idx_composites_latitude_longitude_gender_birth_date_created_at | 1            | latitude    | A         | 2934360     |          |        |      | BTREE      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| 1          | idx_composites_latitude_longitude_gender_birth_date_created_at | 2            | longitude   | A         | 2984080     |          |        |      | BTREE      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| 1          | idx_composites_latitude_longitude_gender_birth_date_created_at | 3            | country     | A         | 2984080     |          |        |      | BTREE      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| 1          | idx_composites_latitude_longitude_gender_birth_date_created_at | 4            | city        | A         | 2984080     |          |        |      | BTREE      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| 1          | idx_composites_latitude_longitude_gender_birth_date_created_at | 5            | gender      | A         | 2984080     |          |        |      | BTREE      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| 1          | idx_composites_latitude_longitude_gender_birth_date_created_at | 6            | birth_date  | A         | 2984080     |          |        |      | BTREE      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| 1          | idx_composites_country_city_gender_birth_date                  | 1            | country     | A         | 1           |          |        |      | BTREE      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| 1          | idx_composites_country_city_gender_birth_date                  | 2            | city        | A         | 14          |          |        |      | BTREE      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| 1          | idx_composites_country_city_gender_birth_date                  | 3            | gender      | A         | 29          |          |        |      | BTREE      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| 1          | idx_composites_country_city_gender_birth_date                  | 4            | birth_date  | A         | 362449      |          |        |      | BTREE      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+
| 1          | idx_composites_country_city_gender_birth_date                  | 5            | orderid     | A         | 2984412     |          |        |      | BTREE      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+

在解释 JSON 中真正有趣的是,他们告诉我们,如果优化器只能使用索引操作的四个部分,毫不奇怪,排序操作就是使用文件排序,正如您所知,这意味着执行速度较慢,这对应用程序性能不利。

idx_composites_country_city_gender_birth_date (country,city,gender,birth_date,orderid)

"ordering_operation": {
          "using_filesort": true,
.....

"key": "idx_composites_country_city_gender_birth_date",    
"used_key_parts": [
              "country",
              "city",
              "gender",
              "birth_date"
            ],

我是否遗漏了什么,是由 WHERE 语句中的 RANGE 子句引起的吗?,我已经使用复合索引序列中的不同列组合进行了测试例如,我正在将 orderid 列更改为 premium,这是一个仅包含 0 和 1 的标志列类型,并且它有效,MySQL 优化器可以利用所有五个列,那么为什么优化器无法对 orderid 列执行相同操作?与基数有关吗?我不太确定,我唯一可以保证的是,无论如何操作,我都必须使 ORDER BY 正常工作,而不会对应用程序性能产生任何影响。

这几天我一直在寻找答案,但仍然无法解决。 差点忘了提及 MySQL 版本,以防有帮助。

+------------+
| version()  |
+------------+
| 5.7.29-log |
+------------+

最佳答案

您注意到它只使用了索引的四列:

    "used_key_parts": [
      "country",
      "city",
      "gender",
      "birth_date"
    ],

尽管 WHERE 子句中的条件引用了所有五列:

WHERE
    pro.country = 'INDONESIA' 
    AND pro.city IN ( 'MAKASSAR' ) 
    AND pro.gender = 0 
    AND ( pro.birth_date BETWEEN ( NOW()- INTERVAL 35 YEAR ) AND ( NOW()- INTERVAL 25 YEAR ) ) 
    AND pro.orderid > 0 

但是,这些条件有些不同。 国家城市性别条件都是平等条件。一旦搜索找到具有这些值的索引子集,接下来就按 birth_date 对子集进行排序,如果有一些行与 birth_date 相关,则这些行是按 orderid 进一步排序。

就像您阅读电话簿一样,您会找到所有姓氏为“Smith”的人,他们是按名字排序的。如果有多个人也具有相同的名字,则会根据各自的电话号码在电话簿中进行排序。

Smith, Sarah 408-555-1234
Smith, Sarah 408-555-5678

但是,如果您搜索姓氏为 Smith 且名字以“S”开头的所有人员,该怎么办?

Smith, Sam   408-555-3298
Smith, Sarah 408-555-1234
Smith, Sarah 408-555-5678
Smith, Stan  408-555-4224

这些未按电话号码排序。仅当它们在前面的列中出现时,它们才会按姓氏排序,然后按名字排序,最后按电话号码排序。

如果您想按电话号码对它们进行排序,您可以创建一个索引,其中的列按其他顺序排列,例如姓氏、电话号码、名字。

Smith 408-555-1234 Sarah
Smith 408-555-2020 David
Smith 408-555-3298 Sam
Smith 408-555-4100 Charlie
Smith 408-555-4224 Stan
Smith 408-555-5555 Annette
Smith 408-555-5678 Sarah

现在它们按电话号码顺序排列,但其中还有其他名称与您的名字以“S”开头的条件不匹配。它们甚至没有按名字排序,因为只有当前两列并列时,名字的第三列才会被排序。

这指出了索引的一个普遍问题:您只能对涉及相等比较的列重新排序。如果要对结果进行排序,则仅当按索引中的列排序并且索引的所有前面的列仅用于相等比较时才可以使用索引。

范围比较中引用一列后,搜索和排序时都会忽略索引中的任何后续列。

换句话说:索引可以有任意数量的列用于相等条件,索引的下一列可以用于范围条件或对结果进行排序。但用于这些操作中的任一操作的列数不超过一列。

您无法优化所有内容。


回复您的评论:如果您在不包括 birth_date 的列上有索引:

alter table profiles3m add key bk1 (country, city, gender, orderid);

然后 EXPLAIN 显示没有文件排序:

EXPLAIN SELECT
    pro.uid 
FROM
    `profiles3m` AS pro 
WHERE
    pro.country = 'INDONESIA' 
    AND pro.city IN ( 'MAKASSAR' ) 
    AND pro.gender = 0 
    AND ( pro.birth_date BETWEEN ( NOW()- INTERVAL 35 YEAR ) AND ( NOW()- INTERVAL 25 YEAR ) ) 
    AND pro.orderid > 0 
ORDER BY
    pro.orderid
LIMIT 30\G

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: pro
   partitions: NULL
         type: range
possible_keys: bk1
          key: bk1
      key_len: 489
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using index condition; Using where

(看起来很低,因为我正在使用空表进行测试。)

需要注意的是,这使用索引来匹配与 countrycitygenderorderid< 匹配的所有行。然后 MySQL 将以困难的方式评估 birth_date 的剩余条件:逐行。

但之后,优化器知道它已经按索引顺序获取了行,因此它知道自然会按 orderid 排序,因此它可以跳过文件排序。

这可能是也可能不是净赢。这取决于有多少行匹配,但必须根据 birth_date 的条件将其丢弃。评估每一行的条件的成本有多大。与使用索引按 birth_date 进行过滤所节省的成本相比,情况如何。

关于MYSQL 优化器只是忽略我在复合索引中用于 ORDER BY 的最后一列,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62500996/

相关文章:

c# - 我应该使用哪个框架和数据库?

MySQL 索引创建

php - PHP 中的简单 SQL 查询

php - PHP 类的 SQL 连接被拒绝

mysql - 从两个表插入一个表的 SQL 语句(使用另一个没有连接条件的 id)

c# - 将可空整数 (int?SomeValue = NULL) 作为参数传递给存储过程

SQL:按日期顺序订购 Month_year

mysql - Jboss Messaging (jbm_tx) 表不断增长

mysql - 依赖 MySQL 功能与我的脚本

MySQL 授予指定域上的用户