傳統的分庫分表都是經過應用層邏輯實現的,對於數據庫層面來講,都是普通的表和庫。html
首先,在單臺數據庫服務器性能足夠的狀況下,分庫對於數據庫性能是沒有影響的。在數據庫存儲上,database
只起到一個namespace
的做用。database
中的表文件存儲在一個以database名
命名的文件夾中。好比下面的employees
數據庫:mysql
mysql> show tables in employees; +---------------------+ | Tables_in_employees | +---------------------+ | departments | | dept_emp | | dept_manager | | employees | | salaries | | titles | +---------------------+
在操做系統中看是這樣的:git
# haitian at haitian-coder.local in /usr/local/var/mysql/employees on git:master ● [21:19:47] → ls db.opt dept_emp.frm dept_manager.ibd salaries.frm titles.ibd departments.frm dept_emp.ibd employees.frm salaries.ibd departments.ibd dept_manager.frm employees.ibd titles.frm
database
不是文件,只起到namespace
的做用,因此MySQL
對database
大小固然也是沒有限制的,並且對裏面的表數量也沒有限制。github
C.10.2 Limits on Number of Databases and Tables算法
MySQL has no limit on the number of databases. The underlying file
system may have a limit on the number of directories.sqlMySQL has no limit on the number of tables. The underlying file system
may have a limit on the number of files that represent tables.
Individual storage engines may impose engine-specific constraints.
InnoDB permits up to 4 billion tables.數據庫
因此,爲何要分庫呢?express
答案是爲了解決單臺服務器的性能問題,當單臺數據庫服務器沒法支撐當前的數據量時,就須要根據業務邏輯緊密程度把表分紅幾撮,分別放在不一樣的數據庫服務器中以下降單臺服務器的負載。服務器
分庫通常考慮的是垂直切分,除非在垂直切分後,數據量仍然多到單臺服務器沒法負載,才繼續水平切分。函數
好比一個論壇系統的數據庫因當前服務器性能沒法知足須要進行分庫。先垂直切分,按業務邏輯把用戶相關數據表好比用戶信息、積分、用戶間私信等放入user數據庫;論壇相關數據表好比板塊,帖子,回覆等放入forum數據庫,兩個數據庫放在不一樣服務器上。
拆分後表每每不可能徹底無關聯,好比帖子中的發帖人、回覆人這些信息都在user數據庫中。未拆分前可能一次聯表查詢就能獲取當前帖子的回覆、發帖人、回覆人等全部信息,拆分後由於跨數據庫沒法聯表查詢,只能屢次查詢得到最終數據。
因此總結起來,分庫的目的是下降單臺服務器負載,切分原則是根據業務緊密程度拆分,缺點是跨數據庫沒法聯表查詢。
當數據量超大的時候,B-Tree索引就沒法起做用了。除非是索引覆蓋查詢,不然數據庫服務器須要根據索引掃描的結果回表,查詢全部符合條件的記錄,若是數據量巨大,這將產生大量隨機I/O,隨之,數據庫的響應時間將大到不可接受的程度。另外,索引維護(磁盤空間、I/O操做)的代價也很是高。
緣由:
1.根據MySQL索引實現原理及相關優化策略的內容咱們知道Innodb
主索引葉子節點存儲着當前行的全部信息,因此減小字段可以使內存加載更多行數據,有利於查詢。
2.受限於操做系統中的文件大小限制。
切分原則:
把不經常使用或業務邏輯不緊密或存儲內容比較多的字段分到新的表中可以使表存儲更多數據。。
緣由:
1.隨着數據量的增大,table行數巨大,查詢的效率愈來愈低。
2.一樣受限於操做系統中的文件大小限制,數據量不能無限增長,當到達必定容量時,須要水平切分以下降單表(文件)的大小。
切分原則: 增量區間或散列或其餘業務邏輯。
使用哪一種切分方法要根據實際業務邏輯判斷。
好比對錶的訪問可能是近期產生的新數據,歷史數據訪問較少,能夠考慮根據時間增量把數據按照必定時間段(好比每一年)切分。
若是對錶的訪問較均勻,沒有明顯的熱點區域,則能夠考慮用範圍(好比每500w一個表)或普通Hash或一致性Hash來切分。
全局主鍵問題:
本來依賴數據庫生成主鍵(好比自增)的表在拆分後須要本身實現主鍵的生成,由於通常拆分規則是創建在主鍵上的,因此在插入新數據時須要肯定主鍵後才能找到存儲的表。
實際應用中也已經有了比較成熟的方案。好比對於自增列作主鍵的表,flickr
的全局主鍵生成方案很好的解決了性能和單點問題,具體實現原理能夠參考這個帖子。除此以外,還有相似於uuid的全局主鍵生成方案,好比達達參考的Instagram
的ID生成器。
一致性Hash:
使用一致性Hash切分比普通的Hash切分可擴展性更強,能夠實現拆分表的添加和刪除。一致性Hash的具體原理能夠參考這個帖子,若是拆分後的表存儲在不一樣服務器節點上,能夠跟帖子同樣對節點名或ip取Hash;若是拆分後的表存在一個服務器中則可對拆分後的表名取Hash。
上面介紹的傳統的分庫分表都是在應用層實現,拆分後都要對原有系統進行很大的調整以適應新拆分後的庫或表,好比實現一個SQL
中間件、本來的聯表查詢改爲兩次查詢、實現一個全局主鍵生成器等等。
而下面介紹的MySQL
分區表是在數據庫層面,MySQL
本身實現的分表功能,在很大程度上簡化了分表的難度。
對用戶來講,分區表是一個獨立的邏輯表,可是底層由多個物理子表實現。
也就是說,對於原表分區後,對於應用層來講能夠不作變化,咱們無需改變原有的SQL
語句,至關於MySQL
幫咱們實現了傳統分表後的SQL
中間件,固然,MySQL
的分區表的實現要複雜不少。
另外,在建立分區時能夠指定分區的索引文件和數據文件的存儲位置,因此能夠把數據表的數據分佈在不一樣的物理設備上,從而高效地利用多個硬件設備。
一些限制:
1.在5.6.7以前的版本,一個表最多有1024
個分區;從5.6.7開始,一個表最多能夠有8192
個分區。
2.分區表中沒法使用外鍵約束。
3.主表的全部惟一索引列(包括主鍵)都必須包含分區字段。MySQL
官方文檔中寫的是:
All columns used in the partitioning expression for a partitioned
table must be part of every unique key that the table may have.
這句話不是很好理解,須要經過例子才能明白,MySQL
官方文檔也爲此限制特地作了舉例和解釋。
根據範圍分區,範圍應該連續可是不重疊,使用PARTITION BY RANGE
, VALUES LESS THAN
關鍵字。不使用COLUMNS
關鍵字時RANGE
括號內必須爲整數字段名或返回肯定整數的函數。
根據數值範圍:
CREATE TABLE employees ( id INT NOT NULL, fname VARCHAR(30), lname VARCHAR(30), hired DATE NOT NULL DEFAULT '1970-01-01', separated DATE NOT NULL DEFAULT '9999-12-31', job_code INT NOT NULL, store_id INT NOT NULL ) PARTITION BY RANGE (store_id) ( PARTITION p0 VALUES LESS THAN (6), PARTITION p1 VALUES LESS THAN (11), PARTITION p2 VALUES LESS THAN (16), PARTITION p3 VALUES LESS THAN MAXVALUE );
根據TIMESTAMP
範圍:
CREATE TABLE quarterly_report_status ( report_id INT NOT NULL, report_status VARCHAR(20) NOT NULL, report_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) PARTITION BY RANGE ( UNIX_TIMESTAMP(report_updated) ) ( PARTITION p0 VALUES LESS THAN ( UNIX_TIMESTAMP('2008-01-01 00:00:00') ), PARTITION p1 VALUES LESS THAN ( UNIX_TIMESTAMP('2008-04-01 00:00:00') ), PARTITION p2 VALUES LESS THAN ( UNIX_TIMESTAMP('2008-07-01 00:00:00') ), PARTITION p3 VALUES LESS THAN ( UNIX_TIMESTAMP('2008-10-01 00:00:00') ), PARTITION p4 VALUES LESS THAN ( UNIX_TIMESTAMP('2009-01-01 00:00:00') ), PARTITION p5 VALUES LESS THAN ( UNIX_TIMESTAMP('2009-04-01 00:00:00') ), PARTITION p6 VALUES LESS THAN ( UNIX_TIMESTAMP('2009-07-01 00:00:00') ), PARTITION p7 VALUES LESS THAN ( UNIX_TIMESTAMP('2009-10-01 00:00:00') ), PARTITION p8 VALUES LESS THAN ( UNIX_TIMESTAMP('2010-01-01 00:00:00') ), PARTITION p9 VALUES LESS THAN (MAXVALUE) );
添加COLUMNS
關鍵字可定義非integer範圍及多列範圍,不過須要注意COLUMNS
括號內只能是列名,不支持函數;多列範圍時,多列範圍必須呈遞增趨勢:
根據DATE
、DATETIME
範圍:
CREATE TABLE members ( firstname VARCHAR(25) NOT NULL, lastname VARCHAR(25) NOT NULL, username VARCHAR(16) NOT NULL, email VARCHAR(35), joined DATE NOT NULL ) PARTITION BY RANGE COLUMNS(joined) ( PARTITION p0 VALUES LESS THAN ('1960-01-01'), PARTITION p1 VALUES LESS THAN ('1970-01-01'), PARTITION p2 VALUES LESS THAN ('1980-01-01'), PARTITION p3 VALUES LESS THAN ('1990-01-01'), PARTITION p4 VALUES LESS THAN MAXVALUE );
根據多列範圍:
CREATE TABLE rc3 ( a INT, b INT ) PARTITION BY RANGE COLUMNS(a,b) ( PARTITION p0 VALUES LESS THAN (0,10), PARTITION p1 VALUES LESS THAN (10,20), PARTITION p2 VALUES LESS THAN (10,30), PARTITION p3 VALUES LESS THAN (10,35), PARTITION p4 VALUES LESS THAN (20,40), PARTITION p5 VALUES LESS THAN (MAXVALUE,MAXVALUE) );
根據具體數值分區,每一個分區數值不重疊,使用PARTITION BY LIST
、VALUES IN
關鍵字。跟Range
分區相似,不使用COLUMNS
關鍵字時List
括號內必須爲整數字段名或返回肯定整數的函數。
CREATE TABLE employees ( id INT NOT NULL, fname VARCHAR(30), lname VARCHAR(30), hired DATE NOT NULL DEFAULT '1970-01-01', separated DATE NOT NULL DEFAULT '9999-12-31', job_code INT, 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) );
數值必須被全部分區覆蓋,不然插入一個不屬於任何一個分區的數值會報錯。
mysql> CREATE TABLE h2 ( -> c1 INT, -> c2 INT -> ) -> PARTITION BY LIST(c1) ( -> PARTITION p0 VALUES IN (1, 4, 7), -> PARTITION p1 VALUES IN (2, 5, 8) -> ); Query OK, 0 rows affected (0.11 sec) mysql> INSERT INTO h2 VALUES (3, 5); ERROR 1525 (HY000): Table has no partition for value 3
當插入多條數據出錯時,若是表的引擎支持事務(Innodb
),則不會插入任何數據;若是不支持事務,則出錯前的數據會插入,後面的不會執行。
可使用IGNORE
關鍵字忽略出錯的數據,這樣其餘符合條件的數據會所有插入不受影響。
mysql> TRUNCATE h2; Query OK, 1 row affected (0.00 sec) mysql> SELECT * FROM h2; Empty set (0.00 sec) mysql> INSERT IGNORE INTO h2 VALUES (2, 5), (6, 10), (7, 5), (3, 1), (1, 9); Query OK, 3 rows affected (0.00 sec) Records: 5 Duplicates: 2 Warnings: 0 mysql> SELECT * FROM h2; +------+------+ | c1 | c2 | +------+------+ | 7 | 5 | | 1 | 9 | | 2 | 5 | +------+------+ 3 rows in set (0.00 sec)
與Range
分區相同,添加COLUMNS
關鍵字可支持非整數和多列。
Hash
分區主要用來確保數據在預先肯定數目的分區中平均分佈,Hash
括號內只能是整數列或返回肯定整數的函數,實際上就是使用返回的整數對分區數取模。
CREATE TABLE employees ( id INT NOT NULL, fname VARCHAR(30), lname VARCHAR(30), hired DATE NOT NULL DEFAULT '1970-01-01', separated DATE NOT NULL DEFAULT '9999-12-31', job_code INT, store_id INT ) PARTITION BY HASH(store_id) PARTITIONS 4;
CREATE TABLE employees ( id INT NOT NULL, fname VARCHAR(30), lname VARCHAR(30), hired DATE NOT NULL DEFAULT '1970-01-01', separated DATE NOT NULL DEFAULT '9999-12-31', job_code INT, store_id INT ) PARTITION BY HASH( YEAR(hired) ) PARTITIONS 4;
Hash
分區也存在與傳統Hash
分表同樣的問題,可擴展性差。MySQL
也提供了一個相似於一致Hash
的分區方法-線性Hash
分區,只須要在定義分區時添加LINEAR
關鍵字,若是對實現原理感興趣,能夠查看官方文檔。
CREATE TABLE employees ( id INT NOT NULL, fname VARCHAR(30), lname VARCHAR(30), hired DATE NOT NULL DEFAULT '1970-01-01', separated DATE NOT NULL DEFAULT '9999-12-31', job_code INT, store_id INT ) PARTITION BY LINEAR HASH( YEAR(hired) ) PARTITIONS 4;
按照KEY進行分區相似於按照HASH分區,除了HASH分區使用的用戶定義的表達式,而KEY分區的 哈希函數是由MySQL
服務器提供。MySQL 簇(Cluster)使用函數MD5()來實現KEY分區;對於使用其餘存儲引擎的表,服務器使用其本身內部的
哈希函數,這些函數是基於與PASSWORD()同樣的運算法則。
Key
分區與Hash
分區很類似,只是Hash
函數不一樣,定義時把Hash
關鍵字替換成Key
便可,一樣Key
分區也有對應與線性Hash
的線性Key
分區方法。
CREATE TABLE tk ( col1 INT NOT NULL, col2 CHAR(5), col3 DATE ) PARTITION BY LINEAR KEY (col1) PARTITIONS 3;
另外,當表存在主鍵或惟一索引時可省略Key
括號內的列名,Mysql
將按照主鍵-惟一索引的順序選擇,當找不到惟一索引時報錯。
子分區是分區表中每一個分區的再次分割。建立子分區方法:
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 p1 VALUES LESS THAN (2000), PARTITION p2 VALUES LESS THAN MAXVALUE );
和
CREATE TABLE ts (id INT, purchased DATE) PARTITION BY RANGE( YEAR(purchased) ) SUBPARTITION BY HASH( TO_DAYS(purchased) ) ( PARTITION p0 VALUES LESS THAN (1990) ( SUBPARTITION s0 DATA DIRECTORY = '/disk0/data' INDEX DIRECTORY = '/disk0/idx', SUBPARTITION s1 DATA DIRECTORY = '/disk1/data' INDEX DIRECTORY = '/disk1/idx' ), PARTITION p1 VALUES LESS THAN (2000) ( SUBPARTITION s2 DATA DIRECTORY = '/disk2/data' INDEX DIRECTORY = '/disk2/idx', SUBPARTITION s3 DATA DIRECTORY = '/disk3/data' INDEX DIRECTORY = '/disk3/idx' ), PARTITION p2 VALUES LESS THAN MAXVALUE ( SUBPARTITION s4 DATA DIRECTORY = '/disk4/data' INDEX DIRECTORY = '/disk4/idx', SUBPARTITION s5 DATA DIRECTORY = '/disk5/data' INDEX DIRECTORY = '/disk5/idx' ) );
須要注意的是:每一個分區的子分區數必須相同。若是在一個分區表上的任何分區上使用SUBPARTITION
來明肯定義任何子分區,那麼就必須定義全部的子分區,且必須指定一個全表惟一的名字。
對現有表分區的原則與傳統分表同樣。
傳統的按照增量區間分表對應於分區的Range
分區,好比對錶的訪問可能是近期產生的新數據,歷史數據訪問較少,則能夠按必定時間段(好比年或月)或必定數量(好比100萬)對錶分區,具體根據哪一種取決於表索引結構。分區後最後一個分區即爲近期產生的數據,當一段時間事後數據量再次變大,可對最後一個分區從新分區(REORGANIZE PARTITION
)把一段時間(一年或一月)或必定數量(好比100萬)的數據分離出去。
傳統的散列方法分表對應於分區的Hash/Key分區,具體方法上面已經介紹過。
分區的目的是爲了提升查詢效率,若是查詢範圍是全部分區那麼就說明分區沒有起到做用,咱們用explain partitions
命令來查看SQL
對於分區的使用狀況。
通常來講,就是在where
條件中加入分區列。
好比表salaries
結構爲:
mysql> show create table salaries\G; *************************** 1. row *************************** Table: salaries Create Table: CREATE TABLE `salaries` ( `emp_no` int(11) NOT NULL, `salary` int(11) NOT NULL, `from_date` date NOT NULL, `to_date` date NOT NULL, PRIMARY KEY (`emp_no`,`from_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 /*!50100 PARTITION BY RANGE (year(from_date)) (PARTITION p1 VALUES LESS THAN (1985) ENGINE = InnoDB, PARTITION p2 VALUES LESS THAN (1986) ENGINE = InnoDB, PARTITION p3 VALUES LESS THAN (1987) ENGINE = InnoDB, PARTITION p4 VALUES LESS THAN (1988) ENGINE = InnoDB, PARTITION p5 VALUES LESS THAN (1989) ENGINE = InnoDB, PARTITION p6 VALUES LESS THAN (1990) ENGINE = InnoDB, PARTITION p7 VALUES LESS THAN (1991) ENGINE = InnoDB, PARTITION p8 VALUES LESS THAN (1992) ENGINE = InnoDB, PARTITION p9 VALUES LESS THAN (1993) ENGINE = InnoDB, PARTITION p10 VALUES LESS THAN (1994) ENGINE = InnoDB, PARTITION p11 VALUES LESS THAN (1995) ENGINE = InnoDB, PARTITION p12 VALUES LESS THAN (1996) ENGINE = InnoDB, PARTITION p13 VALUES LESS THAN (1997) ENGINE = InnoDB, PARTITION p14 VALUES LESS THAN (1998) ENGINE = InnoDB, PARTITION p15 VALUES LESS THAN (1999) ENGINE = InnoDB, PARTITION p16 VALUES LESS THAN (2000) ENGINE = InnoDB, PARTITION p17 VALUES LESS THAN (2001) ENGINE = InnoDB, PARTITION p18 VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */
則下面的查詢沒有利用分區,由於partitions
中包含了全部的分區:
mysql> explain partitions select * from salaries where salary > 100000\G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: salaries partitions: p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13,p14,p15,p16,p17,p18 type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 2835486 Extra: Using where
只有在where
條件中加入分區列才能起到做用,過濾掉不須要的分區:
mysql> explain partitions select * from salaries where salary > 100000 and from_date > '1998-01-01'\G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: salaries partitions: p15,p16,p17,p18 type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 1152556 Extra: Using where
與普通搜索同樣,在運算符左側使用函數將使分區過濾失效,即便與分區函數想同也同樣:
mysql> explain partitions select * from salaries where salary > 100000 and year(from_date) > 1998\G; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: salaries partitions: p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13,p14,p15,p16,p17,p18 type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 2835486 Extra: Using where
傳統分表後,count
、sum
等統計操做只能對全部切分表進行操做後以後在應用層再次計算得出最後統計數據。而分區表則不受影響,可直接統計。
Queries involving aggregate functions such as SUM() and COUNT() can easily be parallelized. A simple example of such a query might be SELECT salesperson_id, COUNT(orders) as order_total FROM sales GROUP BY salesperson_id;. By 「parallelized,」 we mean that the query can be run simultaneously on each partition, and the final result obtained merely by summing the results obtained for all partitions.
分區對原系統改動最小,分區只涉及數據庫層面,應用層不須要作出改動。
分區有個限制是主表的全部惟一字段(包括主鍵)必須包含分區字段,而分表沒有這個限制。
分表包括垂直切分和水平切分,而分區只能起到水平切分的做用。
博客地址:http://haitian299.github.io/2016/05/26/mysql-partitioning/