PostgreSQL create index concurrently原理分析

PostgreSQL create index concurrently原理分析

使用create index(不使用concurrently选项)对表建立索引时,需要对表加Share锁,即5号锁。Share锁会阻止其它事务对表进行修改(插入、更新和删除)。使用concurrently选项创建索引时只需要对表加4号锁,允许其它事务对表进行并发修改。为了处理和其它写事务的并发,使用concurrently选项时,需要使用2个MVCC快照对表进行2次全表扫描,共分为3个阶段,使用3个事务。

下面描述create index concurrently new_index on t(a)的过程。主要函数是DefineIndex、validate_index。这两个函数对下面三个阶段进行了注释,读者可以自行翻阅。

第一阶段

将new_index的定义写入相关系统表,如pg_class、pg_index,在pg_index中把new_index的indisready标记(是否允许其它事务插入索引)、indisvalid标记(是否可以用来查询)设置为false。虽然此时new_index既不允许插入,也不可用,但是,其它事务见到new_index的定义之后,再对基表t进行修改时,必须保证HOT链满足new_index的定义。即,对某些更新而言,不能再使用HOT更新,而是需要产生新的HOT链。

获取一个session锁,防止第一个事务提交之后,表t或索引new_index被其它事务删除。

这个阶段需要处理的工作到这里就结束了,将第一个事务提交,开启一个新事务,开始第二阶段。

第二阶段

此时需要进行一段时间的等待,因为在开启第二阶段之前,必须保证整个系统中,new_index的定义对所有其它事务都可见。即,保证所有其它事务的HOT更新都满足new_index的定义。如何等待呢?等待其它正在写基表t的事务结束即可,PG使用函数WaitForLockers(heaplocktag, ShareLock, true)进行等待。

等待结束后,所有再需要打开基表t进行写入的事务,都可以看到new_index的定义,它们保证对基表t的HOT更新满足此索引的定义。

获取一个MVCC快照,对基表t进行一次全表扫描。把对此快照可见的所有元组插入new_index中,和不使用concurrently选项不同,使用MVCC快照并不索引recently-dead的元组。在这个阶段,其它事务对基表t进行写入时,并不维护索引(不向new_index中插入索引项),仅仅保证HOT更新满足new_index定义。

扫描结束后,将indisready标记为true,其它事务修改基表时,需要维护索引,但是indisvalid仍然为false(仍然不能用来查询)。

将第二个事务提交,开启第三个事务。

第三阶段

此时又需要进行一段时间的等待,因为,第三阶段开始后,必须保证后续其它事务都自己维护new_index。这里再次使用WaitForLockers(heaplocktag, ShareLock, true)进行等待。

等待结束后,所有其它事务都会自己维护new_index。我们只需要将在第二个阶段中其它事务对基表t的修改反应到索引上即可。

再获取一个新的MVCC快照,进行一次全表扫描,将索引中缺少的元组添加到索引中。添加数据的过程,类似merge join:将索引中heap的ctid找出来,排序;全表扫描,找出heap上可见元组的ctid;进行merge join。因为HOT的存在,这个merge join的过程有点复杂,详细过程参见validate_index_heapscan。

此时,又需要进行等待,这次等待之后,其它事务就可以用new_index进行查询了。这次等待的原因是:我们在第二、三阶段都没有将recently-dead的元组添加到索引中,如果某个事务持有一个很老的快照,这个事务使用new_index进行扫描时,就会少数据。因此,需要等待整个系统中持有太老快照的事务结束,这由WaitForOlderSnapshots(limitXmin, true)完成。

等待结束,将indisvalid标记为true,然后发送失效消息。索引就可以被后续的查询使用了。

Comments are closed.