MySQL之分區

分合思想

技術不要拘泥於一點,重要的是編程思想,即思想是最重要的。當數據量大的時候,需要具有分的思想去細化粒度。當數據量太碎片的時候,需要具有合的思想來粗化粒度。

表分區介紹

InnoDB存儲結構

首先要先介紹一下InnoDB邏輯存儲結構和區的概念,它的所有數據都被邏輯地存放在表空間,表空間又由段,區,頁組成。
在這裏插入圖片描述

段就是上圖的segment區域,常見的段有數據段、索引段、回滾段等,在InnoDB存儲引擎中,對段的管理都是由引擎自身所完成的。

區就是上圖的extent區域,區是由連續的頁組成的空間,無論頁的大小怎麼變,區的大小默認總是爲1MB。爲了保證區中的頁的連續性,InnoDB存儲引擎一次從磁盤申請4-5個區,InnoDB頁的大小默認爲16kb,即一個區一共有64(1MB/16kb=16)個連續的頁。每個段開始,先用32頁(page)大小的碎片頁來存放數據,在使用完這些頁之後纔是64個連續頁的申請。這樣做的目的是,對於一些小表或者是undo類的段,可以開始申請較小的空間,節約磁盤開銷。

頁就是上圖的page區域,也可以叫塊。頁是InnoDB磁盤管理的最小單位。默認大小爲16KB,可以通過參數innodb_page_size來設置。常見的頁類型有:數據頁,undo頁,系統頁,事務數據頁,插入緩衝位圖頁,插入緩衝空閒列表頁,未壓縮的二進制大對象頁,壓縮的二進制大對象頁等。

表分區

分區是根據一定的規則,數據庫把一個表分解成多個更小的、更容易管理的部分。就訪問數據庫應用而言,邏輯上就只有一個表或者一個索引,但實際上這個表可能有N個物理分區對象組成,每個分區都是一個獨立的對象,可以獨立處理,可以作爲表的一部分進行處理。分區對應用來說是完全透明的,不影響應用的業務邏輯。幾個分區就有幾個.idb文件,不是我們上述extent區域。分區是將一個表或索引分解成多個更小,更可管理的部分。每個區都是獨立的,可以獨立處理,也可以作爲一個更大對象的一部分進行處理。這個是MySQL支持的功能,業務代碼無需改動。要知道MySQL是面向OLTP的數據,它不像TIDB等其他DB。
有時候可能會有這麼一種誤區,只要啓用了分區,數據庫就會運行的更快。這個結論結論是存在很多問題的,就經驗來看,分區可能會給某些SQL語句性能帶來提高,但是分區主要用於數據庫高可用性的管理。在OLTP應用中,對於分區的使用應該非常小心,總之,如果只是一味地使用分區,而不理解分區是如何工作的,也不清楚你的應用如何使用分區,那麼分區極有可能會對性能產生負面的影響。
MySQL數據庫支持的分區類型爲水平分區(指將同一個表中不同行的記錄分配到不同的物理文件中),並不支持垂直分區(指將同一表中不同列的記錄分配到不同的物理文件中)。此外,MySQL數據庫的分區是局部分區索引,一個分區中既存放了數據又存放了索引。而全局分區是指,數據存放在各個分區中,但是所有數據的索引放在一個對象中。目前,MySQL數據庫還不支持全局分區。
 無論哪種類型的分區,如果表中存在主鍵或唯一索引時,分區列必須是唯一索引的一個組成部分。

show global variable like '%partition%';
show plugins\G

表分區優點

  • 和單個磁盤或者文件系統分區相比,可以存儲更多數據
  • 優化查詢。在where子句中包含分區條件時,可以只掃描必要的一個或者多個分區來提高查詢效率;同時在涉及sum()和count()這類聚合函數的查詢時,可以容易的在每個分區上並行處理,最終只需要彙總所有分區得到的結果
  • 對於已經過期或者不需要保存的數據,可以通過刪除與這些數據有關的分區來快速刪除數據
  • 跨多個磁盤來分散數據查詢,以獲得更大的查詢吞吐量

啓動分區後形成的文件

  • .frm文件:表結構文件
  • .ibd文件:InnoDB中,索引和數據都在同個文件.ibdata(你的執行結果可能是.MYD數據文件和.MYI索引文件,沒關係,這是MyIsAm存儲引擎,對應着InnoDB的.ibd文件
  • .par文件:你執行的結果可能有.par文件也可能沒有。注意:從MySql 5.7.6開始,不再創建.par分區定義文件。分區定義存儲在內部數據字典中。

數據處理

分區表後,提高了MySql性能。如果一張表的話,那就只有一個.ibd文件,一顆大的B+樹。如果分表後,將按分區規則,分成不同的區,也就是一個大的B+樹,分成多個小的樹。
讀的效率肯定提升了,如果走分區鍵索引的話,先走對應分區的輔助索引B+樹,再走對應分區的聚集索引B+樹。如果沒有走分區鍵,將會在所有分區都會執行一次。會造成多次邏輯IO!平時開發如果想查看sql語句的分區查詢可以使用explain partitons select xxxxx語句。可以看到一句select語句走了幾個分區。

表分區類型

目前MySQL支持以下幾種類型的分區,RANGE分區,LIST分區,HASH分區,KEY分區。如果表存在主鍵或者唯一索引時,分區列必須是唯一索引的一個組成部分。實戰十有八九都是用RANGE分區。

RANGE分區

RANGE分區是實戰最常用的一種分區類型,行數據基於屬於一個給定的連續區間的列值被放入分區。但是記住,當插入的數據不在一個分區中定義的值的時候,會拋異常。RANGE分區主要用於日期列的分區,比如交易表啊,銷售表啊等。可以根據年月來存放數據。如果你分區走的唯一索引中date類型的數據,那麼注意了,優化器只能對YEAR(),TO_DAYS(),TO_SECONDS(),UNIX_TIMESTAMP()這類函數進行優化選擇。實戰中可以用int類型,那麼只用存yyyyMM就好了。也不用關心函數了。

CREATE TABLE `m_test_db`.`Order` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `partition_key` INT NOT NULL,
  `amt` DECIMAL(5) NULL,
  PRIMARY KEY (`id`, `partition_key`)) 
PARTITION BY RANGE(partition_key) PARTITIONS 5( 
	PARTITION part0 VALUES LESS THAN (201901),  
	PARTITION part1 VALUES LESS THAN (201902),  
	PARTITION part2 VALUES LESS THAN (201903),  
	PARTITION part3 VALUES LESS THAN (201904),  
	PARTITION part4 VALUES LESS THAN (201905)
) ;

插入如下數據:
INSERT INTO `m_test_db`.`Order` (`id`, `partition_key`, `amt`) VALUES ('1', '201901', '1000');
INSERT INTO `m_test_db`.`Order` (`id`, `partition_key`, `amt`) VALUES ('2', '201902', '800');
INSERT INTO `m_test_db`.`Order` (`id`, `partition_key`, `amt`) VALUES ('3', '201903', '1200');

現在我們查詢一下,通過EXPLAIN PARTITION命令發現SQL優化器只需搜對應的區,不會搜索所有分區
在這裏插入圖片描述
如果sql語句有問題,那麼會走所有區。會很危險。所以分區表後,select語句必須走分區鍵。
在這裏插入圖片描述

LIST分區

LIST分區和RANGE分區很相似,只是分區列的值是離散的,不是連續的。LIST分區使用VALUES IN,因爲每個分區的值是離散的,因此只能定義值。

CREATE TABLE employees (
  id INT NOT NULL,
  name VARCHAR(30),
  hired DATE NOT NULL DEFAULT '1970-01-01',
  separated DATE NOT NULL DEFAULT '9999-12-31',
  store_id INT
)
PARTITION BY LIST(store_id)
  PARTITION pNorth VALUES IN (3,5,6,9,17),
  PARTITION pEast VALUES IN (1,2,10,11,19,20),
  PARTITION pWest VALUES IN (4,12,13,14,18),
  PARTITION pCentral VALUES IN (7,8,15,16)
);

HASH分區

說到哈希,那麼目的很明顯了,將數據均勻的分佈到預先定義的各個分區中,保證每個分區的數量大致相同。
要使用HASH分區來分割一個表,要在CREATE TABLE 語句上添加一個“PARTITION BY HASH (expr)”子句,其中“expr”是一個返回一個整數的表達式。它可以僅僅是字段類型爲MySQL 整型的一列的名字。此外,你很可能需要在後面再添加一個“PARTITIONS num”子句,其中num是一個非負的整數,它表示表將要被分割成分區的數量,如果沒有包括一個PARTITIONS子句,那麼分區的數量將默認爲1。

CREATE TABLE employees_h (
  id INT NOT NULL,
  lname VARCHAR(30),
  store_id INT
)
  PARTITION BY HASH(store_id)
  PARTITIONS 4;

MySQL還支持線性哈希功能,它與常規哈希的區別在於,線性哈希功能使用的一個線性的2的冪(powers-of-two)運算法則,而常規哈希使用的是求哈希函數值的模數。
線性哈希分區和常規哈希分區在語法上的唯一區別在於,在“PARTITION BY” 子句中添加“LINEAR”關鍵字。

CREATE TABLE employees_lh (
  id INT NOT NULL,
  name VARCHAR(30),
  hired DATE NOT NULL DEFAULT '1970-01-01',
  separated DATE NOT NULL DEFAULT '9999-12-31',
  store_id INT
)
  PARTITION BY LINEAR HASH(YEAR(hired))
  PARTITIONS 4;

按照線性哈希分區的優點在於增加、刪除、合併和拆分分區將變得更加快捷,有利於處理含有極其大量(1000吉)數據的表。它的缺點在於,與使用常規HASH分區得到的數據分佈相比,各個分區間數據的分佈不大可能均衡。

KEY分區

KEY分區和HASH分區相似,不同之處在於HASH分區使用用戶定義的函數進行分區,KEY分區使用數據庫提供的函數進行分區。

COLUMNS分區

在前面說了RANGE、LIST、HASH和KEY這四種分區中,分區的條件是:數據必須爲整形(interger),如果不是整形,那應該需要通過函數將其轉化爲整形,如YEAR(),TO_DAYS(),MONTH()等函數。MySQL5.5版本開始支持COLUMNS分區,可視爲RANGE分區和LIST分區的一種進化。COLUMNS分區可以直接使用非整形的數據進行分區,分區根據類型直接比較而得,不需要轉化爲整形。此外,RANGE COLUMNS分區可以對多個列的值進行分區。
COLUMNS分區支持以下的數據類型:

  1. 所有的整形類型,如INT、SMALLINT、TINYINT和BIGINT。而FLOAT和DECIMAL則不予支持。
  2. 日期類型,如DATE何DATETIME。其餘的日期類型不予支持。
  3. 字符串類型,如CHAR、VARCHAR、BINARY和VARBINARY。而BLOB和TEXT類型不予支持。

對於日期類型的分區,我們不再需要YEAR()和TO_DATS()函數了,而直接可以使用COLUMNS,如:

CREATE TABLE `t_c` (
  `key` varchar(50),
  `value` varchar(50),
  `create_time` datetime
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  PARTITION BY RANGE COLUMNS (create_time) (
  PARTITION p0 VALUES LESS THAN ('2017-01-01 00:00:00'),
  PARTITION p1 VALUES LESS THAN ('2017-03-01 00:00:00')
);
//字符串分區
CREATETABLE`monitor_2`(
  `key`varchar(15),
  `value`varchar(50),
  `create_time`datetime,
  `city`VARCHAR(15)
)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4
PARTITIONBYLISTCOLUMNS(city)(
PARTITIONp0VALUESIN('shanghai','beijing','shenzhen'),
PARTITIONp1VALUESIN('hubei','henan','hunan')
);

對比RANGE分區和LIST分區,Columns分區的亮點除了支持數據類型增加之外,另外一大亮點是Columns分區還支持多列分區。如:

CREATE TABLE `monitor_3` (
  `key` varchar(15),
  `value` varchar(50),
  `create_time` datetime,
  `test` VARCHAR(1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE COLUMNS(create_time,test) (
PARTITION p0 VALUES LESS THAN ('2017-01-01 00:00:00','yes'),
PARTITION p1 VALUES LESS THAN ('2017-03-01 00:00:00','no'),
PARTITION p2 VALUES LESS THAN (MAXVALUE,MAXVALUE)
);

MySQL 5.5開始支持COLUMNS分區,對於之前的RANGE和LIST分區,用戶可以用RANGE COLUMNS和LIST COLUMNS分區進行很好的代替了。

MySQL子分區

子分區(subparttitioning)是在分區的基礎上再進行分區,有時也稱這種分區爲複合分區。MySQL數據庫允許在RANGE和LIST的分區上再進行HASH或KEY的子分區,如:

createtablets(aint,bdate)engine=innodb
  partitionbyrange(year(b))
  subpartitionbyhash(to_days(b))
  subpartitions2(
  partitionp0valueslessthan(1990),
  partitionp1valueslessthan(2000),
  partitionp2valueslessthanmaxvalue
);

// 結果爲
mysql> system ls -ln /data/mysql/3306/data/db
total 592
-rw-r----- 1 27 27    67 Feb 27 12:03 db.opt
-rw-r----- 1 27 27  8578 Feb 27 15:54 ts.frm
-rw-r----- 1 27 27 98304 Feb 27 15:54 ts#P#p0#SP#p0sp0.ibd
-rw-r----- 1 27 27 98304 Feb 27 15:54 ts#P#p0#SP#p0sp1.ibd
-rw-r----- 1 27 27 98304 Feb 27 15:54 ts#P#p1#SP#p1sp0.ibd
-rw-r----- 1 27 27 98304 Feb 27 15:54 ts#P#p1#SP#p1sp1.ibd
-rw-r----- 1 27 27 98304 Feb 27 15:54 ts#P#p2#SP#p2sp0.ibd
-rw-r----- 1 27 27 98304 Feb 27 15:54 ts#P#p2#SP#p2sp1.ibd

表ts先根據b列進行了RANGE分區,然後又進行了一次HASH分區,所以分區的數量應該爲(3×2=)6個,這通過查看物理磁盤上的文件也可以得到證實。我們也可以通過使用subpartition語法來顯示地指出各個子分區的名字。
子分區的建立需要注意以下幾個問題:

  1. 每個子分區的數量必須相同。
  2. 要在一個分區表的任何分區上使用subpartition來明確定義任何子分區,就必須定義所有的子分區。
  3. 每個subpartition子句必須包括子分區的一個名字。
  4. 子分區的名字必須是唯一的。

表分區中注意事項

表分區與水平分表的區別

  1. 水平分表需要用戶預先手動顯式創建出多張分表(如tbl_user0, tbl_user1, tbl_user2),在物理上實實在在的創建多張表,通過客戶端代理(Sharding-JDBC等)或者中間件代理(Mycat等)來實現分表邏輯。
  2. 分區是MySQL的一個插件Plugin功能,將一張大表的數據在數據庫底層分成多個分區文件(如tbl_user#P#p0.ibd, tbl_user#P#p1.ibd, tbl_user#P#p2.ibd),和水平分表不同的是分區不需要顯式的創建“分表”,數據庫會自動創建分區文件的,用戶看到的只是一張普通的表,其實是對應的是多個分區,這個是對用戶是屏蔽的、透明的,在使用上和使用一張表完全一樣,不需要藉助任何功能來實現。分區是一種邏輯上的水平分表,在物理層面還是一張表。

區別於分區的是,分區一般都是放在單機裏的,用的比較多的是時間範圍分區,方便歸檔。只不過分庫分表需要代碼實現,分區則是mysql內部實現。分庫分表和分區並不衝突,可以結合使用。

分區中NULL值

MySQL數據庫允許對NULL值做分區,但是處理的方法與其他數據庫可能完全不同。**MySQL數據庫的分區總是視NULL值小於任何的一個非NULL值,這和MySQL數據庫中處理NULL值的ORDER BY操作是一樣的。**因此對於不同的分區類型,MySQL數據庫對於NULL值的處理也是各不相同。

  1. 對於RANGE分區,如果向分區列插入了NULL值,則MySQL數據庫會將該值放入最左邊的分區。
  2. 對於LIST分區,如果向分區列插入了NULL值,則必須顯示地指出哪個分區放入NULL值,否則會報錯。
  3. 對於HASH和KEY分區,對於NULL值的處理方法和RANGE分區、LIST分區不一樣。任何分區函數都會將含有NULL值的記錄返回爲0。

分區和性能

分區真的會加快數據庫的查詢嗎?實際上可能根本感覺不到查詢速度的提升,甚至會發現查詢速度急劇下降,因此在合理使用分區之前,必須瞭解分區的使用環境。
數據庫的應用分爲兩類:一類是OLTP(在線事務處理),如Blog、電子商務、網絡遊戲等;另一類是OLAP(在線分析處理),如數據倉庫、數據集市。**對於OLAP的應用,分區的確是可以很好地提高查詢的性能,因爲OLAP應用大多數查詢需要頻繁地掃描一張很大的表。**假設有一張1億行的表,其中有一個時間戳屬性列。用戶的查詢需要從這張表中獲取一年的數據。如果按時間戳進行分區,則只需要掃描相應的分區即可。這就是前面介紹的分區修剪技術。
對於OLTP的應用,分區應該非常小心。在這種應用下,通常不可能會獲取一張大表10%的數據,大部分都是通過索引返回幾條記錄即可。而根據B+樹索引的原理可知,對於一張大表,一般的B+樹需要2~3次的磁盤IO。因此B+樹可以很好地完成操作,不需要分區的幫助,並且設計不好的分區會帶來嚴重的性能問題。
如很多開發團隊會認爲含有1000w行的表是一張非常巨大的表,所以他們往往會選擇採用分區,如對主鍵做10個HASH的分區,這樣每個分區就只有100w的數據了,因此查詢應該變得更快了。如select * from table where pk=@pk。但是有沒有考慮過這樣一種情況:100w和1000w行的數據本身構成的B+樹的層次都是一樣的,可能都是2~3層。那麼上述走主鍵分區的索引並不會帶來性能的提高。好的,如果1000w的B+樹高度是3,100w的B+樹高度是2,那麼上述按主鍵分區的索引可以避免1次IO,從而提高查詢的效率。這沒問題,但是這張表只有主鍵索引,沒有任何其他的列需要查詢的。如果還有類似如下的SQL:select * from table where key=@key,這時對於key的查詢需要掃描所有的10個分區,即使每個分區的查詢開銷爲2次IO,則一共需要20次IO。而對於原來單表的設計,對於KEY的查詢只需要2~3次IO。

分區鍵必須包含在主鍵字段

MySQL的分區字段,必須包含在主鍵字段內。主鍵的限制,每一個分區表中的公式中的列,必須在primary key/unique key中包括。分區字段必須包含在主鍵字段內,至於爲什麼MySQL會這樣考慮,CSDN的斑竹是這麼解釋的:爲了確保主鍵的效率,否則同一主鍵區的東西一個在A分區,一個在B分區,顯然會比較麻煩。

MySQL 5.7對分區的改進

在MySQL 5.6裏面,分區的信息是在MySQL Server層維護的(在.par文件裏面),InnoDB引擎層是不知道有分區這個概念的,InnoDB引擎層把每一個分區都當成一張普通的InnoDB表。在打開一個分區表時,會打開很多個分區,打開這些分區表就相當於打開了同等數量的InnoDB表,這需要更多內存存放InnoDB表的元數據和各種與ibd文件打開相關的各種cache與handler的信息。在MySQL 5.7裏面,InnoDB引入了Native Partitioning,它把分區的信息從Server層移到了InnoDB層,打開一個分區表和打開一個InnoDB表的內存開銷基本是一樣的。

使用remove移除分區是僅僅移除分區的定義,並不會刪除數據和drop PARTITION不一樣,後者會連同數據一起刪除
ALTER TABLE tablename REMOVE PARTITIONING ;
通過這種刪除分區的方式會將分區中的數據也刪除,但是通過刪除分區的方式刪除數據會比delete快很多,因爲它相當於刪除一個數據庫一樣因爲每個分區都是一個獨立的數據文件。用來刪除歷史分區數據是非常好的辦法。
alter table employees drop  PARTITION p4;
增加分區只能在最大端增加
alter table employees add PARTITION  (PARTITION p4 VALUES LESS THAN MAXVALUE);
拆分合並分區統稱爲重新定義分區,拆分分爲不會造成數據的丟失,只將會將數據從一個分區移動到另一個分區。
無論是拆分還是合併分區都不能改變分區原本的覆蓋範圍,並且合併分區只能合併連續的分區不能跳過分區合併;並且不能改變分區的類型,例如不能把range分區改成key分區等。
ALTER TABLE employees REORGANIZE PARTITION p0 INTO ( //將P0拆分成s1,s2兩個分區
    PARTITION s0 VALUES LESS THAN (3),
    PARTITION s1 VALUES LESS THAN (6)
);
ALTER TABLE employees REORGANIZE PARTITION s1,p1,p2 INTO ( //將s1,p1,p2合併爲a,b兩個分區
    PARTITION a VALUES LESS THAN (5),
    PARTITION b VALUES LESS THAN (16)
);

參考文獻

MySQL 分庫分表與分區的區別和思考
MySQL性能優化分區之實戰
搞懂MySQL分區
MySql表分區詳解
MySQL 5.7對分區的改進
MySQL RANGE分區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章