我有这个表(简化版)
create table completions (
id int(11) not null auto_increment,
completed_at datetime default null,
is_mongo_synced tinyint(1) default '0',
primary key (id),
key index_completions_on_completed_at_and_is_mongo_synced_and_id (completed_at,is_mongo_synced,id),
) engine=innodb auto_increment=4785424 default charset=utf8 collate=utf8_unicode_ci;
尺寸:
select count(*) from completions; -- => 4817574
现在我尝试执行此查询:
select completions.*
from completions
where
(completed_at is not null)
and completions.is_mongo_synced = 0
order by completions.id asc limit 10;
需要 9分钟。
我看到没有使用任何索引,explain extend
会返回此信息:
id: 1
select_type: SIMPLE
table: completions
type: index
possible_keys: index_completions_on_completed_at_and_is_mongo_synced_and_id
key: PRIMARY
key_len: 4
ref: NULL
rows: 20
filtered: 11616415.00
Extra: Using where
如果我强制索引:
select completions.*
from completions
force index(index_completions_on_completed_at_and_is_mongo_synced_and_id)
where
(completed_at is not null)
and completions.is_mongo_synced = 0
order by completions.id asc limit 10;
需要 1,22s ,这要好得多。 explain extend
返回:
id: 1
select_type: SIMPLE
table: completions
type: range
possible_keys: index_completions_on_completed_at_and_is_mongo_synced_and_id
key: index_completions_on_completed_at_and_is_mongo_synced_and_id
key_len: 6
ref: null
rows: 2323334
filtered: 100
Extra: Using index condition; Using filesort
现在,如果我按completions.id
缩小查询范围,例如:
select completions.*
from completions
force index(index_completions_on_completed_at_and_is_mongo_synced_and_id)
where
(completed_at is not null)
and completions.is_mongo_synced = 0
and completions.id > 2000000
order by completions.id asc limit 10;
需要 1,31s ,仍然很好。 explain extend
返回:
id: 1
select_type: SIMPLE
table: completions
type: range
possible_keys: index_completions_on_completed_at_and_is_mongo_synced_and_id
key: index_completions_on_completed_at_and_is_mongo_synced_and_id
key_len: 6
ref: null
rows: 2323407
filtered: 100
Extra: Using index condition; Using filesort
关键是如果对于最后一个查询我不强制索引:
select completions.*
from completions
where
(completed_at is not null)
and completions.is_mongo_synced = 0
and completions.id > 2000000
order by completions.id asc limit 10;
85ms ,检查 ms ,而不是 s 。 explain extend
返回:
id: 1
select_type: SIMPLE
table: completions
type: range
possible_keys: PRIMARYindex_completions_on_completed_at_and_is_mongo_synced_and_id
key: PRIMARY
key_len: 4
ref: null
rows: 2323451
filtered: 100
Extra: Using where
这不仅令我感到疯狂,而且还因为过滤器数量的微小变化对最后一个查询的性能产生了很大的影响:
select completions.*
from completions
where
(completed_at is not null)
and completions.is_mongo_synced = 0
and completions.id > 1600000
order by completions.id asc limit 10;
13s
我不明白的事情:
查询A:
select completions.*
from completions
where
(completed_at is not null)
and completions.is_mongo_synced = 0
and completions.id > 2000000
order by completions.id asc limit 10;
85ms
查询B:
select completions.*
from completions
force index(index_completions_on_completed_at_and_is_mongo_synced_and_id)
where
(completed_at is not null)
and completions.is_mongo_synced = 0
and completions.id > 2000000
order by completions.id asc limit 10;
1,31s
查询A:
select completions.*
from completions
where
(completed_at is not null)
and completions.is_mongo_synced = 0
and completions.id > 2000000
order by completions.id asc limit 10;
85ms
查询B:
select completions.*
from completions
where
(completed_at is not null)
and completions.is_mongo_synced = 0
and completions.id > 1600000
order by completions.id asc limit 10;
13S
索引:
key index_completions_on_completed_at_and_is_mongo_synced_and_id (completed_at,is_mongo_synced,id),
查询:
select completions.*
from completions
force index(index_completions_on_completed_at_and_is_mongo_synced_and_id)
where
(completed_at is not null)
and completions.is_mongo_synced = 0
and completions.id > 2000000
order by completions.id asc limit 10;
评论中要求的更多数据
基于is_mongo_synced
值的行数
select
completions.is_mongo_synced,
count(*)
from completions
group by completions.is_mongo_synced;
结果:
[
{
"is_mongo_synced":0,
"count(*)":2731921
},
{
"is_mongo_synced":1,
"count(*)":2087869
}
]
没有order by
的查询
select completions.*
from completions
where
(completed_at is not null)
and completions.is_mongo_synced = 0
and completions.id > 2000000
limit 10;
544ms
select completions.*
from completions
force index(index_completions_on_completed_at_and_is_mongo_synced_and_id)
where
(completed_at is not null)
and completions.is_mongo_synced = 0
and completions.id > 2000000
limit 10;
314ms
但是,无论如何,我需要订单,因为我正在批量扫描表。
答案 0 :(得分:4)
你的问题非常复杂。但是,您的第一个问题是:
select completions.*
from completions
where completed_at is not null and
completions.is_mongo_synced = 0
order by completions.id asc
limit 10;
(is_mongo_synced, completed_at)
上的最佳索引。可能还有其他方法来编写查询,但在您强制使用的索引中,列不是最佳顺序。
第二个查询的性能差异可能是因为数据实际上正在排序。一些额外的数十万行可能会影响排序时间。对id
的值的依赖可能是不使用索引的方式。如果您将索引更改为(is_mongo_synced, id, completed_at)
,则更有可能使用索引。
MySQL有很好的复合索引文档。您可能需要查看here。
添加索引后:
KEY `index_completions_on_is_mongo_synced_and_id_and_completed_at` (`is_mongo_synced`,`id`,`completed_at`) USING BTREE,
再次执行长查询
select completions.*
from completions
where
(completed_at is not null)
and completions.is_mongo_synced = 0
order by completions.id asc limit 10;
156ms ,非常好。
检查explain extended
我们看到MySQL正在使用正确的索引:
id: 1
select_type: SIMPLE
table: completions
type: ref
possible_keys: index_completions_on_completed_at_and_is_mongo_synced_and_id,index_completions_on_is_mongo_synced_and_id_and_completed_at
key: index_completions_on_is_mongo_synced_and_id_and_completed_at
key_len: 2
ref: const
rows: 1626322
filtered: 100
Extra: Using index condition; Using where
答案 1 :(得分:3)
你正试图强迫索引
(completed_at, is_mongo_synced, id)
它是一个b树,它必须首先探索completed_at
不是NULL
的所有不同值,然后对每个值进行正确的mongo_synced,它们会收集所有ID并对它们进行排序,最后访问该表以获取所需的行。
另一方面,使用主键(假设它是聚类键),它只是跳转获取具有completions.id>的页面。 2000000并读取连续的行,直到它收集其中10个,如果不在此页面上,则获取下一个。
最后,两个查询都可能检查表中相似数量的页面+第一个必须获取整个索引并对其进行排序。
如果您想使用索引,请尝试
(is_mongo_synced, id, completed_at)
请参阅clustered indexes上的手册。
答案 2 :(得分:2)
警告:我假设InnoDB。
建立最佳指数
is_mongo_synced
。这使得查找在索引中的一个连续点中查找。如果添加completed_at
,它将扫描所有非NULL条目,收集ids
以进行后续排序。排序(ORDER BY
)会花费一些成本,INDEX(is_mongo_synced, completed_at, ...)
无法避免。
如果您添加id
,现在有可能避免排序。但它仍然必须完成过滤(以避免NULL completed_at
行)。因此,INDEX(is_mongo_synced, id, ...)
可能很好。
如果您有两个索引,优化器不擅长在这两个索引之间进行选择,因为它在很大程度上取决于数据的分布以及是否还有LIMIT
。您理解数据的人可能或者可能无法正确选择哪个索引会更好。
我说“......”。我的意思是你可以停在那里,或者你可以在索引中添加更多列。添加更多列会进入所谓的“覆盖索引”。如果 all SELECT
中提到的列在二级索引中存在(任何地方),那么它就是“覆盖”。所以?首先,让我备份......
在 secondary 索引中查找内容时,会在BTree底部找到PRIMARY KEY
。然后通过向下钻取聚类PK的BTree来查找其他列。这种额外的钻取可能是昂贵的。但...
如果索引是“覆盖”,则无需进行额外的BTree向下钻取。
你不小心有一个“覆盖”索引,但不是最佳顺序。需要扫描整个索引,然后进行排序。我的每个索引都避免扫描整个索引,从而可能更快。
通过添加额外的列,我有两个(竞争)覆盖索引:
KEY mci (is_mongo_synced, completed_at, id)
KEY mic (is_mongo_synced, id, completed_at)
旁边......由于PK会自动添加到每个辅助密钥中,即使我只提到前两列,也会存在这些3列索引。所以,如果你尝试2和3,不要感到困惑,但没有发现差异。
为清楚起见,我会将'mci'和'mic'留下3个明确的列。
重新分析它们......
'mci'将扫描包含is_mongo_synced=0 AND completed_at IS NOT NULL
的索引部分。索引中的这些“行”是连续的,从而最小化磁盘命中。然后它会获取ID,然后对其进行排序。
'mic'将扫描包含is_mongo_synced=0
的索引部分。这比“mci”更重要。但是这些ID是有序的,从而消除了这种情况。但是,它现在必须在扫描索引时刮掉NULL行。
底线。我会用'mic'和'mci'替换你的复合索引('cmi')。
如果您有其他查询,例如查看特定完成日期的查询,您可能仍需要以completed_at
开头的索引。
另见我的索引食谱:mysql.rjweb.org/doc.php/index_cookbook_mysql。
还有一件事......如果您需要的所有数据和/或索引块都在缓存中(“缓冲池”),则查询的运行速度可能是您访问磁盘的速度的10倍。请参阅innodb_buffer_pool_size
以调整该缓存 - 通常70%的可用内存是好的。你的9分钟测试气味就像缓冲池要冷或太小。