打開網易新聞 查看精彩圖片

導讀

TiDB 全局索引在分區(qū)表中提供了一種優(yōu)化查詢性能的新方式。與本地索引不同,全局索引通過打破索引與分區(qū)的一對一映射關系,提升了跨分區(qū)查詢的效率。本文將詳細介紹 TiDB 全局索引的工作原理、發(fā)展歷程以及創(chuàng)建方法,并通過性能測試和最佳實踐,幫助用戶更好地理解和應用全局索引,提高數據庫的查詢性能和整體效率。

打開網易新聞 查看精彩圖片

在 TiDB 中,全局索引是一種定義在分區(qū)表上的索引類型,它允許索引分區(qū)與表分區(qū)之間建立一對多的映射關系,即一個索引分區(qū)可以對應多個表分區(qū)。這與 TiDB 早期版本中的本地索引(Local Index)不同,本地索引的索引分區(qū)與表分區(qū)之間是一對一的映射關系,即一個分區(qū)對應一個局部的索引塊。

全局索引能覆蓋整個表的數據,使得主鍵和唯一鍵在不包含分區(qū)鍵的情況下仍能保持全局唯一性。此外,全局索引可以在一次操作中訪問多個分區(qū)的索引數據,而無需對每個分區(qū)的本地索引逐一查找,顯著提升了針對非分區(qū)鍵的查詢性能。

下圖簡單展示了本地索引和全局索引的區(qū)別

打開網易新聞 查看精彩圖片
打開網易新聞 查看精彩圖片

  • v7.6.0 版本之前:TiDB 僅支持分區(qū)表的本地索引。這意味著,對于分區(qū)表上的唯一鍵,必須包含表分區(qū)表達式中的所有列。如果查詢條件中沒有使用分區(qū)鍵,那么查詢將不得不掃描所有分區(qū),這會導致查詢性能下降。
  • v7.6.0 版本:引入了系統(tǒng)變量tidb_enable_global_index,用于開啟全局索引功能。然而,當時該功能仍在開發(fā)中,不推薦用戶啟用。
  • v8.3.0 版本:全局索引功能作為實驗性特性發(fā)布。用戶可以通過在創(chuàng)建索引時顯式使用GLOBAL關鍵字來創(chuàng)建全局索引。
  • v8.4.0 版本:全局索引功能正式成為一般可用(GA)特性。用戶可以直接使用 GLOBAL 關鍵字創(chuàng)建全局索引,而無需再設置系統(tǒng)變量tidb_enable_global_index。從這個版本開始,該系統(tǒng)變量被棄用,并且始終為ON。
  • v8.5.0 版本:全局索引功能支持了包含分區(qū)表達式中的所有列。
  • v9.0.0 版本:全局索引功能支持了非唯一索引的情況。在分區(qū)表中,除聚簇索引外都可以被創(chuàng)建為全局索引。

打開網易新聞 查看精彩圖片

在 TiDB 中,創(chuàng)建全局索引(Global Index)時,可以在CREATE INDEX或ALTER TABLE語句中使用GLOBAL關鍵字,或在建表時通過GLOBAL關鍵字或/*T![global_index] GLOBAL */注釋指定。

創(chuàng)建全局索引的語法:

CREATE[UNIQUE]INDEXindex_nameONtable_name(column_list)[GLOBAL];
ALTERTABLEtable_nameADD[UNIQUE]INDEXindex_name(column_list)[GLOBAL];

示例:

1.創(chuàng)建全局唯一索引:

CREATEUNIQUEINDEXidx_globalONemployees(email)GLOBAL;

此語句在employees表的email列上創(chuàng)建一個全局唯一索引,確保每個電子郵件地址在整個表中唯一。

2. 添加全局索引:

ALTERTABLEordersADDINDEXidx_global_order_date(order_date)GLOBAL;

此語句向orders表添加一個名為idx_global_order_date的全局索引,索引列為order_date。

3.在建表時創(chuàng)建全局索引:

CREATETABLE`sbtest`(
`id`intNOTNULL,
`k`intNOTNULLDEFAULT'0',
`c`charNOTNULLDEFAULT'',
KEY`idx1`(`k`)GLOBAL,
KEY`idx2`(`k`)/*T![global_index]GLOBAL*/
)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_bin
PARTITIONBYHASH(`id`)PARTITIONS5;

此語句在創(chuàng)建sbtest表時同時創(chuàng)建了兩個名為idx1和idx2的全局索引,兩個索引的索引列都為k。

打開網易新聞 查看精彩圖片

提升查詢性能

全局索引能夠有效提高檢索非分區(qū)列的效率。當查詢涉及非分區(qū)列時,全局索引可以快速定位相關數據,避免了對所有分區(qū)的全表掃描,可以顯著降低 cop task 的數量,這對于分區(qū)數量龐大的場景尤為有效。

經過測試,在分區(qū)數量為 100 的情況下,sysbenchselect_random_points場景得到了53 倍的性能提升。

增強應用靈活性

全局索引的引入,消除了分區(qū)表上唯一鍵必須包含所有分區(qū)列的限制。這使得用戶在設計索引時更加靈活,可以根據實際的查詢需求和業(yè)務邏輯來創(chuàng)建索引,而不再受限于表的分區(qū)方案。這種靈活性有助于更好地優(yōu)化查詢性能,滿足多樣化的業(yè)務需求。

減少應用修改工作量

在數據遷移和應用修改過程中,全局索引可以減少對應用的修改工作量。如果沒有全局索引,在遷移數據或修改應用時,可能需要調整分區(qū)方案或重寫查詢語句以適應索引的限制。有了全局索引之后,這些修改可以被避免,從而降低了開發(fā)和維護成本。

如在將 Oracle 數據庫中的某張表遷移到 TiDB 時,因為 Oracle 支持全局索引,可能在某些表上存在一些不包含分區(qū)列的唯一索引,在遷移過程需要對表結構進行調整,以適應 TiDB 的分區(qū)表限制。然而,隨著 TiDB 對全局索引的支持,用戶只需簡單地修改索引定義,將其設置為全局索引,即可與 Oracle 保持一致,從而顯著降低遷移成本。

打開網易新聞 查看精彩圖片

基本思想

在 TiDB 的分區(qū)表中,本地索引的鍵值前綴是分區(qū)表的 ID 而全局索引的前綴是表的 ID。這樣的改動確保了全局索引的數據在 TiKV 上分布是連續(xù)的,降低了查詢索引時 RPC 的數量。

CREATETABLE`sbtest`(
`id`int(11)NOTNULL,
`k`int(11)NOTNULLDEFAULT'0',
`c`char(120)NOTNULLDEFAULT'',
KEYidx(k),
KEYglobal_idx(k)GLOBAL
)partitionbyhash(id)partitions5;

以上面的表結構為例,idx為普通索引,global_idx為全局索引。索引idx的數據會分布在 5 個不同的 ranges 中,如PartitionID1_i_xxx,PartitionID2_i_xxx等,而索引global_idx的數據則會集中在一個 range (TableID_i_xxx) 內。

這樣當我們進行 k 相關的查詢時,如select * from sbtest where k > 1,通過索引idx會構造 5 個不同的 ranges,而通過全局索引global_idx則只會構造 1 個 range,每個 range 在 TiDB 中對應一個或多個 RPC 請求,這樣使用全局索引可以降低數倍的 RPC 請求數,從而提升查詢索引的性能。

下圖更加直觀地展示了在使用idx和global_idx兩個不同索引執(zhí)行select * from sbtest where k > 1查詢語句在 RPC 請求和數據流轉過程中的差異。

打開網易新聞 查看精彩圖片

編碼方式

在 TiDB 中,索引項被編碼為鍵值對。對于分區(qū)表,每個分區(qū)在 TiKV 層被視為一個獨立的物理表,擁有自己的partitionID。因此,分區(qū)表的索引項也被編碼為:

唯一鍵
Key:
-PartitionID_indexID_ColumnValues
Value:
-IntHandle
-TailLen_IntHandle
-CommonHandle
-TailLen_IndexVersion_CommonHandle
非唯一鍵
Key:
-PartitionID_indexID_ColumnValues_Handle
Value:
-IntHandle
-TailLen_Padding
-CommonHandle
-TailLen_IndexVersion

在全局索引中,索引項的編碼方式有所不同。為了使全局索引的鍵布局與當前索引鍵編碼保持兼容,新的索引編碼布局為:

唯一鍵
Key:
-TableID_indexID_ColumnValues
Value:
-IntHandle
-TailLen_PartitionID_IntHandle
-CommonHandle
-TailLen_IndexVersion_CommonHandle_PartitionID
非唯一鍵
Key:
-TableID_indexID_ColumnValues_Handle
Value:
-IntHandle
-TailLen_PartitionID
-CommonHandle
-TailLen_IndexVersion_PartitionID

這種編碼方式使得全局索引的鍵以TableID開頭,而PartitionID被放置在 Value 中。這樣設計的優(yōu)點是,它與現(xiàn)有的索引鍵編碼方式兼容,但同時也帶來了一些挑戰(zhàn),例如在執(zhí)行 DROP PARTITION, TRUNCATE PARTITION 等 DDL 操作時,由于索引項不連續(xù),需要進行額外的處理。

打開網易新聞 查看精彩圖片

影響部分 DDL 性能

當分區(qū)表中存在全局索引時,執(zhí)行諸如 DROP PARTITION(刪除分區(qū))、TRUNCATE PARTITION(清空分區(qū))、REORG PARTITION(重組分區(qū))等部分 DDL 操作時,需要同步更新全局索引的值,這會顯著增加 DDL 操作的執(zhí)行時間。

在 v8.5.0 默認參數下,測試顯示對包含全局索引的 sysbench 表執(zhí)行 DROP PARTITION 或 TRUNCATE PARTITION 操作時,oltp_read_write負載的性能會下降 15% 至 20%。

聚簇索引 (Clustered Index)

聚簇索引不能成為全局索引,是因為如果聚簇索引是全局索引,則表將不再分區(qū)。這是因為聚簇索引的鍵是分區(qū)級別的行數據的鍵,但全局索引是表級別的,這就造成了沖突。如果需要將主鍵設置為全局索引,則需要顯式設置該主鍵為非聚簇索引,如PRIMARY KEY(col1, col2) NONCLUSTERED GLOBAL。

打開網易新聞 查看精彩圖片

  • select_random_pointsin sysbench

示例表結構

CREATETABLE`sbtest`(
`id`int(11)NOTNULL,
`k`int(11)NOTNULLDEFAULT'0',
`c`char(120)NOTNULLDEFAULT'',
`pad`char(60)NOTNULLDEFAULT'',
PRIMARYKEY(`id`)/*T![clustered_index]CLUSTERED*/,
KEY`k_1`(`k`)
/*Key`k_1`(`k`,`c`)GLOBAL*/
)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_bin
/*Partitionbyhash(`id`)partitions100*/
/*Partitionbyrange(`id`)xxxx*/

負載 SQL

SELECTid,k,c,pad
FROMsbtest1
WHEREkIN(xx,xx,xx)

打開網易新聞 查看精彩圖片

  • 通過上述測試可以看出,在高并發(fā)環(huán)境下,全局索引能夠顯著提升分區(qū)表查詢性能,提升幅度可達 50 倍。同時,全局索引還能夠顯著降低資源(RU)消耗。隨著分區(qū)數量的增加,這種性能提升的效果將愈加明顯。

打開網易新聞 查看精彩圖片

全局索引和本地索引

全局索引適用場景:

  • 數據歸檔不頻繁:例如,醫(yī)療行業(yè)的部分業(yè)務數據需要保存 30 年,通常按月分區(qū),然后一次性創(chuàng)建 360 個分區(qū),且很少進行DROP或TRUNCATE操作。在這種情況下,使用全局索引更為合適,因為它能提供跨分區(qū)的一致性和查詢性能。
  • 查詢需要跨分區(qū)的數據:當查詢需要訪問多個分區(qū)的數據時,全局索引可以避免跨分區(qū)掃描,提高查詢效率。

本地索引適用場景:

  • 數據歸檔需求:如果數據歸檔操作很頻繁,且主要查詢集中在單個分區(qū)內,本地索引可以提供更好的性能。
  • 需要使用分區(qū)交換功能:在銀行等行業(yè),可能會將處理后的數據先寫入普通表,確認無誤后再交換到分區(qū)表,以減少對分區(qū)表性能的影響。此時,本地索引更為適用,因為在使用了全局索引之后,分區(qū)表將不再支持分區(qū)交換功能。

全局索引和聚簇索引

由于聚簇索引和全局索引的原理限制,一個索引不能同時作為聚簇索引和全局索引。然而,這兩種索引在不同查詢場景中能提供不同的性能優(yōu)化。在遇到需要同時兼顧兩者的需求時,我們可以將分區(qū)列添加到聚簇索引中,同時創(chuàng)建一個不包含分區(qū)列的全局索引。

假設我們有如下表結構:

CREATETABLE`t`(
`id`intDEFAULTNULL,
`ts`timestampNULLDEFAULTNULL,
`data`varchar(100)DEFAULTNULL
)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_bin
PARTITIONBYRANGE(UNIX_TIMESTAMP(`ts`))
(PARTITION`p0`VALUESLESSTHAN(1735660800)
PARTITION`p1`VALUESLESSTHAN(1738339200)
...)

在上面的t表中,id列的值是唯一的。為了優(yōu)化點查和范圍查詢的性能,我們可以選擇在建表語句中定義一個聚簇索引PRIMARY KEY(id, ts)和一個不包含分區(qū)列的全局索引UNIQUE KEY id(id)。這樣在進行基于id的點查詢時,會走全局索引id,選擇PointGet的執(zhí)行計劃;而在進行范圍查詢時,聚簇索引則會被選中,因為聚簇索引相比全局索引少了一次回表操作,從而提升查詢效率。

修改后的表結構如下所示:

CREATETABLE`t`(
`id`intNOTNULL,
`ts`timestampNOTNULL,
`data`varchar(100)DEFAULTNULL,
PRIMARYKEY(`id`,`ts`)/*T![clustered_index]CLUSTERED*/,
UNIQUEKEY`id`(`id`)/*T![global_index]GLOBAL*/
)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_bin
PARTITIONBYRANGE(UNIX_TIMESTAMP(`ts`))
(PARTITION`p0`VALUESLESSTHAN(1735660800),
PARTITION`p1`VALUESLESSTHAN(1738339200)...)

通過這種方式,我們既能優(yōu)化基于id的點查詢,又能提升范圍查詢的性能,同時確保表的分區(qū)列在基于時間戳的查詢中能得到有效的利用。

打開網易新聞 查看精彩圖片

TiDB 全局索引是 TiDB 在分區(qū)表索引方面的重要特性,它通過允許索引分區(qū)與表分區(qū)之間提供一對多的映射關系,提供了更靈活的索引設計和更高效的查詢性能。全局索引的引入,不僅提升了 TiDB 分區(qū)表在處理復雜查詢和大數據量場景下的能力,還為用戶在數據庫設計和優(yōu)化方面提供了更多的選擇。

然而,全局索引也帶來了一些挑戰(zhàn),如維護成本的增加。在使用全局索引時,需要根據具體的業(yè)務需求和數據特點,合理設計索引,權衡查詢性能和數據修改性能,以達到最佳的數據庫性能。

總之,TiDB 全局索引是一個強大且靈活的特性,能夠幫助用戶更好地優(yōu)化數據庫性能,滿足多樣化的業(yè)務需求。在實際應用中,合理使用全局索引,可以顯著提升查詢性能,提高數據庫的整體效率。