MySQL 5.7 与 5.6:起初索引使用错误,但 "automagically" 数周后修复
MySQL 5.7 vs 5.6: Index usage wrong at first, but "automagically" fixed weeks later
上下文:
我有一个 MySQL 5.6 作为主控,有两个副本,其中一个也是 MySQL 5.6 实例,另一个是 MySQL 5.7。繁重的查询均匀分布在两个副本上。
5.6 副本已启动 运行 大约 2 个月,5.7 较新,运行 大约两周。
使用 AWS RDS。
奇怪的行为:
刚创建后,我注意到由于“错误的索引使用”,一些查询在 5.7 时要慢得多,如比较两个版本的 EXPLAIN
结果所示。甚至一些具有 USE INDEX
子句的查询似乎对 5.7 数据库没有影响。
考虑到它是一个全新的副本,我想:“也许索引统计信息不是最新的?”。
不是这样的。我有一个例程在主数据库上对系统上的每个 table 运行“分析 table”,所以索引统计应该没问题。我可以通过检查副本 mysql.innodb_index_stats
.
上的 last_update
来确认“analyse table”语句已成功复制
但几天过去了,我这边没有任何工作,错误的查询开始使用正确的索引!全部固定。没有完成工作。疯狂的一天。
问题:
什么会影响特定查询执行计划的选择?索引统计数据不是唯一的吗?
是否有任何“caching/warming up”的东西,超过两周的实际工作可以让 Optimiser 改变主意?
“分析table”是否足以保证两个查询具有相同的执行计划?
还有什么可以解释在执行两周后,MySQL 5.7 开始以与 MySQL 5.6 相同的方式工作?
请记住,这两个数据库都是来自相同来源的副本,接收均匀分布的流量。
SQL 示例:
以下查询(这是一个混淆版本)使用了 BAD 索引,一天后它使用了 GOOD 索引。就像魔法™一样。
SELECT sale.number,
seller.id,
seller.name,
sale.id,
COALESCE(billed_amount, 0),
COALESCE(billing.tax_id, '---'),
billing.date,
CASE COALESCE(sale.seller_name_2, '') WHEN '' THEN sale.seller_name_1 ELSE sale.seller_name_2 END,
sale.business_id,
sale.business_name,
sale.total
FROM app_billing billing
JOIN app_sale sale ON billing.sale_id = sale.id
JOIN app_seller seller ON seller.id = sale.seller_id
WHERE billing.tenant_id = 515
AND billing.removed = FALSE
AND billing.date BETWEEN '2020-08-01' AND '2020-08-31'
AND sale.status = 2
AND sale.seller_id IN (368);
-- MySQL 5.7 (BAD, really bad estimate and counter intuitive decision [should definitely be a range scan])
-- 1st step: There are only one seller with ID 368, so thats right.
-- 2nd step: 692 is pretty accurate, there are 695 sales for the seller ID 368.
-- 3rd step: This is tricky. There are a total of 270776 billing records that matches the 695 sale_ids from the previous step. 65% of the matches, have only one correspondence, but the remaining 35% have between 1000 and 5000 correspondences. Average would be 1360.
-- Estimated total number of processed records: 942000~.
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 1 | SIMPLE | seller | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100 | NULL |
| 1 | SIMPLE | sale | NULL | ref | PRIMARY,idx_seller_id | idx_seller_id | 4 | const | 692 | 10 | Using where |
| 1 | SIMPLE | billing | NULL | ref | idx_sale_id,idx_tenant_id_removed_date | idx_sale_id | 4 | sale.id | 2 | 2.16 | Using where |
-- MySQL 5.6 (GOOD, using range scan)
-- 1st step: There are only one seller with ID 368, so thats right.
-- 2nd step: 421 is almost perfect. There are actually 422 billing records across the date '2020-08-01' AND '2020-08-31' for the tenant_id 515 that arent removed.
-- 3rd step: That's right. There's only one sale correspondence per billing,
-- Estimated total number of processed records: 421.
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 1 | SIMPLE | seller | const | PRIMARY | PRIMARY | 4 | const | 1 | NULL |
| 1 | SIMPLE | billing | range | idx_sale_id,idx_tenant_id_removed_date | idx_tenant_id_removed_date | 9 | NULL | 421 | Using index condition |
| 1 | SIMPLE | sale | eq_ref | PRIMARY,idx_seller_id | PRIMARY | 4 | billing.sale_id | 1 | Using where |
MySQL 5.7 及更高版本受到查询优化器 heisenbug 的严重影响,这使得它无法预测地选择一个非常糟糕的执行计划。听起来你好像犯规了。到目前为止,我在 每个 5.7 安装上都看到了这种不一致的、不可预测的 table 行为(比我希望列举的要多)。
关于您的具体问题:
只有统计数据应该有所作为。但是,当 table 中有大量索引(超过大约 10 个)时,查询计划也总是容易混淆。然后就是我上面提到的heisenbug
没有,但是table统计一直在后台维护。听起来它终于得到了正确的数据分布估计 - 最终。
它应该——但通常不是。您可能需要查看以下设置:
innodb_stats_persistent_sample_pages
innodb_stats_persistent
optimizer_switch 设置。您可能想尝试在 5.7 上将其设置为您在 5.6 上设置的相同标志。它有时会有所帮助(但不会太频繁)。
有很多原因导致统计数据可能略有不同,从而导致不同的解释计划。
由于 WHERE
子句测试多个 table 中的列,优化器不一定知道从哪个 table 开始。有时会选错table.
与其追逐统计数据和优化器的选择,不如改进索引,从而希望索引足够快以解决问题:
billing: (tenant_id, sale_id, removed, date, tax_id)
billing: (removed, tenant_id, date, sale_id, tax_id)
sale: (status, seller_id)
无论决定从哪个 table 开始,这组索引都应该有所帮助。每个索引中列的顺序很重要。 (有一些变化同样有效。)
优化器可能“做错事”的原因示例。
- 如果
billing.tenant_id = 515
只出现在一行中,那么开始 billing
可能会很快——获取一行,然后加入以从另一行中获取行 table.
- 同样,如果
sale.status = 2
不经常出现,sale
将是更好的首选。
没有实现上述目标的统计数据。有什么:“5000 行中有 about 100 个不同的 tenent_id
值。”也就是说,优化器将假定 tenant_id=515
出现在 50 行中。
注:我说的是“关于”。这是因为 ANALYZE
不会(假设一个大型 InnoDB table)查看 table 中的每个值。相反,它需要一个样本。 (在文档中查找“潜水次数”。)
一些可能导致主副本行为不同的事情
- 上次更新统计数据是什么时候?没有同步更新。
- 统计数据涉及“随机”探测。因此可能会出现轻微(或大)的变化。
- 在某些情况下,统计信息的微小变化可能会导致所选查询计划的巨大差异——通常在 table 扫描和使用索引之间。优化器 尝试 在性能上没有太大差异的地方进行这种量子变化,但它可能是错误的。
- 可能不同的
SELECTs
在不同的副本上。 SELECTs
可以与通过复制进行的写入交互。这可能导致块分裂(等)发生在 and/or table 不同位置的不同时间。这会间接影响统计数据。
- 等等
上下文:
我有一个 MySQL 5.6 作为主控,有两个副本,其中一个也是 MySQL 5.6 实例,另一个是 MySQL 5.7。繁重的查询均匀分布在两个副本上。
5.6 副本已启动 运行 大约 2 个月,5.7 较新,运行 大约两周。
使用 AWS RDS。
奇怪的行为:
刚创建后,我注意到由于“错误的索引使用”,一些查询在 5.7 时要慢得多,如比较两个版本的 EXPLAIN
结果所示。甚至一些具有 USE INDEX
子句的查询似乎对 5.7 数据库没有影响。
考虑到它是一个全新的副本,我想:“也许索引统计信息不是最新的?”。
不是这样的。我有一个例程在主数据库上对系统上的每个 table 运行“分析 table”,所以索引统计应该没问题。我可以通过检查副本 mysql.innodb_index_stats
.
last_update
来确认“analyse table”语句已成功复制
但几天过去了,我这边没有任何工作,错误的查询开始使用正确的索引!全部固定。没有完成工作。疯狂的一天。
问题:
什么会影响特定查询执行计划的选择?索引统计数据不是唯一的吗?
是否有任何“caching/warming up”的东西,超过两周的实际工作可以让 Optimiser 改变主意?
“分析table”是否足以保证两个查询具有相同的执行计划?
还有什么可以解释在执行两周后,MySQL 5.7 开始以与 MySQL 5.6 相同的方式工作?
请记住,这两个数据库都是来自相同来源的副本,接收均匀分布的流量。
SQL 示例:
以下查询(这是一个混淆版本)使用了 BAD 索引,一天后它使用了 GOOD 索引。就像魔法™一样。
SELECT sale.number,
seller.id,
seller.name,
sale.id,
COALESCE(billed_amount, 0),
COALESCE(billing.tax_id, '---'),
billing.date,
CASE COALESCE(sale.seller_name_2, '') WHEN '' THEN sale.seller_name_1 ELSE sale.seller_name_2 END,
sale.business_id,
sale.business_name,
sale.total
FROM app_billing billing
JOIN app_sale sale ON billing.sale_id = sale.id
JOIN app_seller seller ON seller.id = sale.seller_id
WHERE billing.tenant_id = 515
AND billing.removed = FALSE
AND billing.date BETWEEN '2020-08-01' AND '2020-08-31'
AND sale.status = 2
AND sale.seller_id IN (368);
-- MySQL 5.7 (BAD, really bad estimate and counter intuitive decision [should definitely be a range scan])
-- 1st step: There are only one seller with ID 368, so thats right.
-- 2nd step: 692 is pretty accurate, there are 695 sales for the seller ID 368.
-- 3rd step: This is tricky. There are a total of 270776 billing records that matches the 695 sale_ids from the previous step. 65% of the matches, have only one correspondence, but the remaining 35% have between 1000 and 5000 correspondences. Average would be 1360.
-- Estimated total number of processed records: 942000~.
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 1 | SIMPLE | seller | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100 | NULL |
| 1 | SIMPLE | sale | NULL | ref | PRIMARY,idx_seller_id | idx_seller_id | 4 | const | 692 | 10 | Using where |
| 1 | SIMPLE | billing | NULL | ref | idx_sale_id,idx_tenant_id_removed_date | idx_sale_id | 4 | sale.id | 2 | 2.16 | Using where |
-- MySQL 5.6 (GOOD, using range scan)
-- 1st step: There are only one seller with ID 368, so thats right.
-- 2nd step: 421 is almost perfect. There are actually 422 billing records across the date '2020-08-01' AND '2020-08-31' for the tenant_id 515 that arent removed.
-- 3rd step: That's right. There's only one sale correspondence per billing,
-- Estimated total number of processed records: 421.
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 1 | SIMPLE | seller | const | PRIMARY | PRIMARY | 4 | const | 1 | NULL |
| 1 | SIMPLE | billing | range | idx_sale_id,idx_tenant_id_removed_date | idx_tenant_id_removed_date | 9 | NULL | 421 | Using index condition |
| 1 | SIMPLE | sale | eq_ref | PRIMARY,idx_seller_id | PRIMARY | 4 | billing.sale_id | 1 | Using where |
MySQL 5.7 及更高版本受到查询优化器 heisenbug 的严重影响,这使得它无法预测地选择一个非常糟糕的执行计划。听起来你好像犯规了。到目前为止,我在 每个 5.7 安装上都看到了这种不一致的、不可预测的 table 行为(比我希望列举的要多)。
关于您的具体问题:
只有统计数据应该有所作为。但是,当 table 中有大量索引(超过大约 10 个)时,查询计划也总是容易混淆。然后就是我上面提到的heisenbug
没有,但是table统计一直在后台维护。听起来它终于得到了正确的数据分布估计 - 最终。
它应该——但通常不是。您可能需要查看以下设置:
innodb_stats_persistent_sample_pages innodb_stats_persistent
optimizer_switch 设置。您可能想尝试在 5.7 上将其设置为您在 5.6 上设置的相同标志。它有时会有所帮助(但不会太频繁)。
有很多原因导致统计数据可能略有不同,从而导致不同的解释计划。
由于 WHERE
子句测试多个 table 中的列,优化器不一定知道从哪个 table 开始。有时会选错table.
与其追逐统计数据和优化器的选择,不如改进索引,从而希望索引足够快以解决问题:
billing: (tenant_id, sale_id, removed, date, tax_id)
billing: (removed, tenant_id, date, sale_id, tax_id)
sale: (status, seller_id)
无论决定从哪个 table 开始,这组索引都应该有所帮助。每个索引中列的顺序很重要。 (有一些变化同样有效。)
优化器可能“做错事”的原因示例。
- 如果
billing.tenant_id = 515
只出现在一行中,那么开始billing
可能会很快——获取一行,然后加入以从另一行中获取行 table. - 同样,如果
sale.status = 2
不经常出现,sale
将是更好的首选。
没有实现上述目标的统计数据。有什么:“5000 行中有 about 100 个不同的 tenent_id
值。”也就是说,优化器将假定 tenant_id=515
出现在 50 行中。
注:我说的是“关于”。这是因为 ANALYZE
不会(假设一个大型 InnoDB table)查看 table 中的每个值。相反,它需要一个样本。 (在文档中查找“潜水次数”。)
一些可能导致主副本行为不同的事情
- 上次更新统计数据是什么时候?没有同步更新。
- 统计数据涉及“随机”探测。因此可能会出现轻微(或大)的变化。
- 在某些情况下,统计信息的微小变化可能会导致所选查询计划的巨大差异——通常在 table 扫描和使用索引之间。优化器 尝试 在性能上没有太大差异的地方进行这种量子变化,但它可能是错误的。
- 可能不同的
SELECTs
在不同的副本上。SELECTs
可以与通过复制进行的写入交互。这可能导致块分裂(等)发生在 and/or table 不同位置的不同时间。这会间接影响统计数据。 - 等等