为什么我的 SQL 查询没有使用 table 的复合索引?
Why is my SQL query not using the table's composite index?
我有一个 users table 列:id
(主键),type
,external_id
, external_type
、created_at
、updated_at
索引:
- 小学
(id)
- 独一无二
(external_id, external_type, type)
- 非唯一
(updated_at)
还有一个 settings table 列:id
、user_id
、name
、value
, created_at
, updated_at
, type
索引:
- 小学
(id)
- 独一无二
(user_id, name)
- 非唯一
(user_id)
- 非唯一
(updated_at)
我执行查询:
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”
在解释报告中,我看到:
- 对于用户 table,(external_id、external_type、类型)索引被识别为可能的键,但 NOT使用
- 设置table使用(user_id,名称)索引
目标
- 我想优化这个查询
- 所以我想让用户 table 使用 (external_id, external_type, type) 复合索引
我为调试所做的事情:
- 如果我更改 SELECT 语句的第一行以删除 users.created_at、users.updated_at,它会使用索引
- 如果我尝试为用户添加一个 (external_id, external_type) 非唯一索引 table,它仍然没有使用它
- 如果我将查询的 WHERE 子句更改为添加 users.type=“Blah”,它会使用索引
我错过了什么?
这回答了问题的原始版本。
您可能会通过使用 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_id
和 external_type
的用户,只需在索引中查找即可。然后它将使用 user_id
在 users
table 中查找相应的信息。这应该是最快的方法。
实际上,您可以免费获得第二个,因为它是主键。我懒得创建覆盖索引,因为您要选择这么多列。但这可能会提供略微更好的性能。
不确定您使用的 mysql 是哪个版本。 8.0之前,mysqlinnodb不持久化统计,如果你的数据是倾斜的,内存中的统计很难代表数据。在您的情况下,查询优化器可能认为 table 扫描是最快的,如果统计信息表明 table 用户中的大部分数据 external_id = 3 和 external_type = 'Owner'因为table上没有索引覆盖选中的列,如果使用索引,查询引擎需要根据索引查找数据。
当您更改为 SELECT 索引中的唯一列时,索引将成为覆盖索引,查询引擎将不需要进行查找。
避免双重查找
您的索引是 (external_id, external_type, type)
,但为了获得查询所需的所有信息,它必须使用该索引来查找行,然后使用自动包含的 id
在该索引的末尾查找主 table.
中的 created_at
和 updated_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.
我有一个 users table 列:id
(主键),type
,external_id
, external_type
、created_at
、updated_at
索引:
- 小学
(id)
- 独一无二
(external_id, external_type, type)
- 非唯一
(updated_at)
还有一个 settings table 列:id
、user_id
、name
、value
, created_at
, updated_at
, type
索引:
- 小学
(id)
- 独一无二
(user_id, name)
- 非唯一
(user_id)
- 非唯一
(updated_at)
我执行查询:
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”
在解释报告中,我看到:
- 对于用户 table,(external_id、external_type、类型)索引被识别为可能的键,但 NOT使用
- 设置table使用(user_id,名称)索引
目标
- 我想优化这个查询
- 所以我想让用户 table 使用 (external_id, external_type, type) 复合索引
我为调试所做的事情:
- 如果我更改 SELECT 语句的第一行以删除 users.created_at、users.updated_at,它会使用索引
- 如果我尝试为用户添加一个 (external_id, external_type) 非唯一索引 table,它仍然没有使用它
- 如果我将查询的 WHERE 子句更改为添加 users.type=“Blah”,它会使用索引
我错过了什么?
这回答了问题的原始版本。
您可能会通过使用 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_id
和 external_type
的用户,只需在索引中查找即可。然后它将使用 user_id
在 users
table 中查找相应的信息。这应该是最快的方法。
实际上,您可以免费获得第二个,因为它是主键。我懒得创建覆盖索引,因为您要选择这么多列。但这可能会提供略微更好的性能。
不确定您使用的 mysql 是哪个版本。 8.0之前,mysqlinnodb不持久化统计,如果你的数据是倾斜的,内存中的统计很难代表数据。在您的情况下,查询优化器可能认为 table 扫描是最快的,如果统计信息表明 table 用户中的大部分数据 external_id = 3 和 external_type = 'Owner'因为table上没有索引覆盖选中的列,如果使用索引,查询引擎需要根据索引查找数据。
当您更改为 SELECT 索引中的唯一列时,索引将成为覆盖索引,查询引擎将不需要进行查找。
避免双重查找
您的索引是 (external_id, external_type, type)
,但为了获得查询所需的所有信息,它必须使用该索引来查找行,然后使用自动包含的 id
在该索引的末尾查找主 table.
created_at
和 updated_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.