为什么我的 SQL 查询没有使用 table 的复合索引?

Why is my SQL query not using the table's composite index?

我有一个 users table 列:id(主键),typeexternal_idexternal_typecreated_atupdated_at

索引:

还有一个 settings table 列:iduser_idnamevalue , created_at, updated_at, type

索引:

我执行查询:

SELECT users.id, users.type, users.external_id, users.created_at, users.updated_at,

  settings.id, settings.settings_id, settings.name, settings.value, 
  settings.created_at, settings.updated_at, settings.type

FROM users
  
  LEFT OUTER JOIN settings on settings.user_id = users.id

WHERE users.external_id=3 and users.external_type=“Owner”

在解释报告中,我看到:

目标

我为调试所做的事情:

我错过了什么?

这回答了问题的原始版本。

您可能会通过使用 LEFT JOIN 然后在 WHERE 子句中过滤来混淆优化器。

首先将查询写为:

SELECT u.id, u.type, u.external_id, u.created_at, u.updated_at,
       s.id, s.settings_id, s.name, s.value, 
       s.created_at, s.updated_at, s.type
FROM users u JOIN
     settings s
     ON s.user_id = u.id
WHERE s.external_id = 3 and s.external_type = 'Owner'

table 别名只是使查询更易于编写和读取,不会影响性能。

那么,您需要以下索引:

  • settings(external_id, external_type, user_id)
  • user(id)

MySQL 应该使用 settings 索引来查找匹配 external_idexternal_type 的用户,只需在索引中查找即可。然后它将使用 user_idusers table 中查找相应的信息。这应该是最快的方法。

实际上,您可以免费获得第二个,因为它是主键。我懒得创建覆盖索引,因为您要选择这么多列。但这可能会提供略微更好的性能。

不确定您使用的 mysql 是哪个版本。 8.0之前,mysqlinnodb不持久化统计,如果你的数据是倾斜的,内存中的统计很难代表数据。在您的情况下,查询优化器可能认为 table 扫描是最快的,如果统计信息表明 table 用户中的大部分数据 external_id = 3 和 external_type = 'Owner'因为table上没有索引覆盖选中的列,如果使用索引,查询引擎需要根据索引查找数据。

当您更改为 SELECT 索引中的唯一列时,索引将成为覆盖索引,查询引擎将不需要进行查找。

避免双重查找

您的索引是 (external_id, external_type, type),但为了获得查询所需的所有信息,它必须使用该索引来查找行,然后使用自动包含的 id在该索引的末尾查找主 table.

中的 created_atupdated_at

优化器判断直接进入主 table 会更简单,因此忽略索引。

你可以从你的陈述中看到这一事实的证据:

If I change the first line of the SELECT statement to remove users.created_at, users.updated_at, it uses the index

删除这些列后,它不再需要进行双重查找来完成查询。从索引中进行的一次查找是它选择使用该索引的原因。

以下为:

If I change the query’s WHERE clause to add and users.type=“Blah”, it uses the index

我猜想优化器现在认为双重查找是值得的,如果它可以通过这个更具选择性的查询减少足够多的行。理解优化器的推理并不总是那么容易,但这似乎是最明显的原因。

解决方案

要让它使用索引,你只需要让它成为一个覆盖索引,这样它就不需要执行双重查找。

(external_id,  external_type, type, created_at, updated_at)

这个索引将允许它避免双重查找,因为它可以过滤第一列,然后只使用索引中的其余列来满足 SELECT table无需跳回主 table.