最近這幾個月,特別是 TiDB RC1 發佈後,愈來愈多的用戶已經開始測試起來,也有不少朋友已經在生產環境中使用,咱們這邊也陸續的收到了不少用戶的測試和使用反饋。很是感謝各位小夥伴和早期用戶的厚愛,並且看了這麼多場景後,也總結出了一些 TiDB 的使用實踐 (其實 Spanner 的最佳實踐大部分在 TiDB 中也是適用的,MySQL 最佳實踐也是),也是藉着 Google Cloud Spanner 發佈的東風,看了一下 Spanner 官方的一些最佳實踐文檔,寫篇文章講講 TiDB 以及分佈式關係型數據庫的一些正確的使用姿式,固然,時代也在一直髮展,TiDB 也在不停的進化,這篇文章基本上只表明近期的一些觀察。算法
首先談談 Schema 設計的一些比較好的經驗。因爲 TiDB 是一個分佈式的數據庫,可能在表結構設計的時候須要考慮的事情和傳統的單機數據庫不太同樣,須要開發者可以帶着「這個表的數據會分散在不一樣的機器上」這個前提,才能作更好的設計。數據庫
和 Spanner 同樣,TiDB 中的一張表的行(Rows)是按照主鍵的字節序排序的(整數類型的主鍵咱們會使用特定的編碼使其字節序和按大小排序一致),即便在 CREATE TABLE 語句中不顯式的建立主鍵,TiDB 也會分配一個隱式的。
有四點須要記住:網絡
按照字節序的順序掃描的效率是比較高的;架構
連續的行大機率會存儲在同一臺機器的鄰近位置,每次批量的讀取和寫入的效率會高;併發
索引是有序的(主鍵也是一種索引),一行的每一列的索引都會佔用一個 KV Pair,好比,某個表除了主鍵有 3 個索引,那麼在這個表中插入一行,對應在底層存儲就是 4 個 KV Pairs 的寫入:數據行以及 3 個索引行。框架
一行的數據都是存在一個 KV Pair 中,不會被切分,這點和類 BigTable 的列式存儲很不同。分佈式
表的數據在 TiDB 內部會被底層存儲 TiKV 切分紅不少 64M 的 Region(對應 Spanner 的 Splits 的概念),每一個 Region 裏面存儲的都是連續的行,Region 是 TiDB 進行數據調度的單位,隨着一個 Region 的數據量愈來愈大和時間的推移,Region 會分裂/合併,或者移動到集羣中不一樣的物理機上,使得整個集羣可以水平擴展。函數
建議:高併發
儘量批量寫入,可是一次寫入總大小不要超過 Region 的分裂閾值(64M),另外 TiDB 也對單個事務有大小的限制。性能
存儲超寬表是比較不合適的,特別是一行的列很是多,同時不是太稀疏,一個經驗是最好單行的總數據大小不要超過 64K,越小越好。大的數據最好拆到多張表中。
對於高併發且訪問頻繁的數據,儘量一次訪問只命中一個 Region,這個也很好理解,好比一個模糊查詢或者一個沒有索引的表掃描操做,可能會發生在多個物理節點上,一來會有更大的網絡開銷,二來訪問的 Region 越多,遇到 stale region 而後重試的機率也越大(能夠理解爲 TiDB 會常常作 Region 的移動,客戶端的路由信息可能更新不那麼及時),這些可能會影響 .99 延遲;另外一方面,小事務(在一個 Region 的範圍內)的寫入的延遲會更低,TiDB 針對同一個 Region 內的跨行事務是有優化的。另外 TiDB 對經過主鍵精準的點查詢(結果集只有一條)效率更高。
除了使用主鍵查詢外,TiDB 容許用戶建立二級索引以加速訪問,就像上面提到過的,在 TiKV 的層面,TiDB 這邊的表裏面的行數據和索引的數據看起來都是 TiKV 中的 KV Pair,因此不少適用於表數據的原則也適用於索引。和 Spanner 有點不同的是,TiDB 只支持全局索引,也就是 Spanner 中默認的 Non-interleaved indexes。全局索引的好處是對使用者沒有限制,能夠 scale 到任意大小,不過這意味着,索引信息不必定和實際的數據在一個 Region 內。
建議:
對於大海撈針式的查詢來講 (海量數據中精準定位某條或者某幾條),務必經過索引。
固然也不要盲目的建立索引,建立太多索引會影響寫入的性能。
其實 Spanner 的白皮書已經寫得很清楚了,我再贅述一下:
第一種,過分依賴單調遞增的主鍵,AUTO INCREMENT ID
在傳統的關係型數據庫中,開發者常常會依賴自增 ID 來做爲 PRIMARY KEY,可是其實大多數場景你們想要的只是一個不重複的 ID 而已,至因而不是自增其實無所謂,可是這個對於分佈式數據庫來講是不推薦的,隨着插入的壓力增大,會在這張表的尾部 Region 造成熱點,並且這個熱點並無辦法分散到多臺機器。TiDB 在 GA 的版本中會對非自增 ID 主鍵進行優化,讓 insert workload 儘量分散。
建議:
若是業務沒有必要使用單調遞增 ID 做爲主鍵,就別用,使用真正有意義的列做爲主鍵(通常來講,例如:郵箱、用戶名等)
使用隨機的 UUID 或者對單調遞增的 ID 進行 bit-reverse (位反轉)
第二種,單調遞增的索引 (好比時間戳)
不少日誌類型的業務,由於常常須要按照時間的維度查詢,因此很天然須要對 timestamp 建立索引,可是這類索引的問題本質上和單調遞增主鍵是同樣的,由於在 TiDB 的內部實現裏,索引也是一堆連續的 KV Pairs,不斷的插入單調遞增的時間戳會形成索引尾部的 Region 造成熱點,致使寫入的吞吐受到影響。
建議:
由於不可避免的,不少用戶在使用 TiDB 存儲日誌,畢竟 TiDB 的彈性伸縮能力和 MySQL 兼容的查詢特性是很適合這類業務的。另外一方面,若是發現寫入的壓力實在扛不住,可是又很是想用 TiDB 來存儲這種類型的數據,能夠像 Spanner 建議的那樣作 Application 層面的 Sharding,以存儲日誌爲例,原來的可能在 TiDB 上建立一個 log 表,更好的模式是能夠建立多個 log 表,如:log_1, log_2 … log_N,而後業務層插入的時候根據時間戳進行 hash ,隨機分配到 1..N 這幾個分片表中的一個。
相應的,查詢的時候須要將查詢請求分發到各個分片上,最後在業務層彙總結果。
TiDB 的優化分爲基於規則的優化(Rule Based Optimization)和基於代價的優化(Cost Based Optimization), 本質上 TiDB 的 SQL 引擎更像是一個分佈式計算框架,對於大表的數據由於自己 TiDB 會將數據分散到多個存儲節點上,能將查詢邏輯下推,會大大的提高查詢的效率。
TiDB 基於規則的優化有:
謂詞下推
謂詞下推會將 where/on/having 條件推到離數據表儘量近的地方,好比:
select * from t join s on t.id = s.id where t.c1 < 10
能夠被 TiDB 自動改寫成
select * from (select * from t where t.c1 < 10) as t join s on t.id = s.id
關聯子查詢消除
關聯子查詢可能被 TiDB 改寫成 Join,例如:
select * from t where t.id in (select id from s where s.c1 < 10 and s.name = t.name)
能夠被改寫成:
select * from t semi join s on t.id = s.id and s.name = t.name and s.c1 < 10
聚合下推
聚合函數能夠被推過 Join,因此相似帶等值鏈接的 Join 的效率會比較高,例如:
select count(s.id) from t join s on t.id = s.t_id
能夠被改寫成:
select sum(agg0) from t join (select count(id) as agg0, t_id from s group by t_id) as s on t.id = s.t_id
基於規則的優化有時能夠組合以產生意想不到的效果,例如:
select s.c2 from s where 0 = (select count(id) from t where t.s_id = s.id)
在TiDB中,這個語句會先經過關聯子查詢消除的優化,變成:
select s.c2 from s left outer join t on t.s_id = s.id group by s.id where 0 = count(t.id)
而後這個語句會經過聚合下推的優化,變成:
select s.c2 from s left outer join (select count(t.id) as agg0 from t group by t.s_id) t on t.s_id = s.id group by s.id where 0 = sum(agg0)
再通過聚合消除的判斷,語句能夠優化成:
select s.c2 from s left outer join (select count(t.id) as agg0 from t group by t.s_id) t on t.s_id = s.id where 0 = agg0
基於代價的優化有:
讀取表時,若是有多條索引能夠選擇,咱們能夠經過統計信息選擇最優的索引。例如:
select * from t where age = 30 and name in ( ‘小明’, ‘小強’)
對於包含 Join 的操做,咱們能夠區分大小表,TiDB 的對於一個大表和一個小表的 Join 會有特殊的優化。
例如 select * from t join s on s.id = t.id
優化器會經過對錶大小的估計來選擇 Join 的算法:即選擇把較小的表裝入內存中。
對於多種方案,利用動態規劃算法選擇最優者,例如:
(select * from t where c1 < 10) union all (select * from s where c2 < 10) order by c3 limit 10
t 和 s 能夠根據索引的數據分佈來肯定選擇索引 c3 仍是 c2。
總之正確使用 TiDB 的姿式,或者說 TiDB 的典型的應用場景是:
大數據量下,MySQL 複雜查詢很慢;
大數據量下,數據增加很快,接近單機處理的極限,不想分庫分表或者使用數據庫中間件等對業務侵入性較大,架構反過來約束業務的 Sharding 方案;
大數據量下,有高併發實時寫入、實時查詢、實時統計分析的需求;
有分佈式事務、多數據中心的數據 100% 強一致性、auto-failover 的高可用的需求。
若是整篇文章你只想記住一句話,那就是數據條數少於 5000w 的場景下一般用不到 TiDB,TiDB 是爲大規模的數據場景設計的。若是還想記住一句話,那就是單機 MySQL 能知足的場景也用不到 TiDB。