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

MYSQL Optimizer just ignore the last column which i use to ORDER BY in the composite indexes

我有一个 table 包含大约 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 用于订购目的,它只是 uid 列的副本,您可能知道 InnoDB table 中的 PRIMARY KEY 不能用于排序,因为它可能指示优化器使用 PRIMARY KEY 作为索引而不是使用我们的复合索引,这就是创建 [=17= 的想法]栏目来自.

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

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      |
+------------+----------------------------------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+

Explain 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 子句引起的吗?我已经在我们的复合索引序列中使用不同的列组合进行了测试,例如我正在更改orderidpremium 这是一个标志列类型,只包含 0 和 1,并且有效 MySQL 优化器可以利用所有五个列,那么为什么优化器不能这样做与 orderid 列?它与基数有关吗?我不太确定,我唯一可以保证的是,我必须使 ORDER BY 正常工作而不影响应用程序性能,无论怎么做。

这两天一直在找答案,还是解决不了。 差点忘了提到 MySQL 版本以防有帮助。

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

MySQL 不能使用索引进行排序。 birthdate 的条件意味着索引中的行未按 orderid.

排序

我认为没有办法解决这个问题。

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

    "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 

但是,这些条件有些不同。 countrycitygender上的条件都是相等条件。一旦搜索找到具有这些值的索引子集,那么该子集接下来按 birth_date 排序,如果有一些行与 birth_date 相关,则这些行进一步按 [=22] 排序=].

就像如果你读一本 telephone 书,你会找到所有姓“Smith”的人,他们是按名字排序的。如果有多个人也有相同的名字,他们将根据各自的 phone 编号在 phone 书中排序。

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

这些未按 phone 编号排序。他们按姓氏排序,然后按名字排序,然后按 phone 数字排序,仅当它们在前面的列中并列时。

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

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

现在他们的顺序是 phone,但其中还有其他名字不符合您以“S”开头的名字的条件。它们甚至不按名字排序,因为名字的第三列只有在前两列并列时才会排序。

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

range 比较中引用一列后,索引中的任何后续列都将被忽略以进行搜索和排序。

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

您无法优化所有内容。


关于您的评论:如果您在 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

rows 看起来很低,因为我正在用空 table 测试它。)

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

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

这可能是也可能不是净赢。这取决于有多少行被匹配但必须被 birth_date 上的条件抛出。以及为每一行评估该条件的成本是多少。这与使用索引按 birth_date.

过滤所节省的成本相比如何?