【问题标题】:MySQL query run time is better even though its execution plan is badMySQL 查询运行时间更好,即使它的执行计划很糟糕
【发布时间】:2021-06-15 11:24:05
【问题描述】:

我正在尝试优化这个 MySQL 查询,但在理解执行计划方面经验不足我很难理解执行计划。

我的问题是:你能帮我理解为什么New Query的查询执行计划比Original query差吗?即使新查询在 Prod 中表现更好。

重现这种情况所需的 SQL 是 here
最后还保留了相关的表定义(表 bill_range 使用外键 bill_id 引用账单)

原始查询在 PROD 中需要 10 秒才能完成

select * 
from bill_range 
where (4050 between low and high ) 
order by bill_id limit 1;

新查询(我正在强制/建议使用索引)在 PROD 中需要 5 秒才能完成

select * 
from bill_range 
use index ( bill_range_low_high_index) 
where (4050 between low and high ) 
order by bill_id limit 1;

但执行计划给出的建议是原始查询更好(这是我的理解似乎错误的部分)

原始查询

新查询

  1. 原始查询的列“类型”建议 index 而新查询 说所有
  2. “键”列是 bill_id(可能是 FK 上的索引) 原始查询和 Null 用于新查询
  3. 原始查询的列“行”为 1,而新查询的列“行”为 9

因此,鉴于所有这些信息,这并不意味着新查询实际上比原始查询更糟糕。 如果这是真的,为什么新查询的性能更好?还是我读错了执行计划。

表格定义

CREATE TABLE bill_range (
    id int(11) NOT NULL AUTO_INCREMENT,
    low varchar(255) NOT NULL,
    high varchar(255) NOT NULL,
    PRIMARY KEY (id),
    bill_id int(11) NOT NULL,
    FOREIGN KEY (bill_id) REFERENCES bill(id)
);
CREATE TABLE bill (
    id int(11) NOT NULL AUTO_INCREMENT,
    label varchar(10),
    PRIMARY KEY (id)
);
create index bill_range_low_high_index on bill_range( low, high);

注意:我提供 2 个表的定义的原因是因为原始查询决定使用基于外键的索引来计费表

【问题讨论】:

  • 索引的基数是多少?如果它的基数很差,使用索引也无济于事。
  • 查询没有使用bill表,为什么需要它的定义?
  • 优化器并不总是正确的,您的基准可能无法重现。例如,当您在 10 秒时对第一个查询进行基准测试时,当时可能有其他进程在 MySQL 上运行。通常,您希望避免强制使用索引,因为 MySQL 比您更聪明(大多数时候)。
  • @Barmar 在 PROD 中 bill_range_low_high_index 的基数为 200K
  • 不不!不要将数字 (4500) 与 VARCHAR 进行比较!!修复它,然后重新检查这里讨论的所有内容;它可能不再有效。

标签: mysql sql indexing query-optimization


【解决方案1】:

您的索引对于您的查询来说不是很理想。如果可以的话,让我解释一下。

MySQL 索引使用 BTREE 数据结构。这些在索引顺序访问模式下运行良好(因此 MySQL 的第一个存储引擎的 MyISAM 名称)。它支持跳转到索引中特定位置的查询,然后逐个元素地遍历索引。典型的例子就是这个,索引在col

 SELECT whatever FROM tbl WHERE col >= constant AND col <= constant2

这是对WHERE col BETWEEN constant AND constant2的重写。

让我们重新编写您的查询,使这种模式显而易见,因此您想要的列是明确的。

select id, low, high, bill_id 
from bill_range 
where low <= 4050 
  and high >= 4050 
order by bill_id limit 1;

high 列上的索引允许从具有high &gt;= 4050 的第一个符合条件的行开始进行范围扫描。然后,我们可以继续使它成为一个复合索引,包括bill_idlow 列。

CREATE INDEX high_billid_low ON bill_range (high, bill_id, low);

因为我们想要最低匹配的bill_id,所以我们接下来将其放入索引中,最后是low 值。因此,查询计划程序随机访问high 对第一个符合条件的行的索引,然后扫描直到找到满足low 标准的第一个索引项。然后就完成了:这就是想要的结果。 bill_id 已经订购了它,所以它可以停止。 ORDER BY 来自索引。查询可以完全从索引中得到满足——即所谓的覆盖索引

至于为什么您的两个查询执行不同:首先,查询规划器决定按bill_id 顺序扫描您的数据,寻找第一个匹配的low/high 对。可能它决定实际对结果集进行排序可能比按顺序扫描bill_ids 更昂贵。在我看来,您的第二个查询进行了表扫描。为什么会更快,谁知道呢?

请注意,此索引也适用于您。

CREATE INDEX low_billid_high ON bill_range (low DESCENDING, bill_id, high);

在 InnoDB 中,表的 PK id 隐含地包含在每个索引中,因此无需在复合索引中提及。

而且,您仍然可以按照最初编写它的方式编写它;查询规划器会弄清楚你想要什么。

专业提示:避免使用SELECT * ...* 使您更难推断您需要检索的列。

【讨论】:

  • 我认为“重写”正是解析器正在做的事情。做EXPLAIN ...; SHOW WARNINGS;验证。
  • 关于索引的注释:INDEX(x DESC, y ASC) 在 MySQL 8.0 中采用 DESC。它在所有其他版本中被忽略。此注释在您使用ORDER BY x DESC, y ASCORDER BY x ASC, y DESC 时才重要。我无法想象这对WHERE 很重要。
  • 评论/回答教了很多。几个值得注意的点:(1) 列 lowhigh 是 varchar 类型(如在我的 PROD 中)。 (2)SHOW WARNINGS 得到cannot use range access on index 'bill_range_low_high_index' due to type or collation conversion on field 'low' 。 (3) 如果我将int 用于lowhigh 并且新查询的执行计划得到改进,但原始查询执行计划仍然更好,这就会消失。 (4) TODO:如果我更改为int,我无法在我的实际项目中测试运行时间。将更新结果。 (5) 解决方案中提出的索引没有改变执行计划
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-22
相关资源
最近更新 更多