准备好的语句不使用预期的索引
Posted
技术标签:
【中文标题】准备好的语句不使用预期的索引【英文标题】:Prepared Statement does not use expected index 【发布时间】:2021-10-15 19:45:37 【问题描述】:我有一个非常大的 IOT 样本表,我正在尝试对其运行相对简单的查询。使用 mysql CLI 正常运行查询会在 ~0.07 秒内返回结果。如果我首先通过 PDO 或运行 SQL PREPARE 语句准备查询,则请求需要一分钟。
我启用了优化器跟踪功能,看起来当语句准备好时,MySql 忽略了它应该使用的索引并对整个表进行文件排序。如果我做错了什么或者这看起来像 MySql 错误,我想知道任何见解。
该表本身包含超过 1 亿个样本,其中至少有 30 万个与此处查询的设备相关联。我使用 MySql 8.0.23 运行了这些测试,但是当我升级到 8.0.25 时,问题仍然存在。
表定义(省略了一些数据行)
Create Table: CREATE TABLE `samples` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`organization_id` int unsigned NOT NULL,
`device_id` int unsigned NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`raw_reading` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `samples_organization_id_foreign` (`organization_id`),
KEY `samples_reverse_device_id_created_at_organization_id_index` (`device_id`,`created_at` DESC,`organization_id`),
CONSTRAINT `samples_device_id_foreign` FOREIGN KEY (`device_id`) REFERENCES `devices` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT `samples_organization_id_foreign` FOREIGN KEY (`organization_id`) REFERENCES `organizations` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=188315314 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
在
select *
from `samples`
where `samples`.`device_id` = 5852
and `samples`.`device_id` is not null
and `id` != 188315308
order by `created_at` desc
limit 1;
一分钟以上运行的Sql
prepare test_prep from 'select * from `samples` where `samples`.`device_id` = ? and `samples`.`device_id` is not null and `id` != ? order by `created_at` desc limit 1';
set @a = 5852;
set @b = 188315308;
execute test_prep using @a, @b;
可以在my gist 找到未准备好的 SQL 的跟踪,但相关部分是
"reconsidering_access_paths_for_index_ordering":
"clause": "ORDER BY",
"steps": [
],
"index_order_summary":
"table": "`samples`",
"index_provides_order": true,
"order_direction": "asc",
"index": "samples_reverse_device_id_created_at_organization_id_index",
"plan_changed": false
,
可以在my other gist 找到准备好的查询的跟踪,但相关部分是
"reconsidering_access_paths_for_index_ordering":
"clause": "ORDER BY",
"steps": [
],
"index_order_summary":
"table": "`samples`",
"index_provides_order": false,
"order_direction": "undefined",
"index": "samples_reverse_device_id_created_at_organization_id_index",
"plan_changed": false
,
【问题讨论】:
and samples.device_id is not null
是多余的;删除它。
是的,我知道。我的框架(Laravel)将其添加到它的数据库抽象中。它不会真正影响问题,因为它是优化器丢弃的第一件事。
可能问题与mysql用于参数的隐式转换有关。如果您准备带有硬编码参数的代码,请尝试检查它是如何进行的。
您是否考虑过运行EXPLAIN
来获取性能/索引特定的优化信息?可能值得注意。
唉,抽象有时会碍事。
【参考方案1】:
你要使用的索引还不错:
`samples_reverse_device_id_created_at_organization_id_index`
(`device_id`,`created_at` DESC,`organization_id`)
但是,不是覆盖索引。如果查询性能真的很重要,我会添加一个至少涵盖过滤谓词的索引。您不需要真正的覆盖索引,因为您正在检索所有列。我会尝试:
create index ix1 on samples (device_id, created_at, id);
编辑
另一个可以促进索引使用的技巧是尽可能延迟谓词id != 188315308
。如果您知道此谓词将与其余谓词生成的前 100 行中的至少一行匹配,您可以尝试将查询改写为:
select *
from (
select *
from `samples`
where `samples`.`device_id` = 5852
order by `created_at` desc
limit 100
) x
where `id` != 188315308
order by `created_at` desc
limit 1
【讨论】:
有趣。我假设默认情况下 Id 是索引的一部分。优化器跟踪似乎暗示它是。我会试试看。 @KirillMorozov 你是绝对正确的。我不经常处理聚簇表,所以请忽略答案的第一部分。 从技术上讲,这不是“覆盖”,因为*
中有更多列。然而,它确实“覆盖”了WHERE
。而且,如果优化器足够聪明,ORDER BY
.【参考方案2】:
摆脱这个,因为= 5852
保证它是错误的:
and `samples`.`device_id` is not null
那么你的索引,或者这个索引,应该可以正常工作。
INDEX(device_id, created_at, id)
不要使用@variables;优化器似乎不看它们包含的值。也就是说,而不是
set @a = 5852;
set @b = 188315308;
execute test_prep using @a, @b;
简单地做
execute test_prep using 5852, 188315308;
考虑在 bugs.mysql.com 上写一份错误报告
我怀疑"order_direction": "undefined"
是问题的一部分。
【讨论】:
【参考方案3】:不是完整的解决方案,而是一种解决方法。我只在我的时间戳上添加了一个索引,这似乎满足了优化器。
KEY `samples_created_at_index` (`created_at` DESC),
我将尝试清理一个最小的测试用例并将其发布在 MySql 错误上。如果有任何结果,我会在此处添加后续内容。
【讨论】:
以上是关于准备好的语句不使用预期的索引的主要内容,如果未能解决你的问题,请参考以下文章
在 JDBC 中,为啥准备好的语句的参数索引从 1 而不是 0 开始?