分區表帶來的性能提升
我們先基于下面的SQL和存儲過程創建一張分區表,并插入1億條記錄:
DROP TABLE if exists employees_partition;
CREATE TABLE if not exists `employees_partition` (
`id` int(11) NOT NULL ,
`name` varchar(32) DEFAULT NULL COMMENT '員工姓名',
`job_no` varchar(16) NOT NULL COMMENT '員工工號',
UNIQUE key(job_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
PARTITION BY KEY (job_no) PARTITIONS 32 ;
-- 創建存儲過程
DROP PROCEDURE IF EXISTS insertemployees;
DELIMITER $$
CREATE PROCEDURE insertemployees()
BEGIN
DECLARE i INT;
SET i=1;
WHILE(i<=100000000) DO
insert into employees_partition values(i, CONCAT(i, '-NAME'), CONCAT('NO.', i));
SET i=i+1;
END WHILE;
END;
$$
DELIMITER ;
-- 調用存儲過程
call insertemployees();
數據插入完成后,還給name
列加上索引。
接下來分別嘗試有分片鍵查詢,二級索引(idx_name)查詢,無分片鍵查詢這三種非常典型查詢,并查看執行計劃(并且為了防止查詢結果被緩存,每條SQL都加上SQL_NO_CACHE):
- 有分片鍵查詢
由下圖可知,有分片鍵查詢的性能簡直狂拽吊炸天。而且我們看查詢計劃,能夠選定特定分區p24,并且索引類型也是最優秀的const:
- 二級索引查詢
由下圖可知,二級索引查詢查詢性能也相當不錯,但是條件沒有分片鍵,所以無法選擇特定分區,其查詢計劃顯示的目標分區是p0~p31所有32個分區:
說明:二級索引查詢具體查詢性能與索引列的可選性有很大的關系,由于筆者構造的索引列的可選性為1,所以查詢性能很好。如果是一個狀態列,1億條數據不同的值只有不到10個,那查詢性能就要差很多了,不止是分區表,普通表也是如此。
- 無分片鍵查詢
由下圖可知,條件中既沒有分片鍵,也沒有普通索引,這時候查詢性能就很差了,查詢耗時近39秒,無法用到任何索引,而且目標分區是所有32個分區:
說明:事實上不止分區表,就是普通表,這種查詢性能也是極差的,因為需要全表掃描。
筆者基于另一張沒有分區,且數據總量也是1億的表,執行條件不會走索引的SQL,耗時也是令人震驚的30s+:
mysql> select SQL_NO_CACHE * from employees_nopartition where `id`='8989898';
+---------+--------------+------------+
| id | name | job_no |
+---------+--------------+------------+
| 8989898 | 8989898-NAME | NO.8989898 |
+---------+--------------+------------+
1 row in set, 1 warning (30.78 sec)
mysql> show create table employees_nopartition\G
*************************** 1. row ***************************
Table: employees_nopartition
Create Table: CREATE TABLE `employees_nopartition` (
`id` int(11) NOT NULL,
`name` varchar(32) DEFAULT NULL COMMENT '員工姓名',
`job_no` varchar(16) NOT NULL COMMENT '員工工號',
UNIQUE KEY `job_no` (`job_no`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
- 總結
對于分區表,如果查詢條件能夠避免雷區,即不會有全表掃描查詢,或者低效索引查詢(這些條件在分庫分表上性能也很差)。所有SQL的條件要么有分片鍵,要么有高效的索引,那么都性能提升是很明顯的。
分區表對性能提升如此明顯,為什么還是有那么多拒絕分區表的聲音,或者說一線互聯網公司還是以分庫分表為主?筆者在以前的文章《分庫分表技術演進暨最佳實踐》中也列舉了若干知名互聯網公司的分庫分表中間件,例如阿里的tddl、cobar,美團的zebra,360的atlas,開源社區的sharding-sphere,mycat等。這是因為分區表本身有諸多的限制,這些公司結合自己的業務特點,分區表完全不能滿足自己的需求!
分區表的限制
看上去帥氣的分區表,MySQL官方列舉了好多好多的限制,如下所示:
- 分區最大數
對于沒有使用NDB存儲引擎的表來說,分區最大數限制為8192,這個數量包含了子分區數量。
- 不支持查詢緩存
對于分區表來說,查詢緩存是不支持的,涉及分區表的查詢會自動關閉查詢緩存,且不能開啟。
- InnoDB分區表不支持外鍵
InnoDB存儲引擎的分區表不支持外鍵。
- 全文索引
即使分區表是InnoDB或者MyISAM存儲引擎,全文索引也不被支持。例如執行如下SQL會報錯: [Err] 1214 - The used table type doesn't support FULLTEXT indexes:
DROP TABLE if exists employees_partition;
CREATE TABLE if not exists `employees_partition` (
`id` int(11) NOT NULL ,
`uname` varchar(32) DEFAULT NULL COMMENT '員工姓名',
`job_no` varchar(16) NOT NULL COMMENT '員工工號',
FULLTEXT (`uname`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
PARTITION BY KEY(uname) PARTITIONS 32 ;
但是去掉PARTITION BY KEY(uname) PARTITIONS 32 ;則OK。
- 空間列
一些POINT或者GEOMETRY這樣的空間數據類型列,不能被用在分區表中。例如在分區表中定義一個名為geo的空間類型列:geo GEOMETRY; 或者geo POINT; 會報錯:[Err] 1178 - The storage engine for the table doesn't support GEOMETRY。如果不是分區表,則能成功創建該表。
- 臨時/日志表
臨時表和日志表都不能被分區。對日志表中執行 ALTER TABLE ... PARTITION BY ...會報錯。
- 算術&邏輯運算符
分區表達式中可以使用+, -, * 這些運算符。但是位運算符例如|, &, ^, <<, >>, 和 ~ 是不支持的。例如PARTITION BY HASH(id+1) PARTITIONS 32 是支持的,但是PARTITION BY HASH(id<<1) PARTITIONS 32 則不支持。此外,分區表達式還有很多的內置函數不支持,分區表達式支持的內置函數可參考:https://dev.mysql.com/doc/refman/5.7/en/partitioning-limitations-functions.html
- 分區鍵數據類型
分區鍵必須要么是整型列,要么是整型列表達式。ENUM枚舉類型的列不能被作為分區表達式。但是,這個限制有兩個特殊情況:
- [LINEAR] KEY分區方式,只要不是TEXT或者BLOB類型,其他任何類型列都可以作為分區鍵。因為MySQL內部的hash算法能夠正確處理這些類型。PARTITION BY KEY(uname) PARTITIONS 32 是可以的,PARTITION BY HASH(uname) PARTITIONS 32 則不行。
- RANGE COLUMNS 或者 LIST COLUMNS 分區方式,可以使用string,DATE和DATETIME類型作為分區列,例如下面的SQL什么是有效的:
CREATE TABLE rc (c1 INT, c2 DATE)
PARTITION BY RANGE COLUMNS(c2) (
PARTITION p0 VALUES LESS THAN('1990-01-01'),
PARTITION p1 VALUES LESS THAN('1995-01-01'),
PARTITION p2 VALUES LESS THAN('2000-01-01'),
PARTITION p3 VALUES LESS THAN('2005-01-01'),
PARTITION p4 VALUES LESS THAN(MAXVALUE)
);
CREATE TABLE lc (c1 INT, c2 CHAR(1))
PARTITION BY LIST COLUMNS(c2) (
PARTITION p0 VALUES IN('a', 'd', 'g', 'j', 'm', 'p', 's', 'v', 'y'),
PARTITION p1 VALUES IN('b', 'e', 'h', 'k', 'n', 'q', 't', 'w', 'z'),
PARTITION p2 VALUES IN('c', 'f', 'i', 'l', 'o', 'r', 'u', 'x', NULL)
);
- Window系統不支持DATA DIRECTORY和INDEX DIRECTORY
我們都知道創建分區表時,可以為每個分區指定 DATA DIRECTORY 和 INDEX DIRECTORY。但是Window系統或者MyISAM的子分區是不支持該語法的。
- 單數據庫實例&服務器資源
分區表歸根結底是在一個數據庫實例上。那么它就會受到單數據庫實例的連接數限制、 IO瓶頸、 swap空間、 FD等諸多限制。
分區PK.分庫分表
看到這么多的限制,不要慌張。畢竟任何一項都有優缺點,沒有銀彈。我們先對分區表一些我認為完全可以接受的限制做一個說明。
- 分區最大數
8192個分區數限制,雖然不像分庫分表可以無限制擴容下去,但是即使按照單表千萬的行業標準,也能妥妥的容納幾百億的的數據。除了淘寶訂單,頭條評論這種海量數據,我相信99%的業務場景是遠遠達不到這個上限的。
- 全文索引&InnoDB分區表不支持外鍵
現在應該沒有對大表加外鍵的操作了吧?也基本上沒有業務場景需要用到數據庫的全文索引吧?有也是瞎搞,不接受反駁。
- 空間列&臨時表&日志表
用這些功能的就更少了,不接受反駁。
- Window系統不支持DATA DIRECTORY和INDEX DIRECTORY
用Window作為生產環境服務器的也是極少數,不接受反駁。
- 不支持查詢緩存
這個好像也沒啥用,如果真的查詢頻率很高,為什么不用Redis或者memcache呢?
- 分區鍵限制
仔細看看分區鍵,以及分區鍵表達式限制,也就那么回事。一些常用的比如選擇整型列例如用戶ID作為分區鍵,選擇字符串類型列例如訂單號作為分區鍵,選擇日期時間作為分區鍵也都是支持的。所以,那些限制只在極端業務場景才會碰到。
接下來是一些確實有影響的限制。我們在分區表、單庫分表和分庫分表三種方案之間進行對比如下(需要說明的是分庫分表包括單庫分表和分庫分表):
P.K. | 分區表 | 單庫分表 | 分庫分表 |
---|---|---|---|
連接數 | 單庫限制 | 單庫限制 | 無限制 |
存儲能力 | 8192個分區 | 單庫限制 | 無限制 |
不走分片鍵 | 全表鎖 | 自研or中間件 | 自研or中間件 |
走分片鍵 | 性能高 | 性能高 | 性能高 |
并發能力 | 一般 | 一般 | 高 |
運維成本 | 低 | 高 | 很高 |
開發成本 | 低 | 高 | 很高 |
事務 | 本地事務 | 本地事務+分布式事務 | 本地事務+分布式事務 |
通過分區表、單庫分表和分庫分表三種方案的對比我們發現,單庫分表相比分區表完全沒有任何優勢,它們都會受到單個數據庫實例引發的連接數、存儲能力、并發能力等的限制。單庫分表相對于分區表甚至還會引入一些不必要的麻煩,例如跨分片鍵的操作,即使這種操作頻率很低,但是只要有需求就需要自研或者引入第三方中間件,從而大大增加開發成本和維護成本。而分區表應對這類操作則不需要任何代價,甚至還可以通過引入一個從庫給這些系統使用從而防止對核心主庫的影響。
分區表和單庫分表的并發能力有限,很多寶貴的資源都受到單個實例和服務器的限制,這才是一線互聯網公司核心數據不使用分區表的主要原因。例如美團外賣訂單表,淘寶訂單表等,這些業務都有相同的特點:高并發、海量數據,所以只能選分庫分表。所以那些高并發,海量數據場景下才會碰到的問題,例如冷熱數據分離,數據歸檔,擴容等,就不在PK范圍之內了。
但是為什么我還是要為分區表正名呢?因為滿足高并發、海量數據的大表畢竟是小數公司。很多公司的很多業務表,雖然整個生命周期內也會有幾億,甚至上十億,但是并不會有高并發的可能,這種業務表就非常適合分區表!畢竟分區表能夠滿足我們需求的情況下,它的開發成本和維護成本要比分庫分表小很多呀!
分區總結
MySQL的分區發展這么多年,從來沒見過官方有要將其拋棄的想法。這是因為,在很多特定業務場景下,它的便捷性和對性能的提升是顯而易見的。廝大大說過:沒有蹩腳的中間件,只有蹩腳的程序員!我對分區的評價則是:存在即合理!
如果你的業務滿足如下的特點,可以大膽嘗試使用分區表:
- 可預估生命周期內數據量在十億量級,而不是百億甚至千億的海量數據;
- 不會有高并發的可能,即你的用戶是有一定局限性的,而不會成為全民爆款;
筆者就碰到很多業務非常適合使用分區表,這類大表生命周期內的上限是絕對可以預估在10億量級以下的,即使這些表將來超過10億,那起碼也是若干年以后的事情。一個方案能抗3~5年那絕對是一個優秀的方案,如果能抗10年,那對于現階段來說,絕對是一個完美的方案了。
筆者對一個重構為分區表的業務表估算如下:
目前存量數據2kw,日增長4w,即年增長約1500w。
分區表設定128個分區,每個分區表約定上限1kw,那么總計可存儲128kw數據(12.8億數據)。
- 如果年復合增長10%,可以確保業務運行24年;
- 如果年復合增長20%,可以確保業務運行16年;
- 如果年復合增長50%,可以確保業務運行10年;
- 如果年復合增長100%,可以確保業務運行7年;
- 如果年復合增長200%,可以確保業務運行5年;
所以,分區表在特定業務場景下,絕對是一個既省時又省力,還能減少以后維護成本的絕佳方案。了解每個技術的優缺點,然后以最小的代價,解決業務的痛點。而不是看著網上一些文章,自己沒有經過任何求證,就否定一門技術。說不定在你對她轉身離去的時候,你錯過了很美麗的風景!