PostgreSQL 中的并發創建索引
一、初體驗 Create Index Concurrently
在 PostgreSQL 11 之前,創建索引和表數據更新是互斥的,也就是說創建索引時會持有一把鎖,這時候任何對表數據的增加、更新、刪除操作,都將等待索引創建完成才能繼續執行。
如下面的例子:
- 創建示例表
- 打開一個 psql 客戶端,執行創建索引操作
- 索引創建時打開另一個 psql 客戶端,并向表中插入數據,此時另一個事務已經持有表鎖了,所以會一直等待事務結束之后才會繼續執行
可以在事務執行期間,通過 pg_locks 表查看事務持有的鎖,可以看到創建索引的操作占據了 ShareLock(5 號鎖),插入操作需要獲取 RowExclusiveLock 鎖,而這兩者是互斥的。
索引創建和表更新操作的互斥,帶來一個嚴重的后果,那便是如果表數據量較大,創建索引的時間可能很長,如果長時間鎖表的話,會導致表無法更新,可能會對在線業務產生很大的影響。
于是 PostgreSQL 在 11 版本中支持了并發創建索引,即 CREATE INDEX CONCURRENTLY,其主要功能是在創建索引的時候,不阻塞表數據的更新。
還是看上面的示例,只需要將第一個事務的 sql 修改為 create index CONCURRENTLY idx_a on articles (a);,那么其他事務的表數據更新操作將會正常執行,不會被阻塞。
然后再看其持有的鎖,可以看到已經變成了 ShareUpdateExclusiveLock(4 號鎖):
在并發創建索引的時候,如果遇到了不符預期的錯誤,或者手動取消,那么這個索引將會留在表中,但是被標識為 INVALID,表示這個索引不可用,也就是說將不會使用這個索引進行索引掃描。
后續可以手動將其 DROP 掉,然后重新建立索引,也可以執行 REINDEX CONCURRENTLY 重建索引。
注意:CREATE INDEX CONCURRENTLY 不能在事務塊中執行,也就是說我們不能顯式的 begin 開啟事務然后執行 CREATE INDEX CONCURRENTLY。
二、CREATE INDEX CONCURRENTLY 的三個步驟
主要的代碼位置在 https://github.com/postgres/postgres/blob/master/src/backend/commands/indexcmds.c#L488
DefineIndex 方法中主要是處理索引創建的邏輯,方法前面部分主要是做一系列校驗和參數初始化等,然后調用 index_create 方法將索引的元信息存儲到 pg_index、pg_class 等表中。
并且如果判斷到不是 concurrently 創建索引的話,這里會直接返回,也就是說這之后的邏輯都是處理 CONCURRENTLY 并發索引創建的部分。
接著上面的代碼往下看,就是 postgres 的并發創建索引邏輯,主要分為了三個步驟,這部分代碼的注釋也有一些相應的說明。
- 開啟一個事務,通過 index_create 方法,將索引的元數據信息存儲到 pg_class、pg_index 表中,并且標識索引的 indisready 和 indisvalid 屬性為 false,表示目前索引沒有 ready 并且不可用_;_提交事務,并開啟一個新的事務進入下一階段。
此階段相當于 DefineIndex 的前一部分,和正常的 create index 的邏輯是相同的。
- 1. 進入此階段時,需要等待系統中其他正在寫數據的事務提交,因為必須讓新索引的定義對其他所有的事務都可見,保證 HOT 更新滿足表的索引定義,調用 WaitForLockers 函數進行等待 2. 通過 index_concurrently_build 創建索引,同時持有 4 號鎖,不阻塞表的增/刪/改操作;獲取一個 MVCC 快照,將此快照下可見的元組寫入到索引中;此時如果有其他的事務有新的寫入,將不會插入到索引中,只是保證 HOT 滿足索引的定義;提交事務,將索引設置為 isready,后續如果有新的數據插入,將會維護這個索引,但是索引仍然是 invalid,不能用于索引掃描。并再開啟一個新的事務,進入到下一階段。
- 1. 又要等待當前所有寫事務都提交,保證所有的事務都能看到新索引的定義 2. 在第二階段的執行過程中,有可能又有新的元組插入到表中,所以需要再獲取一個新的 MVCC 快照,并將新的元組插入到索引中,調用函數 validate_index 3. 此時還需要一次等待,將指定 xmin 的之前的事務提交,調用函數 WaitForOlderSnapshots 4. 最后將索引置為 valid,后續其他事務便可以使用該索引進行查詢了
三、Reindex Concurrently
REINDEX 是一個更加復雜的命令,PostgreSQL 中也是支持對 REINDEX 進行 CONCURRENTLY 操作的,了解了 CREATE INDEX 之后,我們再來看看 Reindex Concurrently 是如何在 PostgreSQL 上執行的。
ExecReindex
PostgreSQL 的 REINDEX 的主要邏輯在方法 ExecReindex 中,對 Reindex 的處理分為了三種情況:
- REINDEX_OBJECT_INDEX(針對索引)
- REINDEX_OBJECT_TABLE(針對表)
- REINDEX_OBJECT_SCHEMA、REINDEX_OBJECT_SYSTEM、REINDEX_OBJECT_DATABASE(針對 schema、系統表、整個庫)

ReindexRelationConcurrently
這個方法是 Reindex Concurrently 的主要實現邏輯,首先會根據傳入的 relationOid,找到所有需要進行 Reindex 的 indexId,并且跳過一些不能進行 Reindex 的索引,例如系統 catalog 表不支持 Reindex。
主要的代碼位置:https://github.com/greenplum-db/gpdb/blob/main/src/backend/commands/indexcmds.c#L3575
拿到需要進行 Reindex 的索引 Oid 之后,然后進入 Reindex Concurrently 的六個階段:
- 創建新的索引,創建后表中有一個臨時的新的索引,名稱以 idx_ccnew 開頭
- build 新創建的索引,即掃描全表數據,構建索引的內容
- validate 新創建的索引,將前一個階段新插入的數據加入到索引中,這個和 create index concurrently 類似
- 交換索引,將新創建的索引和它對應的需要 reindex 的索引進行交換,舊的索引標識為 invalid
- 將舊的索引設置為 dead 狀態,即 indislive、indisready、indisvalid 均為 false
- 最后將舊的索引 drop 掉
ps. 在 Postgres 的官方文檔中,也有對 Create Index/Reindex Concurrently 的描述,只是沒有深入到代碼細節之中,可以參考看下這兩個步驟的執行步驟。
























