1. 程式人生 > >MySQL效能優化(六):分割槽

MySQL效能優化(六):分割槽

一: 分割槽簡介

分割槽是根據一定的規則,資料庫把一個表分解成多個更小的、更容易管理的部分。就訪問資料庫應用而言,邏輯上就只有一個表或者一個索引,但實際上這個表可能有N個物理分割槽物件組成,每個分割槽都是一個獨立的物件,可以獨立處理,可以作為表的一部分進行處理。分割槽對應用來說是完全透明的,不影響應用的業務邏輯。

分割槽有利於管理非常大的表,它採用分而治之的邏輯,分割槽引入了分割槽鍵的概念,分割槽鍵用於根據某個區間值(或者範圍值)、特定值列表或者hash函式值執行資料的聚集,讓資料根據規則分佈在不同的分割槽中,讓一個大物件碧昂城一些小物件。

MySQL分割槽即可以對資料進行分割槽也可以對索引進行分割槽。

分割槽型別

  • range分割槽:基於一個給定的連續區間範圍(區間要求連續並且不能重疊),把資料分配到不同的分割槽
  • list分割槽:類似於range分割槽,區別在於list分割槽是居於枚舉出的值列表分割槽,range是基於給定的連續區間範圍分割槽
  • hash分割槽:基於給定的分割槽個數,把資料分配到不同的分割槽
  • key分割槽:類似於hash分割槽

注意:無論哪種分割槽,要麼你分割槽表上沒有主鍵/唯一鍵,要麼分割槽表的主鍵/唯一鍵都必須包含分割槽鍵,也就是說不能使用主鍵/唯一鍵欄位之外的其它欄位分割槽。

MySQL分割槽的有限主要包括以下4個方面:

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

分割槽和水平分表功能類似,將一個大表的資料分割到多張小表中去,由於查詢不需要全表掃描了,只需要掃描某些分割槽,所以分割槽能提高查詢速度。

  • 水平分表需要使用者預先手動顯式創建出多張分表(如tbl_user0, tbl_user1, tbl_user2),在物理上實實在在的建立多張表,通過客戶端代理(Sharding-JDBC等)或者中介軟體代理(Mycat等)來實現分表邏輯。

  • 分割槽是MySQL的一個外掛Plugin功能,將一張大表的資料在資料庫底層分成多個分割槽檔案(如tbl_user#P#p0.ibd, tbl_user#P#p1.ibd, tbl_user#P#p2.ibd),和水平分表不同的是分割槽不需要顯式的建立“分表”,資料庫會自動建立分割槽檔案的,使用者看到的只是一張普通的表,其實是對應的是多個分割槽,這個是對使用者是遮蔽的、透明的,在使用上和使用一張表完全一樣,不需要藉助任何功能來實現。分割槽是一種邏輯上的水平分表,在物理層面還是一張表。

二:資料庫檔案

CREATE TABLE `tbl_user_innodb` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `username` varchar(255) DEFAULT NULL,
   `email` varchar(20) DEFAULT NULL,
   `age` tinyint(4) DEFAULT NULL,
   `type` int(11) DEFAULT NULL,
   `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=5100002 DEFAULT CHARSET=utf8;

 CREATE TABLE `tbl_user_myisam` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `username` varchar(255) DEFAULT NULL,
   `email` varchar(20) DEFAULT NULL,
   `age` tinyint(4) DEFAULT NULL,
   `type` int(11) DEFAULT NULL,
   `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`)
 ) ENGINE=myisam AUTO_INCREMENT=5100002 DEFAULT CHARSET=utf8;

通過show variables like ‘%datadir%’;命令檢視mysql的data存放目錄,進入所在的資料庫目錄(如test),不同的引擎資料庫檔案格式不同

  • myisam
    • .frm : 儲存表結構
    • .MYD : 儲存表資料
    • .MYI : 儲存索引檔案
  • innodb: 只有設定成獨立表空間才能做成功表分割槽
    • .frm : 表結構
    • .ibd : 資料 + 索引

這裡寫圖片描述

三:插入500W條資料

CREATE TABLE `tbl_user_no_part` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `username` varchar(255) DEFAULT NULL,
   `email` varchar(20) DEFAULT NULL,
   `age` tinyint(4) DEFAULT NULL,
   `type` int(11) DEFAULT NULL,
   `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

-- 修改mysql預設的結束符號,預設是分號;但是在函式和儲存過程中會使用到分號導致解析不正確
delimiter $$

-- 隨機生成一個指定長度的字串
create function rand_string(n int) returns varchar(255) 
begin 
 # 定義三個變數
 declare chars_str varchar(100) default 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
 declare return_str varchar(255) default '';
 declare i int default 0;

 while i < n do 
   set return_str = concat(return_str, substring(chars_str, floor(1+rand()*52), 1));
   set i = i + 1;
 end while;
 return return_str;
end $$

-- 建立插入的儲存過程
create procedure insert_user(in start int(10), in max_num int(10))
begin
    declare i int default 0; 
    set autocommit = 0;  
    repeat
        set i = i + 1;
        insert into tbl_user_no_part values ((start+i) ,rand_string(8), concat(rand_string(6), '@random.com'), 1+FLOOR(RAND()*100), 3, now());
        until i = max_num
    end repeat;
   commit;
end $$

-- 將命令結束符修改回來
delimiter ;

-- 呼叫儲存過程,插入500萬資料,需要等待一會時間,等待執行完成
call insert_user(100001,5000000);
-- Query OK, 0 rows affected (7 min 49.89 sec) 我的Macbook Pro i5 8G記憶體用了8分鐘才執行完

select count(*) from tbl_user_no_part;

四:range分割槽

MySQL有五種分割槽型別 range、list、hash、key、子分割槽,其中最常用的是range和list分割槽

-- 檢視mysql版本
select version();

-- 檢視分割槽外掛是否啟用 partition active
show plugins;

對於低版本的MySQL,如果InnoDB引擎要想分割槽成功,需要在my.conf中設定innodb_file_per_table=1 設定成獨立表空間
獨立表空間:每張表都有對應的.ibd檔案
innodb_file_per_table=1

range分割槽:給定一個連續區間的範圍值進行分割槽,某個欄位的值滿足這個範圍就會被分配到該分割槽。適用於欄位的值是連續的區間的欄位,如 日期範圍, 連續的數字

-- 語法
create table <table> (
    // 欄位
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1
partition by range (分割槽欄位) (
  partition <分割槽名稱> values less than (Value),
  partition <分割槽名稱> values less than (Value),
  ...
  partition <分割槽名稱> values less than maxvalue
);

range:表示按範圍分割槽
分割槽欄位:表示要按照哪個欄位進行分割槽,可以是一個欄位名,也可以是對某個欄位進行表示式運算如year(create_time),使用range最終的值必須是數字
分割槽名稱: 要保證不同,也可以採用 p0、p1、p2 這樣的分割槽名稱,
less than : 表示小於
Value : 表示要小於某個具體的值,如 less than (10) 那麼分割槽欄位的值小於10的都會被分到這個分割槽
maxvalue: 表示一個最大的值

注意:range 對應的分割槽鍵值必須是數字值,可以使用range columns(分割槽欄位) 對非int型做分割槽,如字串,對於日期型別的可以使用year()、to_days()、to_seconds()等函式

create table emp_date(
    id int not null,
    separated date not null default '9999-12-31'
)
partition by range columns(separated) (
    partiontion p0 values less than ('1990-01-01'),
    partiontion p0 values less than ('2001-01-01'),
    partiontion p0 values less than ('2018-01-01')
);

分割槽可以在建立表的時候進行分割槽,也可以在建立表之後進行分割槽

alter table <table> partition by RANGE(id) (
    PARTITION p0 VALUES LESS THAN (1000000),
    PARTITION p1 VALUES LESS THAN (2000000),
    PARTITION p2 VALUES LESS THAN (3000000),
    PARTITION p3 VALUES LESS THAN (4000000),
    PARTITION p4 VALUES LESS THAN MAXVALUE 
);
-- 建立分割槽表
CREATE TABLE `tbl_user_part` (
   `id` int(11) NOT NULL ,
   `username` varchar(255) DEFAULT NULL,
   `email` varchar(20)     DEFAULT NULL,
   `age` tinyint(4)        DEFAULT NULL,
   `type` int(11)          DEFAULT NULL,
   `create_time` datetime  DEFAULT CURRENT_TIMESTAMP
   -- PRIMARY KEY (`id`,`age`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
PARTITION BY RANGE (age) (
    PARTITION p0 VALUES LESS THAN (20),
    PARTITION p1 VALUES LESS THAN (40),
    PARTITION p2 VALUES LESS THAN (60),
    PARTITION p3 VALUES LESS THAN (80),
    PARTITION p4 VALUES LESS THAN MAXVALUE
);

這裡寫圖片描述
在建立分割槽的時候經常會遇到這個錯誤:A PRIMARY KEY must include all columns in the table’s partitioning function。意思是說分割槽的欄位必須是要包含在主鍵當中。 可以使用PRIMARY KEY (id,xxx)來將多個欄位作為主鍵。在做分割槽表時,選擇分割槽的依據欄位時要謹慎,需要仔細斟酌這個欄位拿來做為分割槽依據是否合適,這個欄位加入到主鍵中做為複合主鍵是否適合。

使用range分割槽時表結構要麼沒有主鍵,要麼分割槽欄位必須是主鍵。

-- 將tbl_user_no_part表中的資料複製到tbl_user_part表中(資料量比較多,可能要等幾分鐘)
INSERT INTO tbl_user_part SELECT * FROM tbl_user_no_part;

SELECT count(*) FROM tbl_user_no_part WHERE age > 25 AND age < 30;
SELECT count(*) FROM tbl_user_part WHERE age > 25 AND age < 30;

這裡寫圖片描述
從查詢結果看,當查詢條件中包括分割槽欄位時,分割槽確實能提高查詢效率

五:list 分割槽

設定若干個固定值進行分割槽,如果某個欄位的值在這個設定的值列表中就會被分配到該分割槽。適用於欄位的值區分度不高的,或者值是有限的,特別是像列舉這樣特點的列。list分割槽使用in表示一些固定的值的列表

-- 語法
create table <table> (
    // 欄位
) ENGINE=資料庫引擎  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1
partition by LIST (分割槽欄位或者基於該欄位的返回的整數值的表示式) (
  partition <分割槽名稱> values IN (Value1,Value2, Value3),
  ...
  partition <分割槽名稱> values IN (Value4, Value5),
);

columns分割槽

在mysql5.5之前range分割槽和list分割槽只支援整數分割槽,可以通過額外的函式運算或者額外的轉換從而得到一個整數。columns分割槽分為 range columns 和 list columns 兩種,支援整數(tinyint到bigint, 不支援decimal 和float)、日期(date、datetime)、字串(char、varchar、binary、varbinary)三大資料型別。

columns分割槽支援一個或者多個欄位作為分割槽鍵,不支援表示式作為分割槽鍵,這點區別於range 和 list 分割槽。需要注意的是range columns 分割槽鍵的比較是基於元組的比較,也就是基於欄位組的比較,這和range分割槽有差異。

create talbe rc3 (
    a int,
    b int
)
partition by range columns(a, b) (
    partition p01 values less than (0, 10),
    partition p02 values less than (10, 10),
    partition p03 values less than (10, 20),
    partition p04 values less than (10, 35),
    partition p05 values less than (10, maxvalue),
    partition p06 values less than (maxvalue, maxvalue),
);

insert into rc3(a, b) values(1, 10);

select (1, 10) < (10, 10) from dual;

-- 根據結果存放到p02分割槽上了
select
    partition_name,
    partition_expression,
    partition_description,
    table_rows
from information_schema.partitions
where table_schema = schema() and table_name = 'rc3';   

range columns分割槽鍵的比較(元組的比較)其實就是多列排序,先根據a欄位排序再根據b欄位排序,根據排序結果來分割槽存放資料,和range單欄位的分割槽排序的規則實際上是一樣的

六:hash分割槽

Hash分割槽主要用來分散熱點讀,確保資料在預先確定個數的分割槽中可能的平均分佈。對一個表執行Hash分割槽時,mysql會對分割槽鍵應用一個雜湊函式,以此確定資料應當放在N個分割槽中的哪個分割槽。

mysql支援兩種hash分割槽,

  • 常規hash分割槽和線性hash分割槽(linear hash分割槽),常規hash分割槽使用的是取模演算法,對應一個表示式expr是可以計算出它被儲存到哪個分割槽中,N = MOD(expr, num)
  • 線性hash分割槽使用的是一個線性的2的冪運演算法則。

對指定的欄位(整型欄位)進行雜湊,將記錄平均的分配到分割槽中,使得所有分割槽的資料比較平均。 hash分割槽只需要指定要分割槽的欄位和要分成幾個分割槽,
expr是一個欄位值或者基於某列值雲散返回的一個整數,expr可以是mysql中有效的任何函式或者其它表示式,只要它們返回一個即非常熟也非隨機數的整數。
num 表示分割槽數量

-- HASH
create table <table> (
    // 欄位
) ENGINE=資料庫引擎  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1
PARTITION BY HASH(expr)
PARTITIONS <num>;

常規hash分割槽方式看上去挺不錯的,通過取模的方式來資料儘可能平均分佈在每個分割槽,讓每個分割槽管理的資料都減少,提高查詢效率,可是當我們要增加分割槽時或者合併分割槽,問題就來了,假設原來是5個常規hash分割槽,現在需要增加一個常規分割槽,原來的取模演算法是MOD(expr, 5), 根據餘數0~4分佈在5個分割槽中,現在新增一個分割槽後,取模演算法變成MOD(expr, 6),根據餘數0~6分割槽在6個分割槽中,原來5個分割槽的資料大部分都需要通過重新計算進行重新分割槽。

常規hash分割槽在管理上帶來了的代價太大,不適合需要靈活變動分割槽的需求。為了降低分割槽管理上的代價,mysql提供了線性hash分割槽,分割槽函式是一個線性的2的冪的運演算法則。同樣線性hash分割槽的記錄被存在那個分割槽也是能被計算出來的。線性hash分割槽的優點是在分割槽維護(增加、刪除、合併、拆分分割槽)時,mysql能夠處理的更加迅速,缺點是:對比常規hash分割槽,線性hash各個分割槽之間資料的分佈不太均衡。

-- LINEAR HASH
create table <table> (
    // 欄位
) ENGINE=資料庫引擎  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1
PARTITION BY LINEAR HASH(expr)
PARTITIONS <num>;

七:key分割槽

按照key進行分割槽非常類似於按照hash進行分割槽,只不過hash分割槽允許使用使用者自定義的表示式,而key分割槽不允許使用用於自定義的表示式,需要使用mysql伺服器提供的hash函式,同時hash分割槽只支援整數分割槽,而key分割槽支援使用出blob or text型別外的其他型別的列作為分割槽鍵。

和hash功能一樣,不同的是分割槽的欄位可以是非int型別,如字串、日期等型別。

可以使用partition by key(expr)子句來建立一個key分割槽表,expr是零個或者多個欄位名的列表。key分割槽也支援線性分割槽linear key


partition by key(expr) partitions num;

-- 不指定預設首選主鍵作為分割槽鍵,在沒有主鍵的情況下會選擇非空唯一鍵作為分割槽鍵
partition by key() partitions num;

-- linear key
partition by linear key(expr)
create table <table> (
    // 欄位
) ENGINE=資料庫引擎  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1
PARTITION BY HASH(分割槽欄位名)
PARTITIONS <count>;

八:子分割槽

子分割槽(subpartition):是分割槽表中對每個分割槽的再次分割,又被稱為複合分割槽,支援對range和list進行子分割槽,子分割槽即可以使用hash分割槽也可以使用key分割槽。複合分割槽適用於儲存非常大量的資料記錄。

-- 根據年進行分割槽
-- 再根據天數分割槽
-- 3個range分割槽(p0,p1,p2)又被進一步分成2個子分割槽,實際上整個分割槽被分成了 3 x 2 = 6個分割槽
create table ts (
    id int, 
    purchased date
) 
partition by range(year(purchased))
subpartition by hash(to_days(purchased)) subpartitions 2 
(
    partition p0 values less than (1990),
    partition p0 values less than (2000),
    partition p0 values less than maxvalue
);
CREATE TABLE IF NOT EXISTS `sub_part` (
  `news_id` int(11) NOT NULL  COMMENT '新聞ID',
  `content` varchar(1000) NOT NULL DEFAULT '' COMMENT '新聞內容',
  `u_id`  int(11) NOT NULL DEFAULT 0s COMMENT '來源IP',
  `create_time` DATE NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '時間'
) ENGINE=INNODB  DEFAULT CHARSET=utf8
PARTITION BY RANGE(YEAR(create_time))
SUBPARTITION BY HASH(TO_DAYS(create_time))
(
PARTITION p0 VALUES LESS THAN (1990) (SUBPARTITION s0, SUBPARTITION s1, SUBPARTITION s2),
PARTITION p1 VALUES LESS THAN (2000) (SUBPARTITION s3, SUBPARTITION s4, SUBPARTITION good),
PARTITION p2 VALUES LESS THAN MAXVALUE (SUBPARTITION tank0, SUBPARTITION tank1, SUBPARTITION tank3)
);

九:管理分割槽

mysql不禁止在分割槽鍵值上使用null,分割槽鍵可能是一個欄位或者一個使用者定義的表示式,一般情況下,mysql的分割槽把null值當做零值或者一個最小值進行處理。range分割槽中,null值會被當做最小值來處理;list分割槽中null值必須出現在列舉列表中,否則不被接受;hash/key分割槽中,null值會被當做領值來處理。

mysql提供了新增、刪除、重定義、合併、拆分分割槽的命令,這些操作都可以通過alter table 命令來實現

-- 刪除list或者range分割槽(同時刪除分割槽對應的資料)
alter table <table> drop partition <分割槽名稱>;

-- 新增分割槽
-- range新增新分割槽
alter table <table> add partition(partition p4 values less than MAXVALUE);

-- list新增新分割槽
alter table <table> add partition(partition p4 values in (25,26,28));

-- hash重新分割槽
alter table <table> add partition partitions 4;

-- key重新分割槽
alter table <table> add partition partitions 4;

-- 子分割槽新增新分割槽,雖然我沒有指定子分割槽,但是系統會給子分割槽命名的
alter table <table> add partition(partition p3 values less than MAXVALUE);

-- range重新分割槽
ALTER TABLE user REORGANIZE PARTITION p0,p1,p2,p3,p4 INTO (PARTITION p0 VALUES LESS THAN MAXVALUE);

-- list重新分割槽
ALTER TABLE <table> REORGANIZE PARTITION p0,p1,p2,p3,p4 INTO (PARTITION p0 VALUES in (1,2,3,4,5));

分割槽優點

1,分割槽可以分在多個磁碟,儲存更大一點

2,根據查詢條件,也就是where後面的條件,查詢只查詢相應的分割槽不用全部查找了

3,進行大資料搜尋時可以進行並行處理。

4,跨多個磁碟來分散資料查詢,來獲得更大的查詢吞吐量