使用索引有性能成本。如果匹配的百分比是整个表的一小部分,则不必扫描整个表就可以弥补这一成本。但是如果匹配的比例很大,那么简单地读取表格会更快。
读取索引是有成本的。一个小的、经常使用的索引可能在内存中,但一个大的或不经常使用的索引可能在磁盘上。这意味着搜索索引和获取匹配行号的磁盘访问速度较慢。如果查询与少量行匹配,则此开销胜过搜索整个表。如果查询匹配大量的行,这个开销就是浪费;无论如何,您将不得不阅读整个表格。
然后是 IO 成本。 使用磁盘顺序读写比随机读写要快得多。我们说话的速度快了 10 到 100 倍。
旋转磁盘有一个物理部分,即磁头,它必须四处移动以读取磁盘的不同部分。移动所需的时间称为“寻道时间”。当您在表中的行之间跳来跳去时,可能是乱序的,这是random access 并会导致查找时间。相比之下,读整张表很可能是一长串连续读;头部不必跳来跳去,没有寻道时间。
SSD 的速度要快得多,无需移动任何物理部件,但它们仍然是 much faster for sequential access than random。
另外,随机访问在操作系统和磁盘之间有更多的开销;它需要更多说明。
因此,如果数据库决定一个查询将匹配表中的大多数行,它可以决定按顺序读取它们并清除不匹配的行比通过索引查找行并使用较慢的随机访问。
考虑一组邮政信箱,每个信箱都在一个大网格中编号。按数字查找每个盒子非常快,但从一个盒子开始并按顺序打开它们要快得多。我们有谁拥有哪个盒子以及他们住在哪里的索引。
您需要获取南北港的邮件。您在索引中查找属于 South Northport 的某个人的邮箱,发现其中只有几个,然后单独获取邮件。这是一个索引查询和随机访问。速度很快,因为只需检查几个邮箱。
现在我请您为所有人接收邮件,但 South Northport。您可以反向使用索引:获取 South Northport 的箱子列表,从每个箱子的列表中减去这些箱子,然后单独获取每个箱子的邮件。但这将是缓慢的随机访问。相反,由于无论如何您都将不得不打开几乎每个箱子,因此最好按顺序检查每个箱子,看看它是否是寄往 South Northport 的邮件。
更正式地说,索引与表扫描的性能是这样的。
# Indexed query
C[index] + (C[random] * M)
# Full table scan
(C[sequential] + C[match]) * N
其中C 是各种常量成本(或足够接近的常量),M 是匹配行数,N 是表中的行数。
我们知道 C[sequential] 比 C[random] 快 10 到 100 倍。因为磁盘访问比 CPU 或内存操作慢得多,C[match](检查行是否匹配的成本)与C[sequential] 相比会相对较小。更正式的...
C[random] >> C[sequential] >> C[match]
使用它我们可以假设C[sequential] + C[match] 是C[sequential]。
# Indexed query
C[index] + (C[random] * M)
# Full table scan
C[sequential] * N
当M << N 索引查询获胜。当 M 接近 N 时,全表扫描获胜。
请注意,使用索引的成本并不是固定不变的。 C[index] 是诸如加载索引、查找键和读取行 ID 之类的事情。这取决于索引的大小、索引的类型以及它是在磁盘上(冷)还是在内存中(热),这可能是非常可变的。这就是为什么当您第一次启动数据库服务器时,前几个查询通常相当慢的原因。
在现实世界中,它比这更复杂。实际上,行被分解为data pages,并且数据库有许多技巧来优化查询和磁盘访问。但是,通常情况下,如果您匹配大部分行,则全表扫描将优于索引查找。
如今,哈希索引的用途有限。它是一个简单的键/值对,只能用于相等性检查。大多数数据库使用B-Tree 作为标准索引。它们的成本稍高一些,但可以处理更广泛的操作,包括相等、范围、比较和前缀搜索,例如 like 'foo%'。
Postgres Index Types documentation 很好地概括了索引类型的各种优缺点。