CREATE INDEX CONCURRENTLY (CIC)大概是DBA们最常用的语句之一,创建索引时只加4级锁,不阻塞DML。听上去非常美好,但在大事务、长事务较多的系统,可能被阻塞得一个中午也建不上一个索引。本篇就从这个无法创建的索引开始,学习CIC的过程、原理以及注意事项。
create table test(id int);
INSERT INTO test(id) VALUES (generate_series(1, 10000));
create table tmp02(a int);
insert into tmp02 values(1);
会话1
select count(*) from test a, test b;
会话2
create index concurrently ind_01 on tmp02(a);
可以看到,即使test和tmp02都不是同一个表,test执行的都不是dml语句,tmp02的索引创建依然被阻塞了。如果会话1中是要执行好几个小时的查询,会话2的索引创建也将一直被阻塞。
查看等待情况
SELECT pid, locktype,virtualxid,relation::regclass, mode FROM pg_locks where granted='f' order by pid;
SELECT pid, locktype,virtualxid,relation::regclass, mode FROM pg_locks where granted='t' order by pid;
我们知道查询语句执行时会获取一个virtualxid(dml语句也会),但为什么创建索引要跟它获取同一个?令人迷茫。
看看执行的函数堆栈,发现DefineIndex调用一个函数叫WaitForOlderSnapshots,它在等更旧的快照。
CIC与HOT息息相关,新建索引后,HOT更新必须符合相应规则。关于HOT,参考:
《PostgreSQL面试题集锦》学习与回答_Hehuyi_In的博客-CSDN博客
postgresql_internals-14 学习笔记(一)-CSDN博客
结合官方文档及网上文章的介绍,CIC的创建可以概括为:三个阶段、两次扫描、三次等待。
初始表状态,索引尚未创建
此阶段后,新事务会看到表中有一个invalid索引(但不可读写),因此此后需要考虑HOT-safe,避免更新索引键值字段导致HOT断链。
等待原因:虽然新索引此时还不能读写,但新事务已经能看到它的存在,此后再对该表进行修改时,必须保证HOT链满足新索引定义。即更新到新索引字段时,需要产生新的HOT链。而早于阶段1开始的事务无法看到新索引,还会按原先的规则进行HOT更新,无法满足要求。
此阶段后,索引可写入但不能查询(因为数据还不一致),其他事务修改该表时,需要维护新索引。
第三阶段实际就是补数据,保证数据一致性。
等待原因:Phase2中事务结束前开始的事务,无法看到新索引已变为可写状态,修改基表时并不维护新索引。
等待原因:旧事务的快照可以看到比构建索引时的快照更旧的行,如果它们使用新索引进行查询,可能索引中会没有它们想要看到的旧数据,导致数据不一致(例如下图中索引并没有值为b的数据,但旧事务可能看到此值)。因此,第3阶段必须等所有旧读写事务结束,才能将新索引置为可读状态。
至此,索引对所有事务可用。
再从源码层学习下CIC的创建过程,DefineIndex函数位于indexcmds.c文件,这里只根据创建阶段截取部分代码。
可以看到锁模式
lockmode = concurrent ? ShareUpdateExclusiveLock : ShareLock;
rel = table_open(relationId, lockmode);
另外分区表不支持CIC
partitioned = rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE;
if (partitioned)
{
/*
* Note: we check 'stmt->concurrent' rather than 'concurrent', so that
* the error is thrown also for temporary tables. Seems better to be
* consistent, even though we could do it on temporary table because
* we're not actually doing it concurrently.
*/
if (stmt->concurrent)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot create index on partitioned table \"%s\" concurrently",
RelationGetRelationName(rel))));
…
}
indexRelationId =
index_create(rel, indexRelationName, indexRelationId, parentIndexId,
parentConstraintId,
stmt->oldNode, indexInfo, indexColNames,
accessMethodId, tablespaceId,
collationObjectId, classObjectId,
coloptions, reloptions,
flags, constr_flags,
allowSystemTableMods, !check_rights,
&createdConstraintId);
index_create函数
/*
* store index's pg_class entry
*/
InsertPgClassTuple(pg_class, indexRelation,
RelationGetRelid(indexRelation),
(Datum) 0,
reloptions);
/* ----------------
* update pg_index
* (append INDEX tuple)
*
* Note that this stows away a representation of "predicate".
* (Or, could define a rule to maintain the predicate) --Nels, Feb '92
* ----------------
*/
UpdateIndexRelation(indexRelationId, heapRelationId, parentIndexRelid,
indexInfo,
collationObjectId, classObjectId, coloptions,
isprimary, is_exclusion,
(constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) == 0,
!concurrent && !invalid,
!concurrent);
UpdateIndexRelation函数
/*
* Build a pg_index tuple
*/
…
values[Anum_pg_index_indisvalid - 1] = BoolGetDatum(isvalid);
values[Anum_pg_index_indisready - 1] = BoolGetDatum(isready);
values[Anum_pg_index_indislive - 1] = BoolGetDatum(true);
…
LockRelationIdForSession(&heaprelid, ShareUpdateExclusiveLock);
PopActiveSnapshot();
CommitTransactionCommand();
StartTransactionCommand();
此阶段后,新事务会看到表中有一个invalid索引(但不可读写),因此此后需要考虑HOT-safe,避免更新索引键值字段导致HOT断链。
WaitForLockers(heaplocktag, ShareLock, true);
/* Set ActiveSnapshot since functions in the indexes may need it */
PushActiveSnapshot(GetTransactionSnapshot());
/* Perform concurrent build of index */
index_concurrently_build(relationId, indexRelationId);
index_concurrently_build函数调用index_build函数
/* Now build the index */
index_build(heapRel, indexRelation, indexInfo, false, true);
/*
* Update the pg_index row to mark the index as ready for inserts. Once we
* commit this transaction, any new transactions that open the table must
* insert new entries into the index for insertions and non-HOT updates.
*/
index_set_state_flags(indexRelationId, INDEX_CREATE_SET_READY);
index_set_state_flags函数
/* Perform the requested state change on the copy */
switch (action)
{
case INDEX_CREATE_SET_READY:
/* Set indisready during a CREATE INDEX CONCURRENTLY sequence */
Assert(indexForm->indislive);
Assert(!indexForm->indisready);
Assert(!indexForm->indisvalid);
indexForm->indisready = true;
break;
…
}
/*
* Commit this transaction to make the indisready update visible.
*/
CommitTransactionCommand();
StartTransactionCommand();
此阶段后,索引可写入但不能查询(因为数据还不一致),其他事务修改该表时,需要维护新索引。
第三阶段实际就是补数据,保证数据一致性。
WaitForLockers(heaplocktag, ShareLock, true);
snapshot = RegisterSnapshot(GetTransactionSnapshot());
PushActiveSnapshot(snapshot);
limitXmin = snapshot->xmin;
PopActiveSnapshot();
UnregisterSnapshot(snapshot);
CommitTransactionCommand();
StartTransactionCommand();
WaitForOlderSnapshots(limitXmin, true);
index_set_state_flags(indexRelationId, INDEX_CREATE_SET_VALID);
index_set_state_flags函数
/* Perform the requested state change on the copy */
switch (action)
{
case INDEX_CREATE_SET_VALID:
/* Set indisvalid during a CREATE INDEX CONCURRENTLY sequence */
Assert(indexForm->indislive);
Assert(indexForm->indisready);
Assert(!indexForm->indisvalid);
indexForm->indisvalid = true;
break;
…
}
CacheInvalidateRelcacheByRelid(heaprelid.relId);
UnlockRelationIdForSession(&heaprelid, ShareUpdateExclusiveLock);
pgstat_progress_end_command();
return address;
}
参考
PostgreSQL create index concurrently原理分析 – 数据库内核研究
Explaining CREATE INDEX CONCURRENTLY - 2ndQuadrant | PostgreSQL
http://mysql.taobao.org/monthly/2020/09/05/
PostgreSQL: Documentation: 14: CREATE INDEX
https://developer.aliyun.com/article/590359