1. 程式人生 > 程式設計 >高效能mysql讀書筆記(四) Mysql高階特性

高效能mysql讀書筆記(四) Mysql高階特性

分割槽表

對使用者來說,分割槽表是一個獨立的邏輯表,但是底層由多個物理子表組成。實現分割槽表的程式碼實際上是對一組底層表的控制程式碼物件的封裝。對分割槽表的請求,都會通過控制程式碼物件轉化成對儲存引擎的介面呼叫。

  • mysql實現分割槽表的子表----對底層表的封裝----意味著索引也是按照分割槽的子表定義的,而沒有全域性索引。

分割槽的一個主要目的是將資料按照一個較粗的粒度分在不同的表中,這樣做可以將相關資料存放在一起,另外,如果想一次批量刪除整個分割槽的資料也會變的很方便。

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

  • 表非常大以至於無法全部都放到記憶體中,或者只在表的最後部分有熱點資料,其他均是歷史資料。
  • 分割槽表的資料更容易維護。例如想批量刪除大量資料可以使用清除整個分割槽的方式。另外,還可以對一個獨立分割槽進行優化,檢查,修復等操作。
  • 分割槽表的資料可以分佈在不同的物理裝置上,從而高效的利用多個硬體裝置。
  • 可以使用分割槽表來避免某些特殊的瓶頸,例如InnoDB的單個索引的互斥訪問,ext3檔案系統的inode鎖競爭等。
  • 如果需要,還可以備份和恢復獨立的分割槽,這在非常大的資料集的場景下效果非常好。

分割槽表本身也有一些限制:

  • 一個表最多隻能有1024個分割槽
  • 在mysql5.1中,分割槽表示式必須是整數,或者是返回整數的表示式,在mysql5.5 中,某些場景中可以直接使用列來進行分割槽。
  • 如果分割槽欄位中有主鍵或者唯一索引的列,那麼所有主鍵列和唯一索引列都必須包含進來。
  • 分割槽表中無法使用外來鍵約束。

分割槽表的原理

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

  • select查詢 當查詢一個分割槽表的時候,分割槽層先開啟並鎖住所有的底層表,優化器先判斷是否可以過濾部分分割槽,然後再呼叫對應的儲存引擎介面訪問各個分割槽的資料。
  • insert操作 當寫入一條記錄時,分割槽層先開啟並鎖住所有的底層表,然後確定哪個分割槽接收這條記錄,再將記錄寫入對應底層表。
  • delete操作 當刪除一條記錄時,分割槽層先開啟並鎖住所有的底層表,然後確定資料對應的分割槽,最後對相應底層表進行刪除操作。
  • update操作 當更新一條記錄時,分割槽層先開啟並鎖住所有的底層表,mysql先確定需要更新的記錄在哪個分割槽,然後取出資料並更新,再判斷更新後的資料應該放在哪個分割槽,最後對底層表進行寫入操作,並對原資料所在的底層表進行刪除操作。

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

分割槽表的型別

mysql支援多種分割槽表,我們看到最多的是根據範圍進行分割槽,每個分割槽儲存落在某個範圍的記錄,分割槽表示式可以是列,也可以是包含列的表示式。

create table sales(
   order_date datetime not null,-- other columns omitted
  ) engine=innodb partition by range(year(order_date)) (
    partition p_2010 values less than (2010),partition p_2011 values less than (2011),partition p_2012 values less than (2012),partition p_catchall  values less than maxvalue
    );
複製程式碼

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

mysql還支援鍵值,雜湊和列表分割槽,這其中有些還支援子分割槽,不過我們在生產環境中很少見到。

我們還看到的一些其他的分割槽技術包括:

  • 根據鍵值進行分割槽,來減少InnoDB的互斥競爭。
  • 使用資料模函式來進行分割槽,然後將資料輪詢放入不同的分割槽。
  • 假設表有一個自增的主鍵列id,希望根據時間將最近的熱點資料集中存放,那麼必須將時間戳包含在主鍵當中才行,而這和主鍵本身的意義相矛盾,這種情況下可以使用這樣的分割槽表示式來實現相同的目的:Hash(id div 100000) ,這將為100萬資料簡歷一個分割槽,這樣一方面實現了當初的分割槽目的,另一方面比起使用時間範圍分割槽還避免了一個問題,就是當超過一定閥值時,如果使用時間範圍分割槽就必須新增分割槽。

如何使用分割槽表

假設我們希望從一個非常大的表中查詢出一段時間的記錄,而這個表中包含了很多年的歷史資料,資料是按照時間排序的,例如希望查詢最近幾個月的資料,這大約有10億條記錄。 首先很肯定:因為資料量巨大,肯定不能在每次查詢的時候都掃描全表。考慮到索引在空間和維護上的消耗,也不希望使用索引。即使真的使用索引,你會發現資料並不是按照想要的房市聚集的,而且會有大量的碎片產生,最終會導致一個查詢產生成千上萬的隨機I/O,應用程式也隨之僵死。 這時候有兩條路可選:讓所有的查詢都只在資料表上做順序掃描或者將資料表和索引全部都快取在記憶體裡。

這裡需要再陳述一遍:在資料量大的時候,B-Tree索引就無法起作用了。除非是索引覆蓋查詢,否則資料庫伺服器需要根據索引掃描的結果回表,查詢所有符合條件的記錄,如果資料量巨大,浙江產生大量隨機I/O,隨之,資料庫的響應時間將達到不可接受的程度。 另外,索引維護的代價也非常高。

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

為了保證大資料量的可拓展性,一般有下面兩個策略:

  • 全量掃描資料,不需要任何索引 可以使用簡單的分割槽方式存放表,不要任何索引,根據分割槽的規則大致定位需要的資料位置。只要能夠使用where條件,將需要的資料限制在少數分割槽中,則效率是很高的。當然,也需要做一些簡單的運算保證查詢的響應時間能夠滿足需求。使用該策略假設不用將資料完全放入到記憶體中,同事還假設需要的資料全都在磁碟上,因為記憶體相對很小,資料很快會被擠出記憶體,所以快取起不了任何作用。這個策略適用於以正常的方式訪問大量資料的時候。
  • 索引資料,並分離熱點 如果資料有明顯的熱點,而且除了這部分資料,其他資料很少被訪問到,那麼可以將這部分熱點資料單獨放到一個分割槽中,讓這個分割槽的資料能夠有機會都快取在記憶體中。這樣查詢就可以值訪問一個很小的分割槽表,能夠使用索引,也能夠有效的使用快取。

什麼情況下會出問題

上面我們介紹的兩個分割槽策略都是基於兩個非常重要的假設:查詢能夠過濾掉很多額外的分割槽,分割槽本身並不會帶來很多額外的代價。而事實證明,這兩個假設在某些場景下會有問題。

  • NULL值會使分割槽過濾無效 關於分割槽表,一個容易讓人誤解的地方就是分割槽的表示式的值可以是null,第一個分割槽是一個特殊分割槽。假設按照partition by range year(order_date)分割槽,那麼所有order__date為null或者是一個非法值的時候,記錄都會被存放到第一個分割槽。 如果第一個分割槽非常大,特別是當使用全量掃描資料,不要任何索引的策略時,代價會非常大。而且掃描兩個分割槽來查詢列也不是我們使用分割槽表的初衷。 為了避免這種情況,可以建立一個“無用”的第一個分割槽,例如上面的例子中可以使用partition p_nulls values less than (0) 來建立第一個分割槽。如果插入表中的資料都是有效的,那麼第一個分割槽就是空的,這樣即使需要檢測第一個分割槽,代價也會非常小。在mysql5.5中就不需要這個優化技巧了,因為可以直接使用列本身而不是基於列的函式進行分割槽。
  • 分割槽列和索引列不匹配 如果定義的索引列和分割槽列不匹配,會導致查詢無法進行分割槽過濾。
  • 選擇分割槽的成本可能很高
  • 開啟並鎖住所有底層表的成本可能很高 當查詢訪問分割槽表的時候,mysql需要開啟並鎖住所有的底層表,這是分割槽表的另一個開銷。這個操作在分割槽過濾之前發生,所以無法通過分割槽過濾降低此開銷,並且該開銷也和分割槽型別無關,會影響所有的查詢。
  • 維護分割槽的成本可能很高

查詢優化

分割槽最大的有點是優化器可以根據分割槽函式來過濾一些分割槽,根據粗粒度索引的優勢,通過分割槽過濾通常可以查詢掃描更少的資料。 對於分割槽表來說,很重要的一點是要在where條件中帶入分割槽列,有時候即使看似 多餘的也要帶上,這樣就可以讓優化器能夠過濾掉無需訪問的分割槽。如果沒有這些條件,mysql就需要讓對應的儲存引擎訪問這個表的所有分割槽,如果表非常大的話,就可能會非常慢。 mysql只能在使用分割槽函式的列本身進行比較時才能過濾分割槽,而不能根據表示式的值去過濾分割槽,即使這個表示式就是分割槽函式也不行。

檢視

mysql5.0版本之後開始引入檢視。檢視本身是一個虛擬表,不存放任何資料,在使用sql語句訪問檢視的時候,它返回的資料是mysql從其他表中生成的。 檢視和表是在同一個名稱空間,mysql在很多地方對於檢視和表是同樣對待的。不過檢視和表也有不同,例如不能對檢視建立觸發器,也不能使用drop table命令刪除檢視。

mysql可以使用兩種辦法處理檢視:合併演演算法和臨時表演演算法。如果可能儘可能使用合併演演算法。

  • 使用MERGE策略,MySQL會先將輸入的查詢語句和檢視的宣告語句進行合併,然後執行合併後的語句並返回。但是如果輸入的查詢語句中不允許包含一些聚合函式如: MIN,MAX,SUM,COUNT,AVG,etc.,or DISTINCT,GROUP BY,HAVING,LIMIT,UNION,UNION ALL,subquery。同樣如果檢視宣告沒有指向任何資料表,也是不允許的。如果出現以上任意情況,MySQL預設會使用UNDEFINED策略。
  • 使用TEMPTABLE策略,MySQL先基於檢視的宣告建立一張temporary table,當輸入查詢語句時會直接查詢這張temporary table。由於需要建立temporary table來儲存檢視的結果集,TEMPTABLE的效率要比MERGE策略低,另外使用temporary table策略的檢視是無法更新的。

如果你想確定mysql到底是使用合併演演算法還是臨時表演演算法,可以explain一條針對檢視的簡單查詢:

explain select * from <view_name>
+----+-------------+------------+------------+--------+---------------+---------+---------+-------------------+------+----------+-------------+
| id | select_type | table      | partitions | type   | possible_keys | key     | key_len | ref               | rows | filtered | Extra       |
+----+-------------+------------+------------+--------+---------------+---------+---------+-------------------+------+----------+-------------+
| 1  | PRIMARY     | <derived2> | <null>     | ALL    | <null>        | <null>  | <null>  | <null>            | 34   | 100.0    | <null>      |
| 2  | DERIVED     | blog       | <null>     | index  | user_id       | user_id | 4       | <null>            | 34   | 100.0    | Using index |
| 2  | DERIVED     | u          | <null>     | eq_ref | PRIMARY       | PRIMARY | 4       | test.blog.user_id | 1    | 100.0    | Using where |
+----+-------------+------------+------------+--------+---------------+---------+---------+-------------------+------+----------+-------------+
複製程式碼

這裡的select_type為derived,說明該檢視是採用的臨時表演演算法實現的。

可更新檢視

可更新檢視是指可以通過更新這個檢視來更新檢視涉及的相關表。只要指定了合適的條件,就可以更新,刪除甚至向檢視中寫入資料。 如果檢視定義中包含了group by,union,聚合函式,以及其他一些特殊情況,就不能被更新了。 更新檢視的查詢也可以是一個關聯語句,但是有一個限制,被更新的列必須來自同一個表中,另外,所有使用臨時表演演算法實現的檢視都無法被更新。

檢視對效能的影響

可以使用檢視實現基於列的許可權控制,卻不需要真正的在系統中建立列許可權,因此沒有額外的開銷。

如果打算使用檢視來提升效能,需要做比較詳細的測試。即使合併演演算法實現的檢視也會有額外的開銷,而且檢視的效能很難預測。 在mysql優化器中,檢視的程式碼執行路徑也完全不同,這部分程式碼測試還不透全面,可能會有一些音層缺陷和問題。所以我們認為檢視還不是那麼成熟。

檢視的限制

mysql還不支援物化檢視(將檢視結果資料存放在一個可以檢視的表中,並定期從原始表中重新整理資料到這個表中). mysql也不支援在檢視中建立索引。不過可以使用構建快取表或者彙總表的辦法來模擬物化檢視和索引。

mysql檢視實現上也有一些讓人煩惱的地方,例如,mysql並不會儲存檢視定義的原始sql語句,所以打算通過執行 show create view 後在簡單的修改其結果的方式來重新定義檢視,可能會大失所望。show create view出來的檢視建立語句將以一種不友好的內部格式呈現,充滿了各種轉義符和引號,沒有程式碼格式,沒有註釋,也沒有縮排。

外來鍵約束

使用外來鍵是有成本的,比如外來鍵通常都要求每次在修改資料時都要在另外一張表中多執行一次查詢操作。 雖然InnoDB強制外來鍵使用索引,但還是無法消除這種約束檢查的開銷。如果外來鍵列的選擇性很低,則會導致一個非常大且選擇性低的索引.

不過在某些場景下,外來鍵會提升一些效能。如果想確保兩個相關表始終有一致的資料,那麼使用外來鍵比在應用程式中檢查一致性的效能要高得多,此外,外來鍵在相關資料的刪除和更新上,也比在應用中維護要更高效,不過外來鍵維護操作是逐行進行的,所以這樣的更新會比批量刪除和更新要慢些。

外來鍵約束使得查詢需要額外訪問一些別的表,這也意味著需要額外的鎖。如果向子表中插入一條記錄,外來鍵約束會讓InnoDB檢查對應的父表記錄,也就需要對父表對應記錄進行加鎖操作,來確保這條記錄不會在這個事務完成之時就被刪除了。這回導致額外的鎖等待,甚至會導致一些死鎖。

如果只是使用外來鍵做約束,那通常在應用程式裡實現該約束會更好。外來鍵會帶來很大的額外消耗。

在mysql內部儲存程式碼

mysql允許通過觸發器,儲存過程,函式的形式來儲存程式碼,從mysql5.1開始,還可以在定時任務中存放程式碼,這個定時任務也被稱為“事件”。儲存過程和儲存函式都被統稱為“儲存程式”。

在mysql中使用儲存程式碼的優點:

  • 它在伺服器內部執行,離資料最近,在伺服器上執行還可以節省頻寬和網路延遲
  • 這是一種程式碼重用。可以方便的統一業務規則,保證某些行為總是一致,所以也可以為應用提供一定的安全性。
  • 它可以簡化程式碼的維護和版本更新
  • 它可以幫助提升安全,比如提供更細粒度的許可權控制。
  • 伺服器可以快取儲存過程的執行計劃,這對於需要反覆呼叫的過程,會大大降低消耗
  • 因為是在伺服器部署的,所以備份,維護都可以在伺服器端完成。所以儲存程式的維護工作會很簡單。
  • 它可以在應用開發和資料庫開發人員之間更好的分工。不過最好是由資料庫專家來開發儲存過程,因為不是每個應用開發人員都能寫出高效的sql查詢。

儲存程式碼也有如下缺點:

  • mysql本身沒有提供好用的開發和除錯工具,所以編寫msyql的儲存程式碼比其他的資料要更難
  • 較之應用程式的程式碼,儲存程式碼效率要稍微差些。例如儲存程式碼中可以使用的函式非常有限,所以使用儲存程式碼很難編寫複雜的字串為好功能,也很難實現太複雜的邏輯
  • 儲存程式碼可能會給應用程式程式碼的部署帶來額外的複雜性。原本只需要部署應用程式碼和庫表結構,現在還要額外的部署mysql內部的儲存程式碼
  • 因為儲存程式都部署在伺服器,所以可能有安全隱患。如果將非標準的加密功能放在了儲存程式中,那麼若資料庫被攻破,資料也就洩漏了。但是若將加密函式放在應用程式程式碼中,那麼攻擊者必須同時攻破程式和資料庫才能獲得資料。
  • 儲存過程會給資料庫增加額外的壓力,而資料庫伺服器的拓展性香餅應用伺服器要差很多
  • mysql並沒有什麼選項可以控制 儲存程式的資源消耗,所以在儲存過程中的一個小錯誤,可能直接把伺服器拖死
  • 儲存程式碼在mysql中的實現也有很多限制--執行計劃是連線級別的,遊標的物化和臨時表相同,在mysql5.5版本之前,異常處理也非常困難,等等
  • 除錯mysql的儲存過程是一件很困難的事情。
  • 它和基於語句的二進位制日誌複製合作的並不好。

最後,儲存程式碼是一種幫助應用隱藏複雜性,使得應用開發更簡單的方法。不過,它的效能可能更低,而且會給MySQL的複製等增加潛在的風險。所以當你打算使用儲存過程的時候,需要問問自己,到底希望程式邏輯在哪兒實現:是資料庫中還是應用程式碼中?這兩種做法都可以,也都很流行。只是當你編寫儲存程式碼的時候,你需要明白這是將程式邏輯放在資料庫中。

繫結變數

當建立一個繫結變數sql時,客戶端向伺服器傳送了一個sql語句的原型。伺服器收到這個sql語句框架後,解析並儲存這個sql語句的部分執行計劃,返回給客戶端一個sql語句處理控制程式碼。以後每次執行這類查詢 ,客戶端都指定使用這個控制程式碼。 繫結變數sql使用問號標記可以接收引數的位置,當真正需要執行具體查詢的時候,則使用具體值代替這些問號。

  • 在伺服器只解析一次sql語句
  • 在伺服器端某些優化器的工作只需要執行一次,因為它會快取一部分的執行計劃。
  • 以二進位制的方式只傳送引數和控制程式碼,比起每次都傳送ascii碼文字效率更高,一個二進位制的日期欄位只需3個位元組,但如果是ascii則需要10個位元組。
  • 僅僅是引數,而不是整個查詢語句,需要傳送到伺服器端,所以網路開銷少
  • mysql在儲存引數的時候,直接將其存放到快取中,不再需要在記憶體中多次複製。

繫結變數相對也更安全,無需在應用程式中處理轉義,一則更簡單了,二則也大大減少了sql注入和攻擊風險。

繫結變數的限制

  • 繫結變數是會話級別的,所以連線之間不能共用繫結變數控制程式碼。
  • 並不是所有的時候使用繫結變數都能獲得更好的效能。如果只是執行一次sql,那麼使用繫結變數無疑比直接執行多了一次額外的準備階段消耗,而且還需要一次額外的網路消耗。
  • 如果總是忘記釋放繫結變數資源,則在伺服器端很容易發生資源“洩漏”。繫結變數sql總數的限制是一個全侷限制,所以在某一個地方的錯誤可能會對所有其的執行緒都產生影響。

字符集和校對

字符集是指一種從二進位制編碼到某類字元符號的對映,校對是指一組用於某個字符集的排序規則。

mysql如何使用字符集

每種字符集都可能有多種校對規則,並且都有一個預設的校對規則。

查詢快取

mysql查詢快取儲存查詢返回的完整結果,當查詢命中該快取,mysql會立即返回結果,跳過瞭解析,優化和執行階段。

查詢快取系統會跟蹤查詢中涉及的每個表,如果這些表發生變化,那麼和這個表相關的所有的快取資料都將失效。

查詢快取對應用程式是完全透明的。

mysql如何判斷快取命中

mysql判斷存換命中的方法很簡單,快取存放在一個引用表中,通過一個雜湊值引用,這個雜湊值包括瞭如下因素:查詢本身,當前要查詢的資料庫,客戶端協議的版本以及一些其他可能影響返回結果的資訊。 當判斷快取是否命中時,mysql不會解析,正規化或者引數化查詢語句,而是直接使用sql語句和客戶端傳送過來的其他原始資訊。任何字元上的不同,都會導致快取的不命中。

查詢語句中有一些不確定的資料時,則不會被快取。如果查詢中包含任何使用者自定義函式,儲存函式,使用者變數,臨時表,mysql庫中的系統表,或者任何包含列級別許可權的表,都不會被快取。

查詢快取對讀寫都會帶來額外的消耗:

  • 讀查詢在開始之前必須先檢查是否命中快取
  • 如果這個讀查詢可以被快取,那麼當完成執行後,mysql若發現查詢快取中沒有這個查詢,會將其結果存入查詢快取,這會帶來額外的系統消耗
  • 這對寫操作也會有影響,因為當向某個表寫入資料的時候,mysql必須將對應表的所有存換都設定失效。如果查詢快取非常大或者碎片很多,這個操作就可能會帶來很大的系統開銷。

如果查詢快取使用了很大量的記憶體,快取失效操作就可能稱為一個非常嚴重的問題瓶頸。如果快取中存放了大量的查詢結果,那麼快取失效操作時整個系統都可能會僵死一會兒。