【问题标题】:MySQL uses incorrect index. Why?MySQL 使用了不正确的索引。为什么?
【发布时间】:2018-03-07 09:49:24
【问题描述】:

有一个典型的Users 表,其中包含字段:id (primary)application_idloginphone 等(application_id - 选择性字段)

索引很少:

index_users_on_application_id,
unique_index_users_on_application_id_and_login
unique_index_users_on_application_id_and_phone

查询本身很简单:

SELECT  `users`.*
FROM `users`
WHERE `users`.`application_id` = 1234
LIMIT 10 OFFSET 0;

棘手的部分是此查询使用两个唯一索引之一(例如unique_index_users_on_application_id_and_login),然后返回按login 排序的用户列表。但我需要它们按id 排序。

为此,我更新了查询:

SELECT  `users`.*
FROM `users`
WHERE `users`.`application_id` = 1234
ORDER BY id
LIMIT 10 OFFSET 0;

好吧,现在解释表明 MySQL 开始使用 PRIMARY 键而不是任何索引。但这是怎么发生的?如果index_users_on_application_id 实际上应该包含两个字段:[application_id, id] (InnoDB),那么该索引非常适合查询,但 MySQL 决定选择另一个。

如果我说IGNORE INDEX(PRIMARY),MySQL 开始使用unique_index_users_on_application_id_and_login,仍然忽略正确的索引。 ORDER BY id+0 时的结果相同。

我还尝试ORDER BY application_id, id 以确保索引最适合,MySQL 仍然选择错误的索引。

任何想法,为什么会发生以及如何确保 MySQL 使用正确的索引而不明确说 USE INDEX(index_users_on_application_id)

Users 表的完整索引列表:

mysql> show indexes from users;
+-------+------------+-----------------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name                                            | Seq_in_index | Column_name          | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+-----------------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| users |          0 | PRIMARY                                             |            1 | id                   | A         |       21893 |     NULL | NULL   |      | BTREE      |         |               |
| users |          0 | index_users_on_confirmation_token                   |            1 | confirmation_token   | A         |          28 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          0 | index_users_on_reset_password_token                 |            1 | reset_password_token | A         |          50 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          0 | index_users_on_application_id_and_external_user_id  |            1 | application_id       | A         |          32 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          0 | index_users_on_application_id_and_external_user_id  |            2 | external_user_id     | A         |         995 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          0 | index_users_on_application_id_and_login             |            1 | application_id       | A         |          30 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          0 | index_users_on_application_id_and_login             |            2 | login                | A         |       21893 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          1 | users_account_id_fk                                 |            1 | account_id           | A         |          44 |     NULL | NULL   |      | BTREE      |         |               |
| users |          1 | users_blob_id_fk                                    |            1 | blob_id              | A         |         118 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          1 | index_users_on_remember_token                       |            1 | remember_token       | A         |           8 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          1 | index_users_on_application_id                       |            1 | application_id       | A         |          32 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          1 | index_users_on_application_id_and_facebook_id       |            1 | application_id       | A         |          32 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          1 | index_users_on_application_id_and_facebook_id       |            2 | facebook_id          | A         |        3127 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          1 | index_users_on_application_id_and_twitter_digits_id |            1 | application_id       | A         |          32 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          1 | index_users_on_application_id_and_twitter_digits_id |            2 | twitter_digits_id    | A         |         138 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          1 | index_users_on_application_id_and_email             |            1 | application_id       | A         |          32 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          1 | index_users_on_application_id_and_email             |            2 | email                | A         |        2189 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          1 | index_users_on_application_id_and_full_name         |            1 | application_id       | A         |          32 |     NULL | NULL   | YES  | BTREE      |         |               |
| users |          1 | index_users_on_application_id_and_full_name         |            2 | full_name            | A         |        5473 |     NULL | NULL   | YES  | BTREE      |         |               |
+-------+------------+-----------------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
19 rows in set (0.01 sec)

解释示例:

mysql> EXPLAIN SELECT  `users`.* FROM `users` WHERE `users`.`application_id` = 56374  ORDER BY id asc LIMIT 1 OFFSET 0;
+----+-------------+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------+---------+-------+------+-----------------------------+
| id | select_type | table | type | possible_keys                                                                                                                                                                                                                                                                                                  | key                                                | key_len | ref   | rows | Extra                       |
+----+-------------+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------+---------+-------+------+-----------------------------+
|  1 | SIMPLE      | users | ref  | index_users_on_application_id_and_external_user_id,index_users_on_application_id_and_login,index_users_on_application_id,index_users_on_application_id_and_facebook_id,index_users_on_application_id_and_twitter_digits_id,index_users_on_application_id_and_email,index_users_on_application_id_and_full_name | index_users_on_application_id_and_external_user_id | 5       | const |    1 | Using where; Using filesort |
+----+-------------+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------+---------+-------+------+-----------------------------+
1 row in set (0.00 sec)

问题本身是使用错误的索引会导致这样的查询(限制为 100 次而不是 1 次)执行 MINUTES 分钟,而使用正确的索引则只是一瞬间的问题。

分析:

SET PROFILING = 1; SELECT `users`.* FROM `users` WHERE `users`.`application_id` = 56374  ORDER BY id asc LIMIT 1 OFFSET 0; SHOW PROFILE FOR QUERY 1; SET PROFILING = 0;
Query OK, 0 rows affected (0.00 sec)

+----------+----------+-----------------+-------+-------+----------------------------------------------------------------------------------------------------------------------------------+----------------------+-------+---------+---------------------+---------------------+---------------------+------------------+-------------+------------+---------+----------------------+------------------------+---------------------+--------------------+---------------------+----------------------+-------------------+------------+----------------+-------------+---------------------------------------------------------------------------------------+-------------------+-------------------------+--------------------------------------------------+----------------+
-- fields list --
+----------+----------+-----------------+-------+-------+----------------------------------------------------------------------------------------------------------------------------------+----------------------+-------+---------+---------------------+---------------------+---------------------+------------------+-------------+------------+---------+----------------------+------------------------+---------------------+--------------------+---------------------+----------------------+-------------------+------------+----------------+-------------+---------------------------------------------------------------------------------------+-------------------+-------------------------+--------------------------------------------------+----------------+
| 27265241 |     NULL | Some Username | NULL  | 9777  | SomeHash | AnotherHash | NULL  | NULL    | 2017-04-12 15:53:32 | 2017-09-21 13:39:51 | 2017-09-24 19:19:06 |             1234 | NULL        | NULL       |    NULL | NULL                 | NULL                   | NULL                | NULL               | 2017-07-05 10:59:59 | NULL                 | NULL              |      12345 | NULL           | NULL        | something_else | NULL              |                       1 | another_hash |          54321 |
+----------+----------+-----------------+-------+-------+----------------------------------------------------------------------------------------------------------------------------------+----------------------+-------+---------+---------------------+---------------------+---------------------+------------------+-------------+------------+---------+----------------------+------------------------+---------------------+--------------------+---------------------+----------------------+-------------------+------------+----------------+-------------+---------------------------------------------------------------------------------------+-------------------+-------------------------+--------------------------------------------------+----------------+
1 row in set (1 min 14.43 sec)

+--------------------------------+-----------+
| Status                         | Duration  |
+--------------------------------+-----------+
| starting                       |  0.000068 |
| Waiting for query cache lock   |  0.000025 |
| init                           |  0.000025 |
| checking query cache for query |  0.000047 |
| checking permissions           |  0.000026 |
| Opening tables                 |  0.000031 |
| After opening tables           |  0.000025 |
| System lock                    |  0.000025 |
| Table lock                     |  0.000026 |
| Waiting for query cache lock   |  0.000037 |
| init                           |  0.000046 |
| optimizing                     |  0.000032 |
| statistics                     |  0.000225 |
| preparing                      |  0.000042 |
| executing                      |  0.000025 |
| Sorting result                 |  0.000057 |
| Sending data                   | 42.952100 |
| end                            |  0.000070 |
| query end                      |  0.000027 |
| closing tables                 |  0.000025 |
| Unlocking tables               |  0.000028 |
| freeing items                  |  0.000028 |
| updating status                |  0.000039 |
| cleaning up                    |  0.000025 |
+--------------------------------+-----------+
24 rows in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

【问题讨论】:

  • 很难回答您的问题 - 您能否发布此表的完整 DDL,包括索引,并显示查询计划?
  • 添加了索引列表和解释示例
  • @Mjh 我想这里的问题是这个查询(带有选定的 PRIMARY 索引)需要大约 30 秒。但是当它与 USE INDEX(index_users_on_application_id) 一起使用时,它需要 0 秒。问题是为什么?为什么 MySQL 在这里选择那个慢索引?
  • @Mjh 这是一个现实世界的问题,“正确”和“不正确”索引之间的区别就像查询执行时间的几分钟。
  • 注意到Profile 是多么无用?它通常说最多的时间是在Sending data。 (事实证明,这不一定是它的目的。)

标签: mysql sql indexing


【解决方案1】:

您应该能够使用索引提示和优化器提示来建议正确的索引用法:

Index hints

Optimizer hints

你可以直接提示表格:

tbl_name [[AS] alias] [index_hint_list]

index_hint_list:
    index_hint [index_hint] ...

index_hint:
    USE {INDEX|KEY}
      [FOR {JOIN|ORDER BY|GROUP BY}] ([index_list])
  | IGNORE {INDEX|KEY}
      [FOR {JOIN|ORDER BY|GROUP BY}] (index_list)
  | FORCE {INDEX|KEY}
      [FOR {JOIN|ORDER BY|GROUP BY}] (index_list)

index_list:
    index_name [, index_name] ...

在您的情况下,我认为最好的解决方案是按名称命名属性,而不是使用星号 * 并在查询中直接使用 IGNORE INDEX (unique_index_users_on_application_id_and_login, unique_index_users_on_application_id_and_phone ) FOR ORDER BY

基于您的代码的示例:

SELECT  u.id,
        u.application_id,
        u.login,
        u.phone,
        # ... here to continue
FROM users as u
IGNORE INDEX (unique_index_users_on_application_id_and_login, unique_index_users_on_application_id_and_phone ) FOR ORDER BY
WHERE u.application_id = 1234
ORDER BY u.id
LIMIT 10 OFFSET 0;

首次编辑
由于下面的评论,我添加了一个使主键无效的技巧。

你也可以通过这个技巧来使主键无效:

SELECT u.id, 
       u.application_id,
       u.login,
       u.phone,
       #...
FROM users as u
WHERE u.application_id = 1234
ORDER BY u.id+0

【讨论】:

  • 在这种情况下是否应该使用 INDEX 甚至 FORCE INDEX (correct_index) 更有效,因为您已经指定了索引?
  • @MilanVelebit 是的,使用和强制会更有效。我坚持希望index without explicitly say USE INDEX(index_users_on_application_id)
  • 看到你回答了这个问题,能否请你告诉我实际问题是什么以及你的答案为 OP 解决了什么问题?
  • @Mjh 由于在我回答之前没有进行任何编辑,我不得不猜测 nattfold 想要对优化器使用索引(在编辑之前回答)。我的回答使他能够这样做,即使在我现在看到的更昂贵的情况下(同样之前没有编辑)。
  • 是的,但我想问你是否了解他的问题实际上是什么?我不。我不明白如果他强制使用索引或使用另一个索引会发生什么。正在解决什么问题?到目前为止,我从未遇到过必须明确告知 MySQL 不要使用主键来支持其他索引的情况。
【解决方案2】:

很难确定,但在很多情况下,MySQL 擅长选择正确的索引。

查询分析器可能需要update the statistics - 有时查询分析器没有很好的数据信息,这可能会导致奇怪的行为。

但是...您的index_users_on_application_id 只有application_id,但没有ID

我猜application_id 的不同值的数量相当少。 ColumnID 是唯一的,其值与表中的行数一样多。没有索引同时包含 where 子句和 order by 子句,因此 MySQL 猜测最昂贵的部分将按 ID 排序,而不是按 application_id 过滤。

所以,我认为 MySQL 在这里做的是正确的事情。在幕后,它返回按 ID 排序的所有行,并遍历该列表并为您提供指定 application_ID 的前 10 个行。

显而易见的做法是使用 application_id 和 ID 创建一个索引,并查看 MySQL 是否选择它。

【讨论】:

  • 所以你建议在 application_id+id 上添加另一个索引,它会起作用吗?我认为索引合并在引擎盖下也是如此
  • 我的猜测是 MySQL 已经决定 application_id 中没有足够的不同值来使索引合并值得。
  • > 显而易见的事情是使用 application_id 和 ID 创建一个索引,并查看 MySQL 是否选择它。我也这样做了,它没有。这很明显,因为 index_users_on_application_id 是相同的(根据二级索引的定义,它包含 id
  • 在这种情况下,正如@mjh 所写,application_id 的基数太低,索引无法使用。
  • OP 暗示该表是 InnoDB。如果是这样,那么INDEX(app_id) 已经隐含地 附加了id(PK)。正如他所说,它实际上是INDEX(app_id, id),所以它可以并且应该使用了那个索引。
【解决方案3】:

当您使用ORDER BY column_name MySQL 扫描 ORDER BY 子句中给出的列中的所有数据时,在您的情况下,id 列将被扫描。对于此数据库,请使用 表指针。在 InnoDB 中,它是 PRIMARY KEY 的值,而在 MyISAM 中,它是 .MYD 文件中的偏移量。

这就是为什么当您使用 ORDER BY id 时,您的查询开始仅使用主键作为索引。

要使用您创建的索引,请添加id 列作为索引的第一列。然后它将有效地使用索引。因此,您在 users 表上的索引 unique_index_users_on_application_id_and_login 应包含以下相同序列 1- id 和 2- application_id 的列。

更多关于 MySQL 性能的 ORDER BY/LIMIT Go here

【讨论】:

  • 我认为您在多个问题上都错了。 ORDER BY id 可能会使 PK 首选,但不是必需的。在索引开头添加id 不会比PK 好。当通过= 过滤并按其他方式排序时,过滤列必须在索引中排在第一位。
  • 我知道在索引开头添加 'id' 并不是更好,但@nattfodd 专门询问如何有效地使用索引而不明确说 USE INDEX(index_users_on_application_id)。这只能通过在索引开头添加“id”来实现。
猜你喜欢
  • 1970-01-01
  • 2014-03-18
  • 1970-01-01
  • 2011-08-24
  • 2021-07-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多