1. 程式人生 > 實用技巧 >MySQL高階特性一:分割槽表

MySQL高階特性一:分割槽表

置頂2019-01-22 17:31:2621827收藏15 分類專欄:MySQL文章標籤:MySQL高階特性分割槽表 版權

對使用者來說,分割槽表時一個獨立的羅技表,但是底層由多個無力字表組成。實現分割槽的程式碼實際上是對一組底層表的控制代碼物件的封裝。對分割槽表的請求,都會通過控制代碼物件轉化成對儲存引擎的介面呼叫。所以分割槽對於SQL層來說是一個完全封裝底層實現的黑盒子,對應用是透明的,但是從底層的檔案系統來看就很容易發現,每一個分割槽表都有一個使用#分隔明明的表文件。

MySQL實現分割槽表的方式;對底層表的封裝,意味著索引也是按照分割槽的子表定義的,而沒有全域性索引。這和Oracle不同,在Oracle中可以更加靈活的定義索引和表是否進行分割槽。

MySQL在建立表時使用partition by 子句定義每個分割槽存放的資料。在執行查詢的時候,優化器會根據分割槽定義過濾哪些沒有我們需要資料的分割槽,這樣查詢就無需掃描所有分割槽,只需要查詢包含需要資料的分割槽就可以了。

在下面的場景中,分割槽可以起到非常大的作用:

① 表非常大以至於無法全部都放在記憶體中,或者只是在表的最後部分有熱點資料,其他均是歷史資料。

② 分割槽表的資料更容易維護。例如,想批量刪除大量資料可以使用清楚整個分割槽的方式。另外,還可以對一個獨立分割槽進行優化、檢查、修復等操作。

③ 分割槽表的資料可以分佈在不同的無力裝置上,從而高效的利用多個硬體裝置。

④ 可以使用分割槽表來避免某些特殊的瓶頸,例如InnoDB的單個索引的互斥訪問、ext3檔案系統的inode鎖競爭等、

⑤ 如果需要,還可以備份和恢復獨立的分割槽,這在非常大的資料集的場景下效果非常好。

MySQL的分割槽實現非常複雜,這裡不打算介紹實現的全部細節,主要專注於分割槽效能方面,如果想了解更多關於分割槽的基礎知識,建議閱讀MySQL光放手冊中關於分割槽一節,其中介紹了很多分割槽的相關基礎知識。另外可以閱讀create table、show create table、alter table和information_schema.partitions、explain關於分割槽部分的介紹。分割槽特性使得create table和alter table命令更復雜。

分割槽表本身也有一些限制,下面是其中比較重要的幾點:

① 一個表最多隻能有1024個分割槽。

② 在MySQL5.1中,分割槽表示式必須是整數,或者是返回整數的表示式。在MySQL5.5中,某些場景可以直接使用列來進行分割槽

③ 如果分割槽欄位中有主見或者唯一索引的列,那麼所有主見列和唯一索引列都必須包含進來。

④ 分割槽表中無法使用外來鍵約束。

1 分割槽表的原理

如前所述,分割槽表由多個相關的底層表實現,這些底層表也是由控制代碼物件表示,所以我們也可以直接訪問各個分割槽。儲存引擎管理分割槽的各個底層表和管理普通表一樣,所有的底層表都必須使用相同的儲存引擎,分割槽表的索引只是在各個底層表上各自加上一個完全相同的索引。從儲存引擎的角度來看,底層表和一個普通表沒有任何不同,儲存引擎也無需知道這是一個普通表還是一個分割槽表的一部分。

分割槽表上的操作按照下面的操作邏輯進行:

① select

當查詢一個分割槽表的時候,分割槽層先開啟並鎖定住所有的底層表,優化器先判斷是否可以過濾部分分割槽,然後在呼叫對應的儲存引擎介面訪問各個分割槽的資料。

② insert

當寫入一條記錄時,分割槽層先開啟並鎖住所有的底層表,然後確定那個分割槽接收者條記錄,在將記錄寫入對應底層表。

③ delete

當刪除一條記錄時,分割槽層先開啟並鎖住所有的底層表,然後確定資料對應的分割槽,最後對響應底層表進行刪除操作。

④ update

當更新一條記錄時,分割槽層先開啟並鎖住所有的底層表,MySQL先確定需要更新的記錄在哪個分割槽,然後取出資料並更新,在判斷更新後的資料應該放在哪個分割槽,最後對底層表進行寫入操作,並對原資料所在的底層表進行刪除操作。

有些操作時支援過濾的。例如,當刪除一條記錄時,MySQL需要先找到這條記錄,如果where條件恰好和分割槽表示式匹配,就可以將所有不包含這條記錄的分割槽都過濾掉。這對update語句同樣有效。如果是insert操作,則本身就是隻命中一個分割槽,其他分割槽都會被過濾掉。MySQL先確定這條記錄屬於哪個分割槽,在將記錄寫入對應的底層分割槽表,無需對任何其他分割槽進行操作。

雖然每個操作都會先開啟並鎖住所有的底層表,但這並不是說分割槽表在處理過程中是鎖住全表的。如果儲存引擎能夠自己實現行級鎖,如InnoDB,則會在分割槽層釋放對應的表鎖。這個加鎖和解鎖過程與InnoDB上的查詢類似。

2 分割槽表的型別

MySQL支援多種分割槽表。我們看到最多的是根據範圍進行分割槽,每個分割槽儲存落在某個範圍的記錄,分割槽表示式可以是列,也可以是包含列的表示式。例如下標可以將每年的資料放在不同的分割槽裡:

  1. create table sales(
  2. order_date DATETIME not null,
  3. -- Other columns omitted
  4. )ENGINE=InnoDB PARTITION BY RANGE(YEAR(order_date))(
  5. PARTITION P_2017 VALUES LESS THAN(2017),
  6. PARTITION P_2018 VALUES LESS THAN(2018),
  7. PARTITION P_2019 VALUES LESS THAN(2019),
  8. PARTITION P_catchall VALUES LESS THAN MAXVALUE
  9. );

partition分割槽子句中可以使用各種函式,但是表示式返回的值要是一個確定的整數,且不能是一個常數。

根據時間間隔進行分割槽,是一種很常見的分割槽方式。

MySQL還支援鍵值、雜湊和列表分割槽,這其中有些還支援子分割槽,不過在生產環境中很少見到。在MySQL5.5中還可以使用range columns型別的分割槽,這樣即使是基於時間的分割槽也無需在將其轉化成一個整數。

在上面的例子中,如果近一年的資料被非常頻繁的訪問,這會導致大量的互斥量的競爭。使用雜湊子分割槽可以將資料切成多個小片,大大降低互斥量的競爭問題。

一些其他的分割槽技術:

① 根據鍵值進行分割槽,來減少InnoDB的互斥量競爭。

② 使用數學模函式來進行分割槽,然後將資料輪訓放入不同的分割槽。例如,可以對日期做模7的運算,或者更簡單的使用返回周幾的函式,如果只想保留最近幾天的資料,這樣分割槽很方便。

③ 假設表有一個自增的主鍵列id,希望根據時間將最近的熱點資料集中存放。那麼必須將時間戳包含在主鍵當中才行,而這和主鍵本身的意義想矛盾。這種情況下也可以使用這樣的分割槽表示式來實現相同的目的:HASH(id DIV 1000000),這將為100W資料建立一個分割槽。這樣一方面實現了當初的分割槽目的,另一方面比起使用時間範圍分割槽還避免了一個超過一定閾值時新建分割槽的問題。

3 如何使用分割槽表

假設我們希望從一個非常大的表中查詢出一段時間的記錄,而這個表中包含了很多年的歷史資料,資料是按照時間怕徐的,例如,希望查詢最近幾個月的資料,這大約有10億條,假設使用的是2012年的硬體裝置,而原表中有10TB的資料,這個資料量原大於記憶體,並且使用的是傳統硬碟,不是山村。你打算如何查詢這個表?如何才能更高效?

首先很肯定:因為資料量巨大,肯定不能在每次查詢的時候都掃描全表。考慮到索引在空間和維護上的消耗,也不希望使用索引,即使真的使用索引,你會發現資料並不是按照想要的方式狙擊的,而且會有大量的碎片產生,最終導致一個查詢產生上千萬的隨機I/O,應用程式也隨之僵死。情況好一點的時候,也許可以通過一兩個索引解決一些問題。不過多數情況下,索引不會有任何作用。這時候只有兩條路可選:讓所有的查詢都只在資料表上做順序掃描,或者將資料表和索引全部都快取在記憶體中。

這裡需要在成熟一遍:在資料量超大的時候,B-Tree索引就無法起作用了。除非是索引覆蓋查詢,否則資料庫伺服器需要根據索引掃描的結果回表,查詢所有符合條件的記錄,如果資料量巨大,這將產生大量隨機I/O,隨之,資料庫的響應時間將大道不可接受的成都。另外,索引維護、磁碟空間、I/O操作的代價也非常高。有些系統如Infobright,意識到這一點於是就完全放棄使用B-Tree索引,而選擇了一些更粗粒度的但消耗更少的方式檢索資料,例如在大量資料上只索引對應的一小塊原資料。

這正是分割槽要做的事情。理解分割槽時還可以將其當做索引的最初形態,以代價非常小的方式定位到需要的資料在那一片區域。在這片區域中,你可以做順序掃描,可以鍵索引,還可以將資料都快取到記憶體等等。因為分割槽無需額外的資料結構記錄每個分割槽有哪些資料,分割槽不需要精確定位每條資料的位置,也就無需額外的資料結構,所以代價非常低。只需要一個簡單的表示式就可以表達每個分割槽存放的是什麼資料。

為了保證大資料量的可擴充套件性,一般有下面兩個策略:

① 全量掃描資料,不要任何索引:

可以使用簡單的分割槽方式存放表,不要任何索引,根據分割槽的規則大致定位需要的資料位置。只要能夠使用where條件,將需要的資料限制在少數分割槽中,則效率是很高的。當然,也需要做一些簡單的運算保證查詢的響應時間能夠滿足需求。使用該策略假設不用講資料完全放入到記憶體中,同時還假設需要的資料全都在磁碟上,因為記憶體相對很小,資料很快會被擠出記憶體,所以快取不起作用。這個策略適合於以政策的方式訪問大量資料的時候。

② 索引資料,並分離熱點

如果資料有明顯的熱點,而且除了這部分資料,其他資料很少被訪問到,那麼可以將這部分熱點資料單獨放在一個分割槽中,讓這個分割槽的資料能夠有機會都快取在記憶體中。這樣查詢就可以只訪問一個很小的分割槽表,能夠使用索引,也能夠有效的使用快取。

僅僅知道這些還不夠,MySQL的分割槽表實現還有很多坑,下面看看如何避免。

4 什麼情況下回出問題

① NULL值會使分割槽過濾無效

關於分割槽表一個容易讓人誤解的地方就是分割槽的表示式的值可以是NULL:第一個分割槽時一個特殊分割槽。假設按照partition by range year(order_date)分割槽,那麼所有order_date為NULL或者是一個非法值的時候,記錄都會被存放到第一個分割槽。現在假設有一下查詢

select * from sales where order_date between '2017-01-01' and '2018-01-31'

實際上,MySQL會檢查兩個分割槽。價差第一個分割槽時因為year()函式在接收非法值的時候可能會返回NULL值,那麼這個範圍的值可能會返回NULL而被存放到第一個分割槽了。這一點對於其他很多函式例如to_days()也一樣。

如果第一個分割槽非常大,特別是當使用全量掃描資料,不要任何索引的策略時,代價會非常大。而且掃描兩個分割槽來查詢列也不是我們使用分割槽表的初衷。為了避免這種情況,可以建立一個無用的第一個分割槽,例如,上面的例子可以使用paritition p_nulls values less than(0)來建立第一個分割槽。如果插入表中的資料都是有效的,那麼第一個分割槽就是空的,這樣即使需要檢測第一個分割槽,代價也會非常小。

在MySQL5.5中就不需要這個優化技巧了,因為可以直接使用列本身而不是基於列的函式進行分割槽:pratition by range columns(order_date)。所以這個案例最好的解決方法就是直接使用列來進行分割槽。

② 分割槽列和索引列不匹配

如果定義的索引列和分割槽列不匹配,會導致查詢無法進行分割槽過濾。假設在a上定義了索引,而在列b上進行分割槽。因為每個分割槽都有其獨立的索引,所以掃描列b上的索引就需要掃描每一個分割槽內對應的索引。如果每個分割槽內對應索引的非葉子節點都在記憶體中,那麼掃描的速度還可以接受,但如果能跳過某些分割槽索引當然會更好。要避免這個問題,應該避免簡歷和分割槽列不匹配的索引,除非查詢中還同時包含了可以過濾分割槽的條件。

聽起來避免這個問題很簡單,不過有時候也會遇到一些意向不到的問題。例如,在一個關聯查詢中,分割槽表在關聯順序中是第二個表,並且關聯使用的索引和分割槽條件並不匹配。那麼關聯時指標對第一個表符合條件的每一行,都需要訪問並搜尋第二個表的所有分割槽。

③ 選擇分割槽的成本可能很高

如前所述分割槽有很多型別,不同型別分割槽的實現方式也不同,所以他們的效能也各不相同。尤其是範圍分割槽,對於回答這一行屬於哪個分割槽、這些符合查詢條件的行在哪些分割槽這樣的問題的成本可能會非常高,因為伺服器需要掃描所有的分割槽定義的列表來找到正確的答案。類似這樣的線性搜尋的效率不高,所以隨著分割槽數的曾昭,成本會越來越高。

我們所實際碰到的最糟糕的一次問題是按行寫入大量資料的時候。沒寫入一行資料到範圍分割槽的表時,都需要掃描分割槽定義列表來找到合適的目標分割槽。可以通過限制分割槽的數量來緩解此問題,根據實踐驚險,對大多數系統來說,100個左右的分割槽時沒有問題的。其他的分割槽型別,比如鍵分割槽和雜湊分割槽,則沒有這樣的問題。

④ 開啟並鎖住所有底層表的成本可能很高

當查詢訪問分割槽表的時候,MySQL需要開啟並鎖住所有的底層表,這是分割槽表的另一個開銷。這個操作在分割槽過濾之前發生,所以無法通過分割槽過濾降低此開銷,並且該開銷也和分割槽型別無關,會影響所有的查詢。這一點對一些本身操作非常快的查詢,比如根據主鍵朝趙 單行,會帶來明顯的額外開銷。可以用批量操作的方式來降低單個操作的此類開銷,例如使用批量插入或者load data infile、一次刪除多行資料,等等。當然同時還是需要限制分割槽的個數。

⑤ 維護分割槽的成本可能很高

某些分割槽維護操作的速度會非常快,例如新增或者刪除分割槽。而有些操作,例如充足分割槽或類似alter語句的操作;這類操作需要賦值資料。重組分割槽的原理與alter類似,先建立一個臨時的分割槽,然後將資料賦值到其中,然後在刪除原分割槽。

下面是目前分割槽實現中的一些其他限制:

① 所有分割槽都必須使用相同的儲存引擎。

② 分割槽函式中可以使用的函式和表示式也有一些限制。

③ 某些儲存引擎不支援分割槽。

④ 對於MyISAM的分割槽表,不能在使用load index into cache操作。

⑤ 對於MyISAM表,使用分割槽表時需要開啟更多的檔案描述符。雖然看其阿里是一個表,實際上背後有很多獨立的分割槽,每一個分割槽對於儲存引擎來說都是一個獨立的表。這樣即使分割槽表只佔用一個表快取條目,檔案描述符還是需要多個。因此,即使已經配置了合適的表快取,以確保不會超過作業系統的單個程序可以開啟的檔案描述符的個數,但對於分割槽表而言 ,還是會出現超過檔案描述符限制的問題。

5 查詢優化

引入分割槽給查詢帶來了一些新的思路和bug。分割槽最大的優點就是優化器可以根據分割槽函式來過濾一些分割槽。根據粗粒度索引的優勢,通過分割槽過濾通常可以讓查詢掃描更少的資料。

所以,對於訪問分割槽表來說,很重要的一點是要在where條件中帶入分割槽列,有時候即使刊例多餘的也要帶上,這樣就可以讓優化器能夠過濾掉無需訪問的分割槽。如果沒有這些條件,MySQL就需要讓對應儲存引擎訪問這個表的所有分割槽,如果表非常大的話,就可能會非常慢。

使用explain partitions select * from sales \G

這個查詢將訪問所有的分割槽。下面我們給where條件中加入一個時間限制條件

MySQL優化器已經很善於過濾分割槽。比如它能夠將範圍條件轉化為離散的值列表,並根據列表中的每個值過濾分割槽。然而,優化器也不是萬能的。下面的查詢的where條件理論上可以裸露分割槽,實際上不行

MySQL只能在使用分割槽函式的列本身進行比較時才能過濾分割槽,而不能夠根據表示式的值去過濾分割槽,即使這個表示式就是分割槽函式也不行。這就和查詢中使用獨立的列才能使用索引的道理是一樣的。所以只需要把上面的穿等價的改寫為:

這裡寫的where條件中帶入的是分割槽列,而不是基於分割槽列的表示式,所以優化器能夠利用這個條件過濾部分分割槽。一個很重要的原則是;幾遍在建立分割槽時可以使用表示式,但在查詢時卻只能根據列來過濾分割槽。

優化器在處理查詢的過程中總是儘可能聰明的去過濾分割槽。例如,若分割槽表時關聯操作中的第二張表,且關聯條件時分割槽間,MySQL就只會在對應的分割槽裡匹配行。explain無法顯示這種情況下的分割槽過濾,因為這是執行時的分割槽過濾,而不是在查詢優化階段的。

6 合併表

合併表是一種早期的、簡單的分割槽實現,和分割槽表相比有一些不同的限制,並且缺乏優化。分割槽表嚴哥來說是一個所及上的概念,使用者無法訪問底層的各個分割槽,對使用者來說分割槽時透明的。但是合併表允許使用者單獨訪問各個字表。分割槽表和優化器的結合更緊密,這也是未來發展的趨勢,而合併表則是一種將被淘汰的技術,在未來版本中有可能被刪除。

和分割槽表類似的是,在MyISAM中各個字表可以被一個結構完全相同的邏輯表鎖封裝。可以簡單的把這個表當做一個老大、早期的、功能有限的分割槽表,因為它自身的特性,甚至可以提供一些分割槽表沒有的功能。

合併表相當於一個容器,裡面包含了多個真實表。可以在create table中使用一種特別的union語法來指定包含哪些真實表:

合併表還有很多其他的限制和行為,下面有幾個重點需要記住:

① 在使用create語句建立一個合併表的時候,並不會檢查各個字表的相容性。入股字表的定義稍有不同,那麼MySQL就可能創建出一個後面無法使用的合併表。另外,如果在成功建立了合併表後在修改某個字表的定義,那麼之後再使用合併表可能會報錯:

Unable to open underlying table which is differently defined or of non-MyISAM type or doesn`t exitst.

② 根據合併表的特性,不能發現,在合併表上無法使用replace語法,無法使用自增欄位。

③ 如果一個查詢訪問合併表,那麼它需要訪問所有字表。這會讓根據鍵查詢單行的查詢速度變慢,如果能夠值訪問一個對應表,速度肯定更快。所以,限制合併表中的字表數量很重要,特別是當合並表是某個關聯查詢的一部分的時候,因為這時候訪問一個表的記錄數可能會將比較操作傳遞到關聯的其他表中,這時減少記錄的訪問就是減少整個關聯操作。當你打算使用合併表時還要記住以下幾點:

【1】執行範文查詢時,需要在每一個字表上各執行一次,這比直接訪問單個表的效能要差很多,而且子表月多,效能越差。

【2】全表掃描和普通表的全表掃描速度相同。

【3】在合併表上做唯一鍵和主鍵查詢時,一旦找到一行資料就會停止。所以一旦查詢在合併表的某一個子表中找到一行資料,就會立刻返回,不會再訪問任何其他的表。

【4】子表的讀取順序和create table語句中的順序相同。