做者:肖亮亮git
Table Partition 是指根據必定規則,將數據庫中的一張表分解成多個更小的容易管理的部分。從邏輯上看只有一張表,可是底層倒是由多個物理分區組成。相信對有關係型數據庫使用背景的用戶來講可能並不陌生。github
TiDB 正在支持分區表這一特性。在 TiDB 中分區表是一個獨立的邏輯表,可是底層由多個物理子表組成。物理子表其實就是普通的表,數據按照必定的規則劃分到不一樣的物理子表類內。程序讀寫的時候操做的仍是邏輯表名字,TiDB 服務器自動去操做分區的數據。sql
優化器可使用分區信息作分區裁剪。在語句中包含分區條件時,能夠只掃描一個或多個分區表來提升查詢效率。數據庫
方便地進行數據生命週期管理。經過建立、刪除分區、將過時的數據進行 高效的歸檔,比使用 Delete 語句刪除數據更加優雅,打散寫入熱點,將一個表的寫入分散到多個物理表,使得負載分散開,對於存在 Sequence 類型數據的表來講(好比 Auto Increament ID 或者是 create time 這類的索引)能夠顯著地提高寫入吞吐。api
TiDB 默認一個表最多隻能有 1024 個分區 ,默認是不區分表名大小寫的。服務器
Range, List, Hash 分區要求分區鍵必須是 INT 類型,或者經過表達式返回 INT 類型。但 Key 分區的時候,可使用其餘類型的列(BLOB,TEXT 類型除外)做爲分區鍵。數據結構
若是分區字段中有主鍵或者惟一索引的列,那麼有主鍵列和惟一索引的列都必須包含進來。即:分區字段要麼不包含主鍵或者索引列,要麼包含所有主鍵和索引列。分佈式
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 源碼的 release-2.1 分支講解,部分講解會在 source-code 分支代碼,目前只支持 Range 分區因此這裏只介紹 Range 類型分區 Table Partition 的源碼實現,包括 create table、select 、add partition、insert 、drop partition 這五種語句。
create table 會重點講構建 Partition 的這部分,更詳細的能夠看 TiDB 源碼閱讀系列文章(十七)DDL 源碼解析,當用戶執行建立分區表的SQL語句,語法解析(Parser)階段會把 SQL 語句中 Partition 相關信息轉換成 ast.PartitionOptions,下文會介紹。接下來會作一系列 Check,分區名在當前的分區表中是否惟1、是否分區 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 部分主要流程以下:
把上文提到語法解析階段會把 SQL語句中 Partition 相關信息轉換成 ast.PartitionOptions
, 而後 buildTablePartitionInfo 負責把 PartitionOptions
結構轉換 PartitionInfo
, 即 Partition 的元信息。
checkPartitionNameUnique 檢查分區名是否重複,分表名是不區大小寫的。
對於每一分區 Range 值進行 Check,checkAddPartitionValue 就是檢查新增的 Partition 的 Range 須要比以前全部 Partition 的 Range 都更大。
TiDB 單表最多隻能有 1024 個分區 ,超過最大分區的限制不會建立成功。
若是分區鍵構成是一個包含函數的表達式須要檢查表達式裏面是不是容許的函數 checkPartitionFuncValid。
檢查分區鍵必須是 INT 類型,或者經過表達式返回 INT 類型,同時檢查分區鍵中的字段在表中是否存在 checkPartitionFuncType。
若是分區字段中有主鍵或者惟一索引的列,那麼多有主鍵列和惟一索引列都必須包含進來。即:分區字段要麼不包含主鍵或者索引列,要麼包含所有主鍵和索引列 checkRangePartitioningKeysConstraints。
經過以上對 PartitionInfo
的一系列 check 主要流程就講完了,須要注意的是咱們沒有對 PartitionInfo
的元數據持久化單獨存儲而是附加在 TableInfo Partition 中。
add partition 首先須要從 SQL 中解析出來 Partition 的元信息,而後對當前添加的分區會有一些 Check 和限制,主要檢查是不是分區表、分區名是已存在、最大分區數限制、是否 Range 值保持遞增,最後把 Partition 的元信息 PartitionInfo 追加到 Table 的元信息 TableInfo中,具體以下:
檢查是不是分區表,若不是分區表則報錯提示。
用戶的 SQL 語句被解析成將 ast.PartitionDefinition 而後 buildPartitionInfo 作的事就是保存表原來已存在的分區信息例如分區類型,分區鍵,分區具體信息,每一個新分區分配一個獨立的 PartitionID。
TiDB 默認一個表最多隻能有 1024 個分區,超過最大分區的限制會報錯。
對於每新增一個分區須要檢查 Range 值進行 Check,checkAddPartitionValue 簡單說就是檢查新增的 Partition 的 Range 須要比以前全部 Partition 的 Rrange 都更大。
checkPartitionNameUnique 檢查分區名是否重複,分表名是不區大小寫的。
最後把 Partition 的元信息 PartitionInfo 追加到 Table 的元信息 TableInfo.Partition 中,具體實如今這裏 updatePartitionInfo。
drop partition 和 drop table 相似,只不過須要先找到對應的 Partition ID,而後刪除對應的數據,以及修改對應 Table 的 Partition 元信息,二者區別是若是是 drop table 則刪除整個表數據和表的 TableInfo 元信息,若是是 drop partition 則需刪除對應分區數據和 TableInfo 中的 Partition 元信息,刪除分區以前會有一些 Check 具體以下:
只能對分區表作 drop partition 操做,若不是分區表則報錯提示。
checkDropTablePartition 檢查刪除的分區是否存在,TiDB 默認是不能刪除全部分區,若是想刪除最後一個分區,要用 drop table 代替。
removePartitionInfo 會把要刪除的分區從 Partition 元信息刪除掉,刪除前會作checkDropTablePartition 的檢查。
對分區表數據則須要拿到 PartitionID 根據插入數據時候的編碼規則構造出 StartKey 和 EndKey 便能包含對應分區 Range 內全部的數據,而後把這個範圍內的數據刪除,具體代碼實如今這裏。
編碼規則:
Key: tablePrefix_rowPrefix_partitionID_rowID
startKey: tablePrefix_rowPrefix_partitionID
endKey: tablePrefix_rowPrefix_partitionID + 1
刪除了分區,同時也將刪除該分區中的全部數據。若是刪除了分區致使分區不能覆蓋全部值,那麼插入數據的時候會報錯。
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 端分佈式執行的計算任務。
<center>圖 1:EXPLAIN 輸出</center>
用戶在使用分區表時,每每只須要訪問其中部分的分區, 就像程序局部性原理同樣,優化器分析 FROM
和 WHERE
子句來消除沒必要要的分區,具體還要優化器根據實際的 SQL 語句中所帶的條件,避免訪問無關分區的優化過程咱們稱之爲分區裁剪(Partition Pruning),具體實如今 這裏,分區裁剪是分區表提供的重要優化手段,經過分區的裁剪,避免訪問無關數據,能夠加速查詢速度。固然用戶能夠刻意利用分區裁剪的特性在 SQL 加入定位分區的條件,優化查詢性能。
Insert 語句 是怎麼樣寫入 Table Partition ?
其實解釋這些問題就能夠了:
普通表和分區表怎麼區分?
插入數據應該插入哪一個 Partition?
每一個 Partition 的 RowKey 怎麼編碼的和普通表的區別是什麼?
怎麼將數據插入到相應的 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 值最大的上界,不然會報錯
TiDB 目前支持 Range 分區類型,具體以及更細節的能夠看 這裏。剩餘其它類型的分區類型正在開發中,後面陸續會和你們見面,敬請期待。它們的源碼實現讀者屆時能夠自行閱讀,流程和文中上述描述相似。