- SQL標準對於數據存儲的物理方面沒有提供太多的指導。SQL語言本身旨在獨立於其工作的模式,表,行或列的任何數據結構或媒體。儘管如此,大多數先進的數據庫管理系統已經開發了一些方法來確定用於存儲特定數據塊的物理位置,比如文件系統、硬件,甚至兩者。在MySQL中,InnoDB存儲引擎長期以來一直支持表空間的概念,而MySQL服務器,甚至在引入分區之前,可以配置爲使用不同的物理目錄來存儲不同的數據庫。
- 分區將這個概念更進一步,通過使您能夠根據需要在很大程度上設置的規則,將各個表的部分分佈到一個文件系統中。實際上,表的不同部分作爲單獨的表存儲在不同的位置。用於完成數據劃分的用戶選擇規則稱爲分區函數,它在在 MySQL 可以是模數, 簡單的匹配一組範圍或值列表,一個 內部哈希函數, 或一個線性哈希函數。函數是根據用戶指定的分區類型選擇的,並以用戶提供的表達式的值作爲參數。這個表達式可以是一個列值,作用於一個或多個列值的函數,也可以是一個或多個列值的集合,這取決於所使用的分區類型
- 在RANGE, LIST和 [LINEAR] HASH 分區的情況下,分區列的值被傳遞給分區函數,該函數返回一個整數值, 該值表示存儲特定記錄的分區的編號。這個函數必須是非常量和非隨機的。它可能不包含任何查詢,但是可以使用在MySQL中有效的SQL表達式,只要該表達式返回NULL或整數就行了。
- MySQL支持水平分區, 即可以將表的不同行分配給不同的物理分區。MySQL 5.7不支持垂直分區,其中一個表的不同列被分配給不同的物理分區。目前還沒有將垂直分區引入MySQL的計劃。
- 每個分區的數據和索引可以被分配到一個特定的目錄中,目錄使用CREATE TABLE語句的PARTITION子句的 DATA DIRECTORY 和INDEX DIRECTORY選項指定
- 表的分區表達式中使用的所有列必須是表可能具有的每個唯一鍵的一部分,包括任何主鍵
- 分區的優點
- 分區可以在一個表中存儲比單個磁盤或文件系統分區上的數據更多的數據。
- 通過刪除僅包含該數據的分區(或多個分區),可以很容易地從分區表中刪除其有用性的數據。相反添加新數據的過程在某些情況下可以很容易地通過添加一個或多個新的分區來存儲特定的數據。
- 由於滿足給定 where 子句的數據只能存儲在一個或多個分區上, 從而自動將所有剩餘的分區從搜索中排除, 因此可以極大地優化某些查詢,因爲在創建了分區表之後,由於在創建分區表後可以更改分區, 因此可以重新組織數據, 以增強在首次設置分區方案時可能不經常使用的頻繁查詢。。這種排除非匹配分區的能力 (因此, 它們所包含的任何行) 通常稱爲分區修剪。
- 此外,MySQL 5.7還支持查詢的顯式分區選擇。例如, SELECT * FROM t PARTITION (p0,p1) WHERE c < 5 只選擇在分區p0和p1中匹配WHERE條件的行。在本例中,MySQL不檢查表t的任何其他分區;當您已經知道要檢查哪些分區或分區時,這可以大大加快查詢速度。分區選擇還支持數據修改語句 DELETE, INSERT, REPLACE, UPDATE
- 類型:
- RANGE
- LIST
- COLUMNS
- RANGE COLUMNS
- LIST COLUMNS
- HASH
- KEY
- 不管您使用的分區類型是什麼,都必須記住,在創建時,分區總是自動編號,從0開始。當將新行插入到分區表中時,將使用這些分區號來標識正確的分區。例如,如果您的表使用4個分區,那麼這些分區的編號爲0,1,2和3。對於RANGE和LIST分區類型,必要確保爲每個分區號定義了一個分區。對於HASH分區,用戶提供的表達式必須計算爲大於0的整數值。對於KEY分區,這個問題由MySQL服務器內部使用的哈希函數自動處理。
- RANGE分區
- 按範圍分區的表以這樣的方式分區,即每個分區包含分區表達式值位於給定範圍內的行。範圍應該是連續的,但不是重疊的,並使用VALUES LESS THAN運算符定義。
- mysql> create table order_log(
- -> id int not null auto_increment,
- -> user_id int not null default 0 comment '用戶ID',
- -> goods_id int not null default 0 comment '商品ID',
- -> add_time int not null default 0 comment '訂單創建時間',
- -> order_year smallint not null default 0 comment '訂單創建時間的對應年份',
- -> primary key(
id
,order_year
) - -> )engine=innodb charset=utf8 comment="訂單表"
- -> PARTITION BY RANGE(order_year) (
- -> PARTITION p0 VALUES LESS THAN (2018),
- -> PARTITION p1 VALUES LESS THAN (2019),
- -> PARTITION p2 VALUES LESS THAN (2020),
- -> PARTITION p3 VALUES LESS THAN (2021),
- -> PARTITION p4 VALUES LESS THAN (2022),
- -> PARTITION p5 VALUES LESS THAN (2023),
- -> PARTITION p6 VALUES LESS THAN (2024),
- -> PARTITION p7 VALUES LESS THAN MAXVALUE
- -> );
- #上面的分區的意思是: 2018年以前的數據都存在p0分區中,2019年以前的都存在p1分區中,一直到p7,大於等於2024年的數據都存在p7中。
- #我們創建一個按年份分區的分區表,大家可能會有疑問,爲什麼我單獨用了order_year字段來單獨表示年份,而不是直接用add_time來進行分區,這是因爲innodb的索引機制導致的,innodb的所有輔助索引都會默認加上主鍵索引,所以主鍵索引越小越好,如果用id+add_time那就是8個字節,現在就是6個字節了
- 我們先查看分區表對應的數據
- mysql> select TABLE_SCHEMA,TABLE_NAME,PARTITION_NAME,PARTITION_EXPRESSION,TABLE_ROWS from information_schema.PARTITIONS where TABLE_NAME='order_log';
- +--------------+------------+----------------+----------------------+------------+
- | TABLE_SCHEMA | TABLE_NAME | PARTITION_NAME | PARTITION_EXPRESSION | TABLE_ROWS |
- +--------------+------------+----------------+----------------------+------------+
- | wordpress | order_log | p0 | order_year | 0 |
- | wordpress | order_log | p1 | order_year | 0 |
- | wordpress | order_log | p2 | order_year | 0 |
- | wordpress | order_log | p3 | order_year | 0 |
- | wordpress | order_log | p4 | order_year | 0 |
- | wordpress | order_log | p5 | order_year | 0 |
- | wordpress | order_log | p6 | order_year | 0 |
- | wordpress | order_log | p7 | order_year | 0 |
- +--------------+------------+----------------+----------------------+------------+
- 8 rows in set (0.00 sec)
- #我們可以看到TABLE_ROWS都是0行,這時候我們插入三條數據看一下
- mysql> insert into order_log select 1,1,1,unix_timestamp('2018-12-12 12:12:12'),2018;
- mysql> insert into order_log select 1,1,1,unix_timestamp('2019-12-12 12:12:12'),2019;
- mysql> insert into order_log select 1,1,1,unix_timestamp('2025-12-12 12:12:12'),2025;
- #按照我們前面的分區算法來看的話,應該是p1有1條,p2有1條,p7有一條,那麼這時候我們再看下
- mysql> select TABLE_SCHEMA,TABLE_NAME,PARTITION_NAME,PARTITION_EXPRESSION,TABLE_ROWS from information_schema.PARTITIONS where TABLE_NAME='order_log';
- +--------------+------------+----------------+----------------------+------------+
- | TABLE_SCHEMA | TABLE_NAME | PARTITION_NAME | PARTITION_EXPRESSION | TABLE_ROWS |
- +--------------+------------+----------------+----------------------+------------+
- | wordpress | order_log | p0 | order_year | 0 |
- | wordpress | order_log | p1 | order_year | 1 |
- | wordpress | order_log | p2 | order_year | 1 |
- | wordpress | order_log | p3 | order_year | 0 |
- | wordpress | order_log | p4 | order_year | 0 |
- | wordpress | order_log | p5 | order_year | 0 |
- | wordpress | order_log | p6 | order_year | 0 |
- | wordpress | order_log | p7 | order_year | 1 |
- +--------------+------------+----------------+----------------------+------------+
- 8 rows in set (0.00 sec)
- #的確跟我們的預期一樣,我們可以在查詢中直接利用到分區,爲了看到效果,我們插入一些測試數據
- mysql> DELIMITER $$
- mysql> CREATE PROCEDURE insert_test_val(in num_limit int,in rand_limit int)
- -> BEGIN
- -> DECLARE i int default 1;
- -> DECLARE user_id int default 1;
- -> DECLARE goods_id int default 1;
- -> DECLARE add_time int default 1;
- -> DECLARE order_year int default 1;
- -> WHILE i<=num_limit do
- -> set user_id = FLOOR(rand()*rand_limit);
- -> set goods_id = FLOOR(rand()*rand_limit);
- -> set add_time = unix_timestamp(now())+(86400floor(730rand()));
- -> set order_year=from_unixtime(add_time,'%Y');
- -> INSERT into order_log values (null,user_id,goods_id,add_time,order_year);
- -> set i = i + 1;
- -> END WHILE;
- -> END;
- -> $$
- Query OK, 0 rows affected (0.01 sec)
- mysql> DELIMITER ;
- mysql> call insert_test_val(100000,10);
- #這時候我們再看一下數據
- mysql> select TABLE_SCHEMA,TABLE_NAME,PARTITION_NAME,PARTITION_EXPRESSION,TABLE_ROWS from information_schema.PARTITIONS where TABLE_NAME='order_log';
- +--------------+------------+----------------+----------------------+------------+
- | TABLE_SCHEMA | TABLE_NAME | PARTITION_NAME | PARTITION_EXPRESSION | TABLE_ROWS |
- +--------------+------------+----------------+----------------------+------------+
- | wordpress | order_log | p0 | order_year | 0 |
- | wordpress | order_log | p1 | order_year | 1 |
- | wordpress | order_log | p2 | order_year | 50045 |
- | wordpress | order_log | p3 | order_year | 61557 |
- | wordpress | order_log | p4 | order_year | 10705 |
- | wordpress | order_log | p5 | order_year | 0 |
- | wordpress | order_log | p6 | order_year | 0 |
- | wordpress | order_log | p7 | order_year | 1 |
- +--------------+------------+----------------+----------------------+------------+
- 8 rows in set (0.00 sec)
- #我們針對order_year=2020的去查詢
- mysql> explain select * from order_log where order_year=2020 order by id asc limit 20;
- +----+-------------+-----------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+-------------+-----------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
- | 1 | SIMPLE | order_log | p3 | index | NULL | PRIMARY | 6 | NULL | 20 | 10.00 | Using where |
- +----+-------------+-----------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
- 1 row in set, 1 warning (0.00 sec)
- #這時候我們可以看到partitions列有p3,那麼說明查詢了時候過濾了其他分區
- LIST分區
- MySQL中的List分區在許多方面類似於RANGE分區。在按RANGE進行分區時,必須顯式地定義每個分區。這兩種類型的分區之間的主要區別是,在LIST分區中,每個分區都是根據一組值列表中的一個列值的成員來定義和選擇的,而不是在一系列連續的值範圍內。這是通過用PARTITION BY LIST(expr)來完成的,其中expr是一個列值或基於列值的表達式,返回一個整數值,然後用一個 VALUES IN (value_list)來定義每個分區,其中value_list是一個逗號分隔的整數列表
- 例如我是某個商品在某個省的總代理,然後我要看下我每個子代理下面的數據
- mysql> create table goods_sales(
- -> id int not null auto_increment ,
- -> num int not null default 0 comment '數量',
- -> money int not null default 0 comment '金額',
- -> agency_id tinyint unsigned not null default 0 comment '代理ID',
- -> primary key (
id
,agency_id
) - -> )engine=innodb charset=utf8
- -> PARTITION BY LIST(agency_id) (
- -> PARTITION p0 VALUES IN (1,2,3,4,5),
- -> PARTITION p1 VALUES IN (6,7,8,9,10),
- -> PARTITION p2 VALUES IN (11,12,13,14,15),
- -> PARTITION p3 VALUES IN (16,17,18,19,20)
- -> );
- 與RANGE分區的情況不同,沒有“catch-all”例如MAXVALUE;分區表達式的所有期望值都應該包含在PARTITION ... VALUES IN (...) 子句中,包含一個未匹配的分區列值的INSERT 語句會出現錯誤,如本例所示:
- mysql> insert into goods_sales select 1,20,10000,21;
- ERROR 1526 (HY000): Table has no partition for value 21
- 您可以使用IGNORE關鍵字來忽略此類型的錯誤。如果這樣做, 則不會插入包含不匹配的分區列值的行, 但會插入具有匹配值的任何行, 並且不會報告任何錯誤:
- mysql> insert IGNORE into goods_sales (
id
,num
,money
,agency_id
) values (1,2,3,4),(2,3,4,5),(1,2,3,100); - Query OK, 2 rows affected, 1 warning (0.01 sec)
- Records: 3 Duplicates: 1 Warnings: 1
- mysql> select * from goods_sales;
- +----+-----+-------+-----------+
- | id | num | money | agency_id |
- +----+-----+-------+-----------+
- | 1 | 2 | 3 | 4 |
- | 2 | 3 | 4 | 5 |
- +----+-----+-------+-----------+
- 2 rows in set (0.00 sec)
- #在這裏,我們可以看到(1,2,3,100)這條數據並沒有插入成功,但也沒影響到前面的兩條數據
- MySQL 5.7 提供對 LIST COLUMNS分區的支持。這是 LIST分區的變體, 使您可以使用非整數類型的列來分區列, 也可以使用多列作爲分區鍵
- COLUMNS分區
- 接下來的兩部分討論COLUMNS分區,它們是RANGE和LIST分區的變體.. COLUMNS分區允許在分區鍵中使用多個列。所有這些列都將被考慮在哪個分區中放置行, 以及確定在分區修剪中要檢查哪些分區以匹配行。
- 此外,RANGE COLUMNS分區和 LIST COLUMNS分區支持使用非整數列來定義值範圍或列表成員。允許的數據類型如下表所示:
- 所有的數字類型 : TINYINT, SMALLINT, MEDIUMINT, INT (INTEGER), and BIGINT(這與按RANGE和 LIST進行分區相同),其他數值數據類型(如DECIMAL or FLOAT)不能作爲分區列。
- 日期類型:DATE 和 DATETIME.
- 字符串類型:CHAR, VARCHAR, BINARY和 VARBINARY, 不支持TEXT和BLOB列
- RANGE COLUMNS分區
- Range columns分區類似於range分區,但是允許您使用基於多個列值的範圍來定義分區。此外,您可以使用除整數類型以外的類型的列來定義範圍。
- RANGE COLUMNS分區與RANGE 分區有很大不同,有以下幾種方式:
- RANGE COLUMNS不接受表達式,只接受列的名稱
- RANGE COLUMNS接受一個或多個列的列表。
- RANGE COLUMNS分區基於元組之間的比較(列值列表),而不是標量值之間的比較。 在RANGE COLUMNS分區中放置行也是基於元組之間的比較。
- RANGE COLUMN分區列不限制爲integer列.string、DATE和DATETIME列也可以用作分區列。
- 下面我們創建一個RANGE COLUMNS分區
- mysql> CREATE TABLE test1(
- -> id int not null auto_increment,
- -> key_a int not null default 0,
- -> key_b int not null default 0,
- -> PRIMARY KEY (
id
,key_a
,key_b
) - -> )engine=innodb charset=utf8
- -> PARTITION BY RANGE COLUMNS(key_a, key_b) (
- -> PARTITION p0 VALUES LESS THAN (1, 20),
- -> PARTITION p1 VALUES LESS THAN (2, 15),
- -> PARTITION p2 VALUES LESS THAN (2, 20),
- -> PARTITION p3 VALUES LESS THAN (MAXVALUE, MAXVALUE)
- -> );
- #然後我們插入幾條數據看一下
- mysql> insert into test1 (
key_a
,key_b
) values (1,15),(2,10),(2,20),(2,30) - mysql> SELECT PARTITION_NAME,TABLE_ROWS FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME = 'test1';
- +----------------+------------+
- | PARTITION_NAME | TABLE_ROWS |
- +----------------+------------+
- | p0 | 1 |
- | p1 | 1 |
- | p2 | 0 |
- | p3 | 2 |
- +----------------+------------+
- 4 rows in set (0.01 sec)
- #我們對比一下,看下這數據是怎麼進去的,多列分區我們比較的是行而不是標量值。我們可以將插入的行值與用於在表test1中定義分區的VALUES THAN LESS THAN子句中插入的行值進行比較,如下所示:
- mysql> SELECT (1,15) < (1,20), (2,10) < (1,20), (2,20) < (1,20),(2,30)< (1,20);
- +-----------------+-----------------+-----------------+----------------+
- | (1,15) < (1,20) | (2,10) < (1,20) | (2,20) < (1,20) | (2,30)< (1,20) |
- +-----------------+-----------------+-----------------+----------------+
- | 1 | 0 | 0 | 0 |
- +-----------------+-----------------+-----------------+----------------+
- 1 row in set (0.01 sec)
- #說明p0有一條數據
- mysql> SELECT (1,15) < (2,15), (2,10) < (2,15), (2,20) < (2,15),(2,30)< (2,15);
- +-----------------+-----------------+-----------------+----------------+
- | (1,15) < (2,15) | (2,10) < (2,15) | (2,20) < (2,15) | (2,30)< (2,15) |
- +-----------------+-----------------+-----------------+----------------+
- | 1 | 1 | 0 | 0 |
- +-----------------+-----------------+-----------------+----------------+
- 1 row in set (0.00 sec)
- #這裏看p1有兩條數據,但(1,15)已經在p0了,所以也只有一條數據
- mysql> SELECT (1,15) < (2,20), (2,10) < (2,20), (2,20) < (2,20),(2,30)< (2,20);
- +-----------------+-----------------+-----------------+----------------+
- | (1,15) < (2,20) | (2,10) < (2,20) | (2,20) < (2,20) | (2,30)< (2,20) |
- +-----------------+-----------------+-----------------+----------------+
- | 1 | 1 | 0 | 0 |
- +-----------------+-----------------+-----------------+----------------+
- 1 row in set (0.00 sec)
- #p2一條數據都沒有,因爲(1,15),(2,10)分別存在p0和p1裏面去了
- LIST COLUMNS分區
- Mysql 5.7支持 LIST COLUMNS分區,這是LIST分區的一種變體,它允許使用多列作爲分區鍵, 以及用非整數類型的列作爲分區列的類型,您可以使用string類型、 DATE和DATETIME列
- mysql> CREATE TABLE test3(
- -> id int not null auto_increment,
- -> key_a int not null default 0,
- -> key_b int not null default 0,
- -> PRIMARY KEY (
id
,key_a
) - -> )engine=innodb charset=utf8
- -> PARTITION BY LIST COLUMNS(key_a) (
- -> PARTITION p0 VALUES IN(1, 2,3,4,5),
- -> PARTITION p1 VALUES IN(6, 7,8,9,10),
- -> PARTITION p2 VALUES IN(11,12,13,14, 15)
- -> );
- #這個LIST COLUMNS分區,我目前感覺跟LIST分區沒什麼兩樣,最多就是可以string,DATE和DATETIME列分區,根本就看不出多列的感覺,遠遠沒有RANGE COLUMNS分區和RANGE分區的效果差異
- HASH分區
- 通過HASH分區主要用於確保預定數量的分區之間的數據的均勻分佈。 使用range或list分區,您必須明確指定要存儲給定列值或列值集合的分區; 使用hash分區,MySQL會爲您處理此問題,並且您只需根據要進行hash的列值和分區表的分區數量指定列值或表達式即可。
- 使用HASH分區來對錶進行分區,需要在 CREATE TABLE語句上面追加一個PARTITION BY HASH (expr)子句,expr是一個表達式,返回一個整數,這也可以簡單是一個列的名稱, 其類型是 MySQL 的整數類型之一,另外,您很可能希望使用PARTITIONS num來執行這個操作,其中num是一個正整數,表示表要劃分的分區數
- 下面我們開始一個案例
- mysql> CREATE TABLE test4(
- -> id int not null auto_increment,
- -> key_a int not null default 0,
- -> key_b int not null default 0,
- -> PRIMARY KEY (
id
,key_a
) - -> )engine=innodb charset=utf8
- -> PARTITION BY HASH(key_a)
- -> PARTITIONS 4;
- Query OK, 0 rows affected (0.28 sec)
- #它不是均勻分佈嗎,我們添加一萬條數據看一下,直接把RANGE分區下的存儲過程改下,具體改成什麼樣我就不發了,然後調用一下插入了一萬條數據,
- mysql> SELECT PARTITION_NAME,TABLE_ROWS FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME = 'test4';
- +----------------+------------+
- | PARTITION_NAME | TABLE_ROWS |
- +----------------+------------+
- | p0 | 3039 |
- | p1 | 3003 |
- | p2 | 2012 |
- | p3 | 1946 |
- +----------------+------------+
- 4 rows in set (0.02 sec)
- #感覺差異有點大,我們嘗試着再插入十萬條數據看一下
- mysql> SELECT PARTITION_NAME,TABLE_ROWS FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME = 'test4';
- +----------------+------------+
- | PARTITION_NAME | TABLE_ROWS |
- +----------------+------------+
- | p0 | 28017 |
- | p1 | 27469 |
- | p2 | 27016 |
- | p3 | 26785 |
- +----------------+------------+
- 4 rows in set (0.00 sec)
- KEY分區
- 按key分區與通過hash分區類似,不同之處在於hash分區採用用戶定義的表達式,用於key分區的hash函數由MySQL服務器提供。NDB集羣爲此目的使用MD5();對於使用其他存儲引擎的表,服務器使用它自己的內部哈希函數,該函數基於與PASSWORD()相同的算法。
- CREATE TABLE ... PARTITION BY KEY 的語法規則類似於創建一個由散列分區的表。主要區別在這裏:
- 使用KEY而不是HASH
- KEY僅包含零個或多個列名稱的列表。作爲分區鍵的任何列必須包含表的主鍵的部分或全部,如果該表有一個。如果沒有指定列名稱作爲分區鍵,則使用表的主鍵,如果有的話。如果沒有主鍵,但有一個唯一鍵,那麼分區鍵使用唯一鍵,但是,如果惟一鍵列沒有定義爲 NOT NULL,那麼就會創建分區失敗
- 與其他分區類型不同,用於按KEY分區的列不限於整數或NULL值。例如下面的create_table語句是有效的
- 針對key分區表,你不能執行ALTER TABLE DROP PRIMARY KEY,如果執行了會報一個錯誤 ERROR 1466 (HY000): Field in list of fields for partition function not found in table.
- 下面我們看一下如何創建
- mysql> create table test5(
- -> id char(11) not null primary key,
- -> add_time int not null default 0
- -> )engine=innodb charset=utf8
- -> PARTITION BY LINEAR KEY ()
- -> PARTITIONS 3;
參考資料:https://dev.mysql.com/doc/refman/5.7/en/partitioning.html