准备好的语句不使用预期的索引

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 开始?

如何使用索引号将数据发送到mysqli(准备好的语句)

准备好的选择语句的结果作为数组

带有 SQL Server 和准备好的语句的 PHP PDO

cakePHP 准备好的语句不工作(获取所有不工作)

MySQLi:使用一个准备好的语句插入多行