[英]MySQL optimizer index choice for simple select query
我有一个名为PendingExpense
的表,它有几个简单的列,但有很多行。 我正在处理一些针对分页 GET 响应的查询,但是在使用查询时遇到了一些混乱,MySQL 优化器似乎做出了一个毫无意义的决定,即在从WHERE
子句过滤之前对ORDER BY
子句进行全索引扫描。
这是在 MySQL 版本 8.0.23 上。
PendingExpense
DDL(注意,companyId 和 loginCredentialId 是我在架构中指定用户的方式):
create table PendingExpense
(
ID bigint auto_increment primary key,
LOGINCREDENTIALID int null,
COMPANYID int null,
DATE datetime null,
-- ... other rows that don't pertain, e.g. amount, status, type, state, country, merchant
)
create index IN_PendingExpense_COMPANYID_ASC_LOGINCREDENTIALID_ASC
on PendingExpense (COMPANYID, LOGINCREDENTIALID);
create index IN_PendingExpense_LOGINCREDENTIALID_ASC
on PendingExpense (LOGINCREDENTIALID);
create index IN_PendingExpense_Date
on PendingExpense (DATE);
然后这是我正在比较的两个查询,除了索引提示之外它们是相同的。 我在下面包括了两者的执行计划:
查询 1(无提示):
explain analyze select id from PendingExpense
where COMPANYID = 1641 and LOGINCREDENTIALID = 2451
order by date DESC, id DESC
limit 101; -- takes 5.5 seconds
-> Limit: 101 row(s) (cost=2356102.00 rows=101) (actual time=2292.676..4474.843 rows=101 loops=1)
-> Filter: ((PendingExpense.LOGINCREDENTIALID = 2451) and (PendingExpense.COMPANYID = 1641)) (cost=2356102.00 rows=105) (actual time=2292.675..4474.818 rows=101 loops=1)
-> Index scan on PendingExpense using IN_PendintExpense_Date (reverse) (cost=2356102.00 rows=5660) (actual time=0.088..4371.774 rows=1491859 loops=1)
查询 2(索引提示):
explain analyze select id from PendingExpense use index (IN_PendingExpense_COMPANYID_ASC_LOGINCREDENTIALID_ASC)
where COMPANYID = 1641 and LOGINCREDENTIALID = 2451
order by date desc, id desc
limit 101; -- .184 seconds
-> Limit: 101 row(s) (cost=9722.30 rows=101) (actual time=38.255..38.267 rows=101 loops=1)
-> Sort: PendingExpense.`DATE` DESC, PendingExpense.ID DESC, limit input to 101 row(s) per chunk (cost=9722.30 rows=27778) (actual time=38.254..38.259 rows=101 loops=1)
-> Index lookup on PendingExpense using IN_PendingExpense_COMPANYID_ASC_LOGINCREDENTIALID_ASC (COMPANYID=1641, LOGINCREDENTIALID=2451) (actual time=0.046..35.410 rows=14170 loops=1)
从本质上讲,我很困惑为什么 MySql 选择在过滤 companyId / loginCredentialId 之前先进行完整索引扫描,而这两个索引已经存在,从而导致效率显着降低。 我更希望不必在我的代码/查询中指定索引提示以保持整洁。 我的印象是 MySQL 通常选择首先运行 where 子句过滤,特别是如果它已经存在索引。
任何帮助/提示/见解都会在这里受到赞赏。 谢谢!
这个复合的覆盖索引应该非常适合该查询:
INDEX(COMPANYID, LOGINCREDENTIALID, -- in either order
date, id) -- last, in this order
前两列通过=
进行测试,从而可以精确找到 INDEX 行。
最后两行可以向后扫描以完美地通过索引。
'覆盖'
由于所有必要的行都在索引中(因此“覆盖”又名“使用索引”),因此不需要触及数据的 BTree。
整个表存在于 B+Tree 中; 它由PRIMARY KEY
排序。 因此,基于 PK 查找单行或行范围是有效的。
每个“二级”索引都是一个非常相似的 B+Tree。 它包含索引中指定的所有列,以及(静默)PK 的所有列。 也就是说,与
PRIMARY KEY(id), INDEX(foo, bar)
二级索引实际上是由(foo, bar, id)
索引的 B+Tree。 当这些列是SELECT
所需的全部时,索引是“覆盖”的,并且只查看 B+Tree。 如果您需要其他列,则id
(在此示例中)用于根据id
进入数据的 BTree 以查找其他列。
“全表扫描”或“全索引扫描”
如果没有索引(PK,也不是辅助索引)用于定位请求的行,则查询将执行“全表扫描”,检查每一行是否相关。 同样,当使用“覆盖”索引时,它可能会使用“完整索引扫描”。
继续上面的示例(并假设另一列x
不在任何索引中),
SELECT * FROM t WHERE id=5; -- point query
SELECT COUNT(*) FROM t WHERE foo=5; -- covering
SELECT bar FROM t WHERE foo=5; -- covering
SELECT x FROM t WHERE foo=5; -- well indexed (but not covering)
SELECT COUNT(*) FROM t WHERE bar=5; -- full index scan (covering but slow)
SELECT * FROM t WHERE bar=5; -- full index scan (plus lookup)
SELECT COUNT(*) FROM t WHERE x=5; -- full table scan
SELECT * FROM t WHERE x=5; -- full table scan
(这些示例是有序的,最快的在前。)
SELECT COUNT(*) ...
返回 1 行。 SELECT * ...
可能会返回许多行,因此可能会更慢。
您的优化查询将首先包含 where 子句,然后是 order by。 所以我会有一个索引
( COMPANYID, LOGINCREDENTIALID, DATE, ID )
公司和证书涵盖 where 子句。 然后是 order by 子句的日期和 ID。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.