1. 程式人生 > >TiDB 原始碼閱讀系列文章(二十)Table Partition

TiDB 原始碼閱讀系列文章(二十)Table Partition

作者:肖亮亮

Table Partition

什麼是 Table Partition

Table Partition 是指根據一定規則,將資料庫中的一張表分解成多個更小的容易管理的部分。從邏輯上看只有一張表,但是底層卻是由多個物理分割槽組成。相信對有關係型資料庫使用背景的使用者來說可能並不陌生。

TiDB 正在支援分割槽表這一特性。在 TiDB 中分割槽表是一個獨立的邏輯表,但是底層由多個物理子表組成。物理子表其實就是普通的表,資料按照一定的規則劃分到不同的物理子表類內。程式讀寫的時候操作的還是邏輯表名字,TiDB 伺服器自動去操作分割槽的資料。

分割槽表有什麼好處?

  1. 優化器可以使用分割槽資訊做分割槽裁剪。在語句中包含分割槽條件時,可以只掃描一個或多個分割槽表來提高查詢效率。

  2. 方便地進行資料生命週期管理。通過建立、刪除分割槽、將過期的資料進行 高效的歸檔,比使用 Delete 語句刪除資料更加優雅,打散寫入熱點,將一個表的寫入分散到多個物理表,使得負載分散開,對於存在 Sequence 型別資料的表來說(比如 Auto Increament ID 或者是 create time 這類的索引)可以顯著地提升寫入吞吐。

分割槽表的限制

  1. TiDB 預設一個表最多隻能有 1024 個分割槽 ,預設是不區分表名大小寫的。

  2. Range, List, Hash 分割槽要求分割槽鍵必須是 INT 型別,或者通過表示式返回 INT 型別。但 Key 分割槽的時候,可以使用其他型別的列(BLOB,TEXT 型別除外)作為分割槽鍵。

  3. 如果分割槽欄位中有主鍵或者唯一索引的列,那麼有主鍵列和唯一索引的列都必須包含進來。即:分割槽欄位要麼不包含主鍵或者索引列,要麼包含全部主鍵和索引列。

  4. TiDB 的分割槽適用於一個表的所有資料和索引。不能只對表資料分割槽而不對索引分割槽,也不能只對索引分割槽而不對錶資料分割槽,也不能只對表的一部分資料分割槽。

常見分割槽表的型別

  • Range 分割槽:按照分割槽表示式的範圍來劃分分割槽。通常用於對分割槽鍵需要按照範圍的查詢,分割槽表示式可以為列名或者表示式 ,下面的 employees 表當中 p0, p1, p2, p3 表示 Range 的訪問分別是  (min, 1991), [1991, 1996), [1996, 2001), [2001, max) 這樣一個範圍。

    CREATE  TABLE employees (
    id INT  NOT  NULL,
    fname VARCHAR(30),
    separated DATE  NOT  NULL
    )
    
    PARTITION BY RANGE ( YEAR(separated) ) (
    PARTITION p0 VALUES LESS THAN (1991),
    PARTITION p1 VALUES LESS THAN (1996),
    PARTITION p2 VALUES LESS THAN (2001),
    PARTITION p3 VALUES LESS THAN MAXVALUE
    );
    
    
  • List 分割槽:按照 List 中的值分割槽,主要用於列舉型別,與 Range 分割槽的區別在於 Range 分割槽的區間範圍值是連續的。

  • Hash 分割槽:Hash 分割槽需要指定分割槽鍵和分割槽個數。通過 Hash 的分割槽表示式計算得到一個 INT 型別的結果,這個結果再跟分割槽個數取模得到具體這行資料屬於那個分割槽。通常用於給定分割槽鍵的點查詢,Hash 分割槽主要用來分散熱點讀,確保資料在預先確定個數的分割槽中儘可能平均分佈。

  • Key 分割槽:類似 Hash 分割槽,Hash 分割槽允許使用使用者自定義的表示式,但 Key 分割槽不允許使用使用者自定義的表示式。Hash 僅支援整數分割槽,而 Key 分割槽支援除了 Blob 和 Text 的其他型別的列作為分割槽鍵。

TiDB Table Partition 的實現

本文接下來按照 TiDB 原始碼的 release-2.1 分支講解,部分講解會在 source-code 分支程式碼,目前只支援 Range 分割槽所以這裡只介紹 Range 型別分割槽 Table Partition 的原始碼實現,包括 create table、select 、add partition、insert 、drop partition 這五種語句。

create table

create table 會重點講構建 Partition 的這部分,更詳細的可以看 TiDB 原始碼閱讀系列文章(十七)DDL 原始碼解析,當用戶執行建立分割槽表的SQL語句,語法解析(Parser)階段會把 SQL 語句中 Partition 相關資訊轉換成 ast.PartitionOptions,下文會介紹。接下來會做一系列 Check,分割槽名在當前的分割槽表中是否唯一、是否分割槽 Range 的值保持遞增、如果分割槽鍵構成為表示式檢查表示式裡面是否是允許的函式、檢查分割槽鍵必須是 INT 型別,或者通過表示式返回 INT 型別、檢查分割槽鍵是否符合一些約束。

解釋下分割槽鍵,在分割槽表中用於計算這一行資料屬於哪一個分割槽的列的集合叫做分割槽鍵。分割槽鍵構成可能是一個欄位或多個欄位也可以是表示式。

// PartitionOptions specifies the partition options.
type PartitionOptions struct {
Tp          model.PartitionType
Expr        ExprNode
ColumnNames []*ColumnName
Definitions []*PartitionDefinition
}// PartitionDefinition defines a single partition.
type PartitionDefinition struct {
Name     model.CIStr
LessThan []ExprNode
MaxValue bool
Comment  string
}
	

PartitionOptions 結構中 Tp 欄位表示分割槽型別,Expr 欄位表示分割槽鍵,ColumnNames 欄位表示 Columns 分割槽,這種型別分割槽有分為 Range columns 分割槽和 List columns 分割槽,這種分割槽目前先不展開介紹。PartitionDefinition 其中 Name 欄位表示分割槽名,LessThan 表示分割槽 Range 值,MaxValue 欄位表示 Range 值是否為最大值,Comment 欄位表示分割槽的描述。

CreateTable Partition 部分主要流程如下:

  1. 把上文提到語法解析階段會把 SQL語句中 Partition 相關資訊轉換成 ast.PartitionOptions , 然後 buildTablePartitionInfo 負責把 PartitionOptions 結構轉換 PartitionInfo,  即 Partition 的元資訊。

  2. checkPartitionNameUnique 檢查分割槽名是否重複,分表名是不區大小寫的。

  3. 對於每一分割槽 Range 值進行 Check,checkAddPartitionValue 就是檢查新增的 Partition 的 Range 需要比之前所有 Partition 的 Range 都更大。

  4. TiDB 單表最多隻能有 1024 個分割槽 ,超過最大分割槽的限制不會建立成功。

  5. 如果分割槽鍵構成是一個包含函式的表示式需要檢查表示式裡面是否是允許的函式 checkPartitionFuncValid

  6. 檢查分割槽鍵必須是 INT 型別,或者通過表示式返回 INT 型別,同時檢查分割槽鍵中的欄位在表中是否存在 checkPartitionFuncType

  7. 如果分割槽欄位中有主鍵或者唯一索引的列,那麼多有主鍵列和唯一索引列都必須包含進來。即:分割槽欄位要麼不包含主鍵或者索引列,要麼包含全部主鍵和索引列 checkRangePartitioningKeysConstraints

  8. 通過以上對 PartitionInfo 的一系列 check 主要流程就講完了,需要注意的是我們沒有對 PartitionInfo 的元資料持久化單獨儲存而是附加在 TableInfo Partition 中。

add partition

add partition 首先需要從 SQL 中解析出來 Partition 的元資訊,然後對當前新增的分割槽會有一些 Check 和限制,主要檢查是否是分割槽表、分割槽名是已存在、最大分割槽數限制、是否 Range 值保持遞增,最後把 Partition 的元資訊 PartitionInfo 追加到 Table 的元資訊 TableInfo中,具體如下:

  1. 檢查是否是分割槽表,若不是分割槽表則報錯提示。

  2. 使用者的 SQL 語句被解析成將 ast.PartitionDefinition 然後 buildPartitionInfo 做的事就是儲存表原來已存在的分割槽資訊例如分割槽型別,分割槽鍵,分割槽具體資訊,每個新分割槽分配一個獨立的 PartitionID。

  3. TiDB 預設一個表最多隻能有 1024 個分割槽,超過最大分割槽的限制會報錯。

  4. 對於每新增一個分割槽需要檢查 Range 值進行 Check,checkAddPartitionValue 簡單說就是檢查新增的 Partition 的 Range 需要比之前所有 Partition 的 Rrange 都更大。

  5. checkPartitionNameUnique 檢查分割槽名是否重複,分表名是不區大小寫的。

  6. 最後把 Partition 的元資訊 PartitionInfo 追加到 Table 的元資訊 TableInfo.Partition 中,具體實現在這裡 updatePartitionInfo

drop partition

drop partition 和 drop table 類似,只不過需要先找到對應的 Partition ID,然後刪除對應的資料,以及修改對應 Table 的 Partition 元資訊,兩者區別是如果是 drop table 則刪除整個表資料和表的 TableInfo 元資訊,如果是 drop partition 則需刪除對應分割槽資料和 TableInfo 中的 Partition 元資訊,刪除分割槽之前會有一些 Check 具體如下:

  1. 只能對分割槽表做 drop partition 操作,若不是分割槽表則報錯提示。

  2. checkDropTablePartition 檢查刪除的分割槽是否存在,TiDB 預設是不能刪除所有分割槽,如果想刪除最後一個分割槽,要用 drop table 代替。

  3. removePartitionInfo 會把要刪除的分割槽從 Partition 元資訊刪除掉,刪除前會做checkDropTablePartition 的檢查。

  4. 對分割槽表資料則需要拿到 PartitionID 根據插入資料時候的編碼規則構造出 StartKey 和 EndKey 便能包含對應分割槽 Range 內所有的資料,然後把這個範圍內的資料刪除,具體程式碼實現在這裡

  5. 編碼規則:

    Key: tablePrefix_rowPrefix_partitionID_rowID

    startKey: tablePrefix_rowPrefix_partitionID

    endKey: tablePrefix_rowPrefix_partitionID + 1

  6. 刪除了分割槽,同時也將刪除該分割槽中的所有資料。如果刪除了分割槽導致分割槽不能覆蓋所有值,那麼插入資料的時候會報錯。

Select 語句

Select 語句重點講 Select Partition 如何查詢的和分割槽裁剪(Partition Pruning),更詳細的可以看 TiDB 原始碼閱讀系列文章(六)Select 語句概覽

一條 SQL 語句的處理流程,從 Client 接收資料,MySQL 協議解析和轉換,SQL 語法解析,邏輯查詢計劃和物理查詢計劃執行,到最後返回結果。那麼對於分割槽表是如何查詢的表裡的資料的,其實最主要的修改是 邏輯查詢計劃 階段,舉個例子:如果用上文中 employees 表作查詢, 在 SQL 語句的處理流程前幾個階段沒什麼不同,但是在邏輯查詢計劃階段,rewriteDataSource 將 DataSource 重寫了變成 Union All 。每個 Partition id 對應一個 Table Reader。

select * from employees

等價於:

select * from (union all
select * from p0 where id < 1991
select * from p1 where id < 1996
select * from p2 where id < 2001
select * from p3 where id < MAXVALUE)

通過觀察 EXPLAIN 的結果可以證實上面的例子,如圖 1,最終物理執行計劃中有四個 Table Reader 因為 employees 表中有四個分割槽,Table Reader 表示在 TiDB 端從 TiKV 端讀取,cop task 是指被下推到 TiKV 端分散式執行的計算任務。

EXPLAN 輸出.png

圖 1:EXPLAIN 輸出

使用者在使用分割槽表時,往往只需要訪問其中部分的分割槽, 就像程式區域性性原理一樣,優化器分析 FROMWHERE 子句來消除不必要的分割槽,具體還要優化器根據實際的 SQL 語句中所帶的條件,避免訪問無關分割槽的優化過程我們稱之為分割槽裁剪(Partition Pruning),具體實現在 這裡,分割槽裁剪是分割槽表提供的重要優化手段,通過分割槽的裁剪,避免訪問無關資料,可以加速查詢速度。當然使用者可以刻意利用分割槽裁剪的特性在 SQL 加入定位分割槽的條件,優化查詢效能。

Insert 語句

Insert 語句 是怎麼樣寫入 Table Partition ?

其實解釋這些問題就可以了:

  1. 普通表和分割槽表怎麼區分?

  2. 插入資料應該插入哪個 Partition?

  3. 每個 Partition 的 RowKey 怎麼編碼的和普通表的區別是什麼?

  4. 怎麼將資料插入到相應的 Partition 裡面?

普通 Table 和 Table Partition 也是實現了 Table 的介面,load schema 在初始化 Table 資料結構的時候,如果發現 tableInfo 裡面沒有 Partition 資訊,則生成一個普通的 tables.Table,普通的 Table 跟以前處理邏輯保持不變,如果 tableInfo 裡面有 Partition 資訊,則會生成一個 tables.PartitionedTable,它們的區別是 RowKey 的編碼方式:

  • 每個分割槽有一個獨立的 Partition ID,Partition ID 和 Table ID 地位平等,每個 Partition 的 Row 和 index 在編碼的時候都使用這個 Partition 的 ID。

  • 下面是 PartitionRecordKey 和普通表 RecordKey 區別。

    • 分割槽表按照規則編碼成 Key-Value pair:

      Key: tablePrefix_rowPrefix_partitionID_rowID

      Value: [col1, col2, col3, col4]

    • 普通表按照規則編碼成 Key-Value pair:

      Key: tablePrefix_rowPrefix_tableID_rowID

      Value: [col1, col2, col3, col4]

  • 通過 locatePartition 操作查詢到應該插入哪個 Partition,目前支援 RANGE 分割槽插入到那個分割槽主要是通過範圍來判斷,例如在 employees 表中插入下面的 sql,通過計算範圍該條記錄會插入到 p3 分割槽中,接著呼叫對應 Partition 上面的 AddRecord 方法,將資料插入到相應的 Partition 裡面。

    INSERT INTO employees VALUES (1, 'PingCAP TiDB', '2003-10-15'),

  • 插入資料時,如果某行資料不屬於任何 Partition,則該事務失敗,所有操作回滾。如果 Partition 的 Key 算出來是一個 NULL,對於不同的 Partition 型別有不同的處理方式:

    • 對於 Range Partition:該行資料被插入到最小的那個 Partition

    • 對於 List partition:如果某個 Partition 的 Value List 中有 NULL,該行資料被插入那個 Partition,否則插入失敗

    • 對於 Hash 和 Key Partition:NULL 值視為 0,計算 Partition ID 將資料插入到對應的 Partition

  • 在 TiDB 分割槽表中分割槽欄位插入的值不能大於表中 Range 值最大的上界,否則會報錯

End

TiDB 目前支援 Range 分割槽型別,具體以及更細節的可以看 這裡。剩餘其它型別的分割槽型別正在開發中,後面陸續會和大家見面,敬請期待。它們的原始碼實現讀者屆時可以自行閱讀,流程和文中上述描述類似。