为什么查询优化器会选择这种糟糕的执行计划?

sne*_*l29 7 innodb mariadb execution-plan

我们有一个包含超过 1TB 数据的 mariadb 表(故事),定期运行一个查询来获取最近添加的行以在其他地方建立索引。

innodb_version: 5.6.36-82.1
version       : 10.1.26-MariaDB-0+deb9u1
Run Code Online (Sandbox Code Playgroud)

当查询优化器决定使用二级索引进行范围遍历(以 1000 为单位)时,查询工作正常

explain extended     SELECT  stories.item_guid
    FROM  `stories`
    WHERE  (updated_at >= '2018-09-21 15:00:00')
      AND  (updated_at <= '2018-09-22 05:30:00')
    ORDER BY  `stories`.`id` ASC
    LIMIT  1000;

+------+-------------+---------+-------+-----------------------------+-----------------------------+---------+------+--------+----------+---------------------------------------+
| id   | select_type | table   | type  | possible_keys               | key                         | key_len | ref  | rows   | filtered | Extra                                 |
+------+-------------+---------+-------+-----------------------------+-----------------------------+---------+------+--------+----------+---------------------------------------+
|    1 | SIMPLE      | stories | range | index_stories_on_updated_at | index_stories_on_updated_at | 5       | NULL | 192912 |   100.00 | Using index condition; Using filesort |
+------+-------------+---------+-------+-----------------------------+-----------------------------+---------+------+--------+----------+---------------------------------------+
1 row in set, 1 warning (0.00 sec)
Run Code Online (Sandbox Code Playgroud)

但偶尔,即使数据集有微小的差异(注意上面查询的第二个时间戳差异,值得一提的是整个表保存了几年的数据,保存了几千万行)决定使用主键索引

explain extended     SELECT  stories.item_guid
    FROM  `stories`
    WHERE  (updated_at >= '2018-09-21 15:00:00')
      AND  (updated_at <= '2018-09-22 06:30:00')
    ORDER BY  `stories`.`id` ASC
    LIMIT  1000;

+------+-------------+---------+-------+-----------------------------+---------+---------+------+--------+----------+-------------+
| id   | select_type | table   | type  | possible_keys               | key     | key_len | ref  | rows   | filtered | Extra       |
+------+-------------+---------+-------+-----------------------------+---------+---------+------+--------+----------+-------------+
|    1 | SIMPLE      | stories | index | index_stories_on_updated_at | PRIMARY | 8       | NULL | 240259 |    83.81 | Using where |
+------+-------------+---------+-------+-----------------------------+---------+---------+------+--------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
Run Code Online (Sandbox Code Playgroud)

导致它遍历整个主键索引(我猜是按顺序)然后过滤updated_at需要几个小时才能完成的字段。

该查询是由 ORM ActiveRecord 创建的,可能远非理想,作为一种变通解决方案,我们提出了几个手动制作的查询,ORDER BY stories.id移出,和/或使用 use/force 索引来避免 PK,因为我们是真正过滤我们的数据集updated_at

我在这里有兴趣了解的是查询优化器如何/为什么选择该执行计划,我了解查询优化器使用索引/表统计信息来做出该决定,但在这种情况下,如果我对innodb工作原理的理解是正确的,这很明显,在不使用任何 PK id 进行过滤的情况下遍历一个巨大的 PK 是不理想的。

我基本上是想了解那个“坏”决定的来源,使用哪些统计数据或未知变量来结束好计划(经常选择的计划,而且速度要快几个数量级)

随时纠正我的任何假设,因为我绝对不是 DBA 专家,

提前致谢!

Ric*_*mes 2

简短回答:WHERE正在ORDER BY向不同的方向牵引。优化器还不够智能,无法始终正确地决定拉动的方向。

长答案:

WHERE任何以 . 开头的 索引的好处updated_at。查看“100%”等内容。这样的索引可以快速找到所需的行,全部 192K。但是查询需要对 192K 行进行排序 ( ORDER BY) 才能到达LIMIT.

ORDER BY id的好处PRIMARY KEY。该索引允许查询按顺序获取所有行(从而避免排序),因此可以获取LIMIT(从而永远不会铲除超过 1000 行。

如果发现 1000 的值较小id,则运行速度会很快。如果所需的 1000 个在表中较晚,查询将运行缓慢。优化器无法预测(没有更多的智能和复杂性)。

update_at主要是跟踪吗id?如果是这样,那么任一索引都会导致本质上相同的块。但优化器既没有注意到这一点,也没有利用它。

可能的加速:

“覆盖”索引是一种包含所有所需列的索引。由于 PK 与数据“聚集”,因此PRIMARY KEY(id)是一种覆盖索引。但我们看到了情况可能有多糟糕。以下可能会更好:

INDEX(updated_at,      -- first, to deal with the WHERE
      id, item_guid)   -- in either order
Run Code Online (Sandbox Code Playgroud)

该查询对 192K 行进行范围扫描的速度比需要在INDEX(updated_at)和 数据之间跳转 192K 次才能找到 的速度更快item_guid。排序不会得到改善。

唉,这加快了更快的查询速度。所以,让我重新思考一下。

分区?

哦,您可能已经找到了 的第五个用例PARTITIONing。六年前,我只有 4 个用例;我还没有找到新的用例。让我谈谈您的情况可能有何不同。

假设你用过PARTITION BY RANGE (TO_DAYS(updated_at))表上,设置了大约50个分区。(如果您需要清除“旧”数据,分区非常好。)

设置分区时,需要重新考虑所有索引。现在有哪些指标?我假设这些:

PRIMARY KEY(id),
INDEX(updated_at)
Run Code Online (Sandbox Code Playgroud)

就你而言,也许不会有太大改变:

PRIMARY KEY(id, updated_at),
INDEX(updated_at)
Run Code Online (Sandbox Code Playgroud)

您的查询会发生什么情况?

解决WHERE第一个问题不会有太大改变。是的,会有分区修剪,加上二级索引。速度将保持大致相同。

通过解决ORDER BY第一个问题,查询计划可以缩减为一个(或两个)分区。id那么在那个/那些分区内按顺序扫描的速度将是原来的 50(或 25)倍。可能会增加排序,因为来自不同分区的行不会按顺序排列。

其他注意事项

我们能看到吗SHOW CREATE TABLE?1TB 表需要很多东西来帮助提高性能。如果您的所有数字都是INTs,那么可能有很多空间可以通过缩小到 等来恢复MEDIUMINT。标准化可能值得做。如果建立了索引,那一定会对插入GUID性能造成很大的负担。等等等等.

Ypercube 的

如果ORDER BY updated_at, id任务可以完成,那么就消除了使用缓慢解释计划的诱惑。并INDEX(updated_at, id, item_guid)成为最佳指标。并且分区变得毫无用处。

分页

不是通过 OFFSET,记住你离开的地方。这讨论了如何处理涉及多个单列的“剩余”