1. 程式人生 > >mysql的分庫分表

mysql的分庫分表

一、為什麼要分表和分割槽?

我們的資料庫資料越來越大,隨之而來的是單個表中資料太多。以至於查詢速度變慢,而且由於表的鎖機制導致應用操作也受到嚴重影響,出現了資料庫效能瓶頸。

mysql中有一種機制是表鎖定和行鎖定,是為了保證資料的完整性。表鎖定表示你們都不能對這張表進行操作,必須等我對錶操作完才行。行鎖定也一樣,別的sql必須等我對這條資料操作完了,才能對這條資料進行操作。當出現這種情況時,我們可以考慮分表或分割槽。

1、分表

什麼是分表?

分表是將一個大表按照一定的規則分解成多張具有獨立儲存空間的實體表,每個表都對應三個檔案,MYD資料檔案,.MYI索引檔案,.frm表結構檔案。這些表可以分佈在同一塊磁碟上,也可以在不同的機器上。app

讀寫的時候根據事先定義好的規則得到對應的表名,然後去操作它。

將單個數據庫表進行拆分,拆分成多個數據表,然後使用者訪問的時候,根據一定的演算法(hash的方式,也可以用求餘(取模)的方式),讓使用者訪問不同的表,這樣資料分散到多個數據表中,減少了單個數據表的訪問壓力。提升了資料庫訪問效能。分表的目的就在於此,減小資料庫的負擔,縮短查詢時間。

Mysql分表分為垂直切分和水平切分

垂直切分是指資料表列的拆分,把一張列比較多的表拆分為多張表

通常我們按以下原則進行垂直拆分:

把不常用的欄位單獨放在一張表;

把text,blob(binary large object,二進位制大物件)等大欄位拆分出來放在附表中;

經常組合查詢的列放在一張表中;

垂直拆分更多時候就應該在資料表設計之初就執行的步驟,然後查詢的時候用jion關鍵起來即可。

水平拆分是指資料錶行的拆分,把一張的表的資料拆成多張表來存放。

水平拆分原則

通常情況下,我們使用hash、取模等方式來進行表的拆分

比如一張有400W的使用者表users,為提高其查詢效率我們把其分成4張表users1,users2,users3,users4

通過用ID取模的方法把資料分散到四張表內Id%4= [0,1,2,3]

然後查詢,更新,刪除也是通過取模的方法來查詢

部分業務邏輯也可以通過地區,年份等欄位來進行歸檔拆分;

進行拆分後的表,這時我們就要約束使用者查詢行為。比如我們是按年來進行拆分的,這個時候在頁面設計上就約束使用者必須要先選擇年,然後才能進行查詢。

分表的幾種方式:

(1)mysql叢集

它並不是分表,但起到了和分表相同的作用。叢集可分擔資料庫的操作次數,將任務分擔到多臺資料庫上。叢集可以讀寫分離,減少讀寫壓力。從而提升資料庫效能。

(2)預先估計會出現大資料量並且訪問頻繁的表,將其分為若干個表

根據一定的演算法(如用hash的方式,也可以用求餘(取模)的方式)讓使用者訪問不同的表。

例如論壇裡面發表帖子的表,時間長了這張表肯定很大,幾十萬,幾百萬都有可能。聊天室裡面資訊表,幾十個人在一起一聊一個晚上,時間長了,這張表的資料肯定很大。像這樣的情況很多。所以這種能預估出來的大資料量表,我們就事先分出個N個表,這個N是多少,根據實際情況而定。以聊天資訊表為例:我們事先建100個這樣的表,message_00,message_01,message_02..........message_98,message_99.然後根據使用者的ID來判斷這個使用者的聊天資訊放到哪張表裡面,可以用hash的方式來獲得,也可以用求餘的方式來獲得,方法很多。

或者可以設計每張表容納的資料量是N條,那麼如何判斷某張表的資料是否容量已滿呢?可以在程式段對於要新增資料的表,在插入前先做統計表記錄數量的操作,當<N條資料,就直接插入,當已經到達閥值,可以在程式段新建立資料庫表(或者已經事先建立好),再執行插入操作)。

(3)利用merge儲存引擎來實現分表

如果要把已有的大資料量表分開比較痛苦,最痛苦的事就是改程式碼,因為程式裡面的sql語句已經寫好了,用merge儲存引擎來實現分表, 這種方法比較適合。

merge分表,分為主表和子表,主表類似於一個殼子,邏輯上封裝了子表,實際上資料都是儲存在子表中的。

我們可以通過主表插入和查詢資料,如果清楚分表規律,也可以直接操作子表。

下面我們來實現一個簡單的利用merge儲存引擎來實現分表的演示:

(1.)建立一個完整表儲存著所有的成員資訊(表名為member)

  1. mysql> create database test;
  2. mysql> use test;
  3. create table member(
  4. id bigint auto_increment primary key,
  5. name varchar(20),
  6. sex tinyint not null default '0'
  7. )engine=myisam default charset=utf8 auto_increment=1;


(2.)加入點測試資料:

  1. mysql> insert into member(name,sex) values('tom1',1);
  2. mysql> insert into member(name,sex) select name,sex from member;

第二條語句多執行幾次就有了很多資料

(3.)下面我們進行分表,這裡我們把member完整表分成兩個子表tb_member1,tb_member2

//建立tb_member2也可以用下面的語句  create table tb_member2 like tb_member1;

(4.)建立主表tb_member


注:INSERT_METHOD,此引數INSERT_METHOD = NO 表示該表不能做任何寫入操作只作為查詢使用,INSERT_METHOD = LAST表示插入到最後的一張表裡面。INSERT_METHOD = first表示插入到第一張表裡面。

檢視一下tb_member表的結構:

mysql> desc tb_member;

注意:檢視子表與主表的欄位定義要一致

(5.)接下來,我們把資料分到兩個子表中去:

  1. mysql> insert into tb_member1(id,name,sex) select id,name,sex from member where id%2=0;
  2. mysql> insert into tb_member2(id,name,sex) select id,name,sex from member where id%2=1;

檢視兩個子表的資料:

檢視一下主表的資料:

測試檢視:


注意:總表只是一個外殼,存取資料發生在一個一個的子表裡面。

注意:每個子表都有自已獨立的相關表文件,而主表只是一個殼,並沒有完整的相關表文件


2、分割槽

什麼是分割槽?

分割槽和分表相似,都是按照規則分解表。不同在於分表將大表分解為若干個獨立的實體表,而分割槽是將資料分段劃分在多個位置存放,分割槽後,表還是一張表,但資料雜湊到多個位置了。app讀寫的時候操作的還是表名字,db自動去組織分割槽的資料。

分割槽主要有兩種形式:

水平分割槽(Horizontal Partitioning) 這種形式分割槽是對錶的行進行分割槽,所有在表中定義的列在每個資料集中都能找到,所以表的特性依然得以保持。

舉個簡單例子:一個包含十年發票記錄的表可以被分割槽為十個不同的分割槽,每個分割槽包含的是其中一年的記錄。

垂直分割槽(Vertical Partitioning)這種分割槽方式一般來說是通過對錶的垂直劃分來減少目標表的寬度,使某些特定的列被劃分到特定的分割槽,每個分割槽都包含了其中的列所對應的行。

舉個簡單例子:一個包含了大text和BLOB列的表,這些text和BLOB列又不經常被訪問,這時候就要把這些不經常使用的text和BLOB了劃分到另一個分割槽,在保證它們資料相關性的同時還能提高訪問速度。

分割槽技術支援

5.6之前,使用這個引數檢視當將配置是否支援分割槽

  1. mysql> SHOW VARIABLES LIKE '%partition%';
  2. +-----------------------+---------------+
  3. |Variable_name | Value |
  4. +-----------------------+---------------+
  5. | have_partition_engine | YES |
  6. +-----------------------+------------------+

如果是yes表示你當前的配置支援分割槽

在5.6及以採用後,則採用如下方式進行檢視

mysql> show plugins;

在顯示結果中,可以看到partition是ACTIVE的,表示支援分割槽

下面我們先演示一個按照範圍(range)方式的表分割槽

(1.)建立range分割槽表
  1. mysql> create database test2;
  2. mysql> use test2;
  3. mysql> create table user (
  4. -> id int ,
  5. -> name varchar(30) ,
  6. -> sex int,
  7. -> primary key(id))default charset=utf8
  8. -> partition by range(id) (
  9. -> partition p0 values less than (3),
  10. -> partition p1 values less than (6),
  11. -> partition p2 values less than (9),
  12. -> partition p3 values less than (12),
  13. -> partition p4 values less than maxvalue
  14. -> );

(2.)插入些資料

到存放資料庫表文件的地方看一下

從information_schema系統庫中的partitions表中檢視分割槽資訊

從某個分割槽中查詢資料

(3.)新增分割槽

mysql> alter table test2.user add partition (partition partionname values less than (n));


(4.)刪除分割槽

當刪除了一個分割槽,也同時刪除了該分割槽中所有的資料。

(5.)分割槽的合併

下面的SQL,將p2 – p4合併為2個分割槽p01 – p02

  1. mysql> alter table test2.user
  2. -> reorganize partition p2,p3,p4 into
  3. -> (partition p01 values less than (8),
  4. -> partition p02 values less than (12)
  5. -> );

3.未分割槽表和分割槽表效能測試

(1.)建立一個未分割槽的表


(2.)建立一個分割槽表,按日期的年份拆分

  1. mysql> CREATE TABLE test2.tab2 ( c1 int, c2 varchar(30) , c3 date )
  2. PARTITION BY RANGE (year(c3)) (PARTITION p0 VALUES LESS THAN (1995),
  3. PARTITION p1 VALUES LESS THAN (1996) , PARTITION p2 VALUES LESS THAN (1997) ,
  4. PARTITION p3 VALUES LESS THAN (1998) , PARTITION p4 VALUES LESS THAN (1999) ,
  5. PARTITION p5 VALUES LESS THAN (2000) , PARTITION p6 VALUES LESS THAN (2001) ,
  6. PARTITION p7 VALUES LESS THAN (2002) , PARTITION p8 VALUES LESS THAN (2003) ,
  7. PARTITION p9 VALUES LESS THAN (2004) , PARTITION p10 VALUES LESS THAN (2010),
  8. PARTITION p11 VALUES LESS THAN MAXVALUE );

注意:最後一行,考慮到可能的最大值

通過儲存過程插入100萬條測試資料

(3.)建立儲存過程:

  1. mysql> delimiter $$ //指定儲存過程結束符
  2. mysql>CREATE PROCEDURE load_part_tab()
  3. begin
  4. declare v int default 0;
  5. while v < 2000000
  6. do
  7. insert into test2.tab1
  8. values (v,'testing partitions',adddate('1995-01-01',(rand(v)*36520) mod 3652));
  9. set v = v + 1;
  10. end while;
  11. end
  12. $$

注意:RAND()函式在0和1之間產生一個隨機數,如果一個整數引數N被指定,它被用作種子值。每個種子產生的隨機數序列是不同的。

(4.)執行儲存過程load_part_tab向test2.tab1表插入資料
  1. mysql> delimiter ;
  2. mysql> call load_part_tab();

(5.)向test2.tab2表中插入資料

mysql> insert into test2.tab2 select * from test2.tab1;

(6.)測試SQL效能


結果表明分割槽表比未分割槽表的執行時間少很多。

(7.)通過explain語句來分析執行情況

explain語句顯示了SQL查詢要處理的記錄數目可以看出分割槽表比未分割槽表的明顯掃描的記錄要少很多。

(8.)建立索引後情況測試

建立索引後分區表比未分割槽表的統計記錄要少很多。(資料量越大差別會明顯些)

二、mysql分割槽的型別

1.RANGE分割槽

基於屬於一個給定連續區間的列值,把多行分配給分割槽。這些區間要連續且不能相互重疊,使用VALUES LESS THAN操作符來進行定義。以下是例項。

  1. CREATE TABLE employees (
  2. id INT NOT NULL,
  3. fname VARCHAR(30),
  4. lname VARCHAR(30),
  5. hired DATE NOT NULL DEFAULT '1970-01-01',
  6. separated DATE NOT NULL DEFAULT '9999-12-31',
  7. job_code INT NOT NULL,
  8. store_id INT NOT NULL
  9. )
  10. partition BY RANGE (store_id) (
  11. partition p0 VALUES LESS THAN (6),
  12. partition p1 VALUES LESS THAN (11),
  13. partition p2 VALUES LESS THAN (16),
  14. partition p3 VALUES LESS THAN (21)
  15. );

按照這種分割槽方案,在商店1到5工作的僱員相對應的所有行被儲存在分割槽P0中,商店6到10的僱員儲存在P1中,依次類推。注意,每個分割槽都是按順序進行定義,從最低到最高。
對於包含資料(72, 'Michael', 'Widenius', '1998-06-25', NULL, 13)的一個新行,可以很容易地確定它將插入到p2分割槽中,但是如果增加了一個編號為第21的商店,將會發生什麼呢?在這種方案下,由於沒有規則把store_id大於20的商店包含在內,伺服器將不知道把該行儲存在何處,將會導致錯誤。要避免這種錯誤,可以建立maxvalue分割槽,所有不在指定範圍內的記錄都會被儲存到maxvalue所在的分割槽中。

mysql> alter table test2.user add partition (partition p4 values less than maxvalue);

2.LIST分割槽

類似於按RANGE分割槽,區別在於LIST分割槽是基於列值匹配一個離散值集合中的某個值來進行選擇。
 LIST分割槽通過使用“PARTITION BY LIST(expr)”來實現,其中“expr” 是某列值或一個基於某個列值、並返回一個整數值的表示式,然後通過“VALUES IN (value_list)”的方式來定義每個分割槽,其中“value_list”是一個通過逗號分隔的整數列表。要按照屬於同一個地區商店的行儲存在同一個分割槽中的方式來分割表,可以使用下面的“CREATE TABLE”語句:

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

這使得在表中增加或刪除指定地區的僱員記錄變得容易起來。例如,假定西區的所有音像店都賣給了其他公司。那麼與在西區音像店工作僱員相關的所有記錄(行)可以使用查詢“ALTER TABLE employees DROP PARTITION pWest;”來進行刪除,它與具有同樣作用的DELETE (刪除)查詢“DELETE query DELETE FROM employees WHERE store_id IN(4,12,13,14,18);”比起來,要有效得多。

要點:如果試圖插入列值不在分割槽值列表中的一行時,那麼“INSERT”查詢將失敗並報錯。例如,假定LIST分割槽的採用上面的方案,下面的插入將失敗:

INSERTINTO employees VALUES(224'Linus''Torvalds''2002-05-01''2004-10-12'4221);

這是因為“store_id”列值21不能在用於定義分割槽pNorth, pEast, pWest,或pCentral的值列表中找到。要重點注意的是,LIST分割槽沒有類似如“VALUES LESS THAN MAXVALUE”這樣的包含其他值在內的定義。將要匹配的任何值都必須在值列表中找到。

3.HASH分割槽

這種模式允許DBA通過對錶的一個或多個列的Hash Key進行計算,最後通過這個Hash碼不同數值對應的資料區域進行分割槽。

hash分割槽的目的是將資料均勻的分佈到預先定義的各個分割槽中,保證各分割槽的資料量大致一致。在RANGE和LIST分割槽中,必須明確指定一個給定的列值或列值集合應該儲存在哪個分割槽中;而在HASH分割槽中,MYSQL自動完成這些工作,使用者所要定一個列值或者表示式,以及指定被分割槽的表將要被分割成的分割槽數量。

mysql> create table t_hash( a int(11), b datetime) partition by hash(year(b)) partitions 4;

hash的分割槽函式頁需要返回一個整數值。partitions子句中的值是一個非負整數,不加的partitions子句的話,預設為分割槽數為1

mysql> insert into t_hash values(1,'2010-04-01');

該記錄會被放入分割槽p2中。因為插2010-04-01進入表t_hash,那麼

MOD(YEAR('2010-04-01'),4)=2
mysql> select * from information_schema.partitions where table_schema='test2' and table_name='t_hash'\G;

***************************1. row ***************************

                TABLE_CATALOG: def

                 TABLE_SCHEMA: test2

                   TABLE_NAME: t_hash

               PARTITION_NAME: p0

            SUBPARTITION_NAME: NULL

   PARTITION_ORDINAL_POSITION: 1

SUBPARTITION_ORDINAL_POSITION:NULL

             PARTITION_METHOD: HASH

          SUBPARTITION_METHOD: NULL

         PARTITION_EXPRESSION: year(b)

      SUBPARTITION_EXPRESSION: NULL

        PARTITION_DESCRIPTION: NULL

                   TABLE_ROWS: 0

               AVG_ROW_LENGTH: 0

                  DATA_LENGTH: 16384

              MAX_DATA_LENGTH: NULL

                 INDEX_LENGTH: 0

                    DATA_FREE: 0

                  CREATE_TIME: 2016-09-1622:48:59

                  UPDATE_TIME: 2016-09-1723:36:22

                   CHECK_TIME: NULL

                     CHECKSUM: NULL

            PARTITION_COMMENT:

                    NODEGROUP: default

              TABLESPACE_NAME: NULL

***************************2. row ***************************

                TABLE_CATALOG: def

                 TABLE_SCHEMA: test2

                   TABLE_NAME: t_hash

               PARTITION_NAME: p1

            SUBPARTITION_NAME: NULL

   PARTITION_ORDINAL_POSITION: 2

SUBPARTITION_ORDINAL_POSITION:NULL

             PARTITION_METHOD: HASH

          SUBPARTITION_METHOD: NULL

         PARTITION_EXPRESSION: year(b)

      SUBPARTITION_EXPRESSION: NULL

        PARTITION_DESCRIPTION: NULL

                   TABLE_ROWS: 0

               AVG_ROW_LENGTH: 0

                  DATA_LENGTH: 16384

              MAX_DATA_LENGTH: NULL

                 INDEX_LENGTH: 0

                    DATA_FREE: 0

                  CREATE_TIME: 2016-09-1622:48:59

                  UPDATE_TIME: 2016-09-1723:36:22

                   CHECK_TIME: NULL

                     CHECKSUM: NULL

            PARTITION_COMMENT:

                    NODEGROUP: default

              TABLESPACE_NAME: NULL

***************************3. row ***************************

                TABLE_CATALOG: def

                 TABLE_SCHEMA: test2

                   TABLE_NAME: t_hash

 PARTITION_NAME: p2

            SUBPARTITION_NAME: NULL

   PARTITION_ORDINAL_POSITION: 3

SUBPARTITION_ORDINAL_POSITION:NULL

             PARTITION_METHOD: HASH

          SUBPARTITION_METHOD: NULL

         PARTITION_EXPRESSION: year(b)

      SUBPARTITION_EXPRESSION: NULL

        PARTITION_DESCRIPTION: NULL

 TABLE_ROWS: 1

               AVG_ROW_LENGTH: 16384

                  DATA_LENGTH: 16384

              MAX_DATA_LENGTH: NULL

                 INDEX_LENGTH: 0

                    DATA_FREE: 0

                  CREATE_TIME: 2016-09-1622:48:59

                  UPDATE_TIME: 2016-09-1723:23:26

                   CHECK_TIME: NULL

                     CHECKSUM: NULL

            PARTITION_COMMENT:

                    NODEGROUP: default

              TABLESPACE_NAME: NULL

***************************4. row ***************************

                TABLE_CATALOG: def

                 TABLE_SCHEMA: test2

                   TABLE_NAME: t_hash

               PARTITION_NAME: p3

            SUBPARTITION_NAME: NULL

   PARTITION_ORDINAL_POSITION: 4

SUBPARTITION_ORDINAL_POSITION:NULL

             PARTITION_METHOD: HASH

          SUBPARTITION_METHOD: NULL

         PARTITION_EXPRESSION: year(b)

      SUBPARTITION_EXPRESSION: NULL

        PARTITION_DESCRIPTION: NULL

                   TABLE_ROWS: 0

               AVG_ROW_LENGTH: 0

                  DATA_LENGTH: 16384

              MAX_DATA_LENGTH: NULL

                 INDEX_LENGTH: 0

                    DATA_FREE: 0

                  CREATE_TIME: 2016-09-16 22:48:59

                  UPDATE_TIME: 2016-09-1723:23:26

                   CHECK_TIME: NULL

                     CHECKSUM: NULL

            PARTITION_COMMENT:

                    NODEGROUP: default

              TABLESPACE_NAME: NULL

4 rows in set(0.00 sec)

可以看到P2分割槽有一條記錄。當前這個例子並不能把資料均勻的分佈到各個分割槽,因為按照YEAR函式進行的,該值本身是離散的。如果對連續的值進行HASH分割槽,如自增長的主鍵,則可以較好地將資料平均分佈。

請思考:

mysql>insert into t_hash values(1,'2012-04-01');

記錄會插入哪個分割槽?

4.key分割槽

key分割槽和hash分割槽相似,不同在於hash分割槽是使用者自定義函式進行分割槽,key分割槽使用mysql資料庫提供的函式進行分割槽,NDB cluster使用MD5函式來分割槽,對於其他儲存引擎mysql使用內部的hash函式。

mysql> create table t_key( a int(11), b datetime) partition by key(b)partitions 4;

上面的RANGE、LIST、HASH、KEY四種分割槽中,分割槽的條件必須是整形,如果不是整形需要通過函式將其轉換為整形。

5.columns分割槽

mysql-5.5開始支援COLUMNS分割槽,可視為RANGE和LIST分割槽的進化,COLUMNS分割槽可以直接使用非整形資料進行分割槽。COLUMNS分割槽支援以下資料型別:
  1. 所有整形,如INT SMALLINT TINYINT BIGINTFLOATDECIMAL則不支援。
  2.   日期型別,如DATEDATETIME。其餘日期型別不支援。
  3.   字串型別,如CHARVARCHARBINARYVARBINARYBLOBTEXT型別不支援。
  4.   COLUMNS可以使用多個列進行分割槽。

三、mysql分表和分割槽有什麼區別呢

1、實現方式上

(a)  mysql的分表是真正的分表,一張表分成很多表後,每一個小表都是完整的一張表,都對應三個檔案,一個.MYD資料檔案,.MYI索引檔案,.frm表結構檔案。

(b)  分割槽不一樣,一張大表進行分割槽後,他還是一張表,不會變成二張表,但是他存放資料的