高性能MySQL-筆記

高性能MySQL

一、MySQL結構

每一個客戶端鏈接會在服務器進程中擁有一個線程,該鏈接的查詢只會在單獨的線程中執行。MySQL會解析查詢,並建立內部數據結構,而後對其進行各類優化。對於SELECT語句,解析查詢以前,服務器會先檢查緩存,若是可以在其中找到對應的查詢,服務器就再也不執行查詢解析,而是直接返回緩存中的結果。mysql

併發控制

MySQL在兩個層面實現併發控制:服務器層與存儲引擎層。算法

在處理併發讀或寫時,能夠經過實現一個由兩種鎖組成的系統來解決問題。這兩種鎖一般被稱爲共享鎖和排他鎖,或者稱爲讀鎖和寫鎖。讀鎖是共享的,或者說是相互不阻塞的,多個客戶能夠在同時讀取同一數據。而寫鎖是排他的,同一時刻只能有一個用戶可以寫入,並防止其餘用戶讀取正在寫入的數據。sql

鎖粒度是指加鎖的對象的大小。顯然,鎖的粒度越小,併發控制效率越高。鎖的各類操做,包括得到鎖、檢查鎖和釋放鎖等,都會增長系統開銷。所以,若是系統花費大量時間來管理鎖,而不是用來獲取數據,就會影響系統性能。數據庫

有兩種常見的縮策略,表鎖和行級鎖。表鎖開銷較小,可是併發控制很差。行級鎖能夠很好地實現併發控制,可是開銷比較大。緩存

事務

事務將幾個操做做爲一個總體,要麼所有執行,要麼所有放棄。事務的四大特性ACID安全

  1. 原子性:整個事務要麼提交要麼回滾,必須做爲一個總體。(幾個操做做爲一個總體)
  2. 一致性:數據庫老是從一個一致狀態轉換到另外一個一致狀態。(不增就不減)
  3. 隔離性:事務提交以前所作的修改不可見。
  4. 持久性:事務提交以後,將修改持久化到數據庫中。

事務處理也會使系統作更多額外工做,用戶能夠根據是否須要進行事務處理,選擇合適的存儲引擎。服務器

下面是四種事務的隔離級別:session

  1. READ UNCOMMITTED (未提交讀):沒有提交、但已經被修改的數據能夠被讀到,也叫髒讀
  2. READ COMITTED (提交讀):一個事務開始時,只能看見已經提交的修改,而且所作的修改對其餘事務不可見。這種存在的一個問題叫不可重複讀,就是指事務A讀取某條記錄以後,事務B對其進行了修改,這時當A再次讀取該數據的時候就會發現與以前讀取的結果不同。
  3. REPEATABLE READ (可重複讀):事務A對數據庫全部行作了修改時,事務B對向數據庫中插入了一行新的數據。這時A發現還有沒有修改的記錄,就行發生幻覺同樣,叫作幻讀。MySQL默認級別。
  4. SERIALIZABLE (可串行化):事務按照串行的方式執行,並在每行數據上加鎖,可能產生大量的超時和鎖爭用問題。

死鎖指的是多個事務在同一資源上相互佔用,並請求對方佔用的資源,致使惡性循環的現象。數據庫系統中實現了各類死鎖檢測和死鎖超時機制。數據結構

在MySQL中默認是自動提交事務的,每一個查詢操做被看成一個事務。可使用set autocommit來設置是否自動提交。能夠經過set session transaction isolation level來設置隔離級別。在同一事務中使用多種存儲引擎是不可靠的。併發

多版本併發控制MVCC

MySQL中大多事務型存儲引擎實現的都不是簡單的行級鎖,通常同時實現了多版本併發控制。MVCC的實現是經過保存數據在某個時間點的快照來實現的。即,不論須要執行多長時間,每一個事務看到的數據都是一致的。典型的實現有樂觀併發控制和悲觀併發控制

InnoDB的MVCC經過在每行記錄的後面保存兩個隱藏的列來實現。這兩個列,一個用來保存過時(被刪除)的版本號,一個用來保存建立的版本號。每一個事務開始時,會使版本號遞增。事務開始時的版本號被用做事務的版本號:

  1. 在查詢的時候,會檢查版本號,並返回小於等於當前版本號,而且刪除版本號大於當前事務版本號的記錄。
  2. 刪除時將當前版本號做爲刪除版本號。
  3. 插入時將當前系統版本號做爲行版本號。
  4. 更新時,插入一行新紀錄,並保存當前系統版本號做爲行版本號,同時保持當前系統版本號做爲以前記錄的刪除版本號。

MySQL存儲引擎

.frm文件用來保存表的定義。

MySQL默認存儲引擎,採用MVCC來支持高併發,而且實現了四個標準的隔離級別。基於聚簇索引創建。

MyISAM不支持事務和行級鎖,並且沒法在崩潰以後安全恢復;它將表存儲在兩個文件中:數據文件和索引文件,分別以.MYD.MYI爲拓展名。它對整張表加鎖,而不是某行。若是建立並導入數據以後,不會再進行修改,可使用壓縮表來減小空間佔用和IO,從而提高查詢性能。

還有一些其餘的存儲引擎。

除非某些InnoDB不具有的特性,而且沒有其餘方法能夠替代,不然都應該優先優先選擇InnoDB引擎。除非萬不得已,不然不要混合使用多種存儲引擎,不然可能帶來一些複雜的問題及潛在的BUG.

修改存儲引擎:

  1. Alter table tbl_name engine = Innodb:須要執行很長時間,MySQL會按行將數據從原表複製到一張新的表中,在複製期間可能會小號系統的IO能力,同時在原表上加鎖。
  2. 導入導出:用mysqldump將數據導出到文件,燃火修改文件中create table語句的存儲引擎選項,注意要同時修改表名。
  3. CREATE & SELECT:數據量不大的時候,能夠先建立一張使用新的存儲引擎的表,而後利用INSERT SELECT將原表中的數據插入到新表中。當數據量比較大的時候,可使用between語句來分批次操做完成。

二、Shcema與數據類型優化

2.1 選擇合適的數據類型

2.1.1 選擇的數據類型原則:

  1. 更小的一般更好:佔用更好的磁盤、內存和緩存,處理時所需的時間週期更小。
  2. 簡單就好:簡單的數據類型操做須要更少的CPU週期。如,整數比字符操做代價更低。
  3. 進來避免null:null的列使得索引、索引統計和值比較都更復雜。

2.1.2 整數類型

MySQL整數能夠指定寬度,如int(11),對大多數應用這是沒有意義的:它不會限制值的合法範圍,只是規定了MySQL的一些交互工具用來顯示字符的個數。對於存儲和計算來講,int(20)和int(1)是相同的。

2.1.3 實數類型

由於須要額外空間和計算開銷,因此儘可能只在對小數進行計算的時候才使用decimal。在數據量比較大的時候,能夠考慮使用bigint代替decimal,將存儲的單位根據小數的位數乘以相應的倍數便可。

2.1.4 字符串類型

CHAR適合存儲短的短的字符串,或者全部的值都接近同一長度。對於常常變動的數據,CHAR也比VARCHAR好,由於定長的CHAR不容易產生碎片。對於很是短的字符串,CHAR也比VARCHAR更好,由於VARCHAR還須要1或2個額外的字節存儲字符串長度。

可使用枚舉替代經常使用的字符串類型,枚舉能夠把一些不重複的字符串存儲成預約義的集合。枚舉字段是按照內部存儲的整數而不是字符串進行排序的。枚舉很差的地方是當向枚舉中增長字段的時候,須要使用ALTER TABLE語句來進行修改。因此,對未來可能會變的字符串,使用枚舉不是個好的主意。

2.1.5 時間和日期類型

若是須要將時間保存到毫秒級別,可使用BIGINT.

2.1.6 位數據類型

可使用BIT列在一列中存儲一個或多個true/false值。BIT(1)定義一個包含單個位的字段,bit(2)存儲兩個位。bit列最多存儲64個位。

MySQL將BIT看成字符串類型,而不是數字類型。當檢索bit(1)時,結果是一個包含二進制0或1值的字符串,而不是ASCII碼的"0"或"1"。而後,在數字上下文場景中檢索時,結果將是爲字符串轉換成的數字.

2.2 範式和反範式

所謂的範式就是,好比,若是咱們須要學生和學校的記錄,若是咱們將學生和學校放在不一樣的表中就符合範式,若是放在同一表中就是反範式的。

範式化具備一些好處,好比:操做更快;每次只要修改少許的數據;表更小,適合放在內存中,操做更快。缺點是一般須要表關聯。

不過實際咱們並不徹底遵照範式和反範式的規則。

2.3 緩存表和彙總表

緩存表表示存儲那些能夠簡單地從schema其餘表獲取數據的表。彙總表保存的是使用GROUP BY語句聚合數據的表,使用匯總表的緣由是,實時計算和統計值是很昂貴的操做,由於要麼須要掃描表中的大部分數據,要麼只能在某些索引上纔能有效運行。

計數器表是用來統計某個操做的次數的表,咱們能夠在一個表中定義一個名爲cnt的字段來表示操做的次數,而後每次執行了操做以後將其加1。可是,加1須要更新操做來完成,每次更新的時候要獲取記錄的鎖,所以併發效率不高。解決這個問題,咱們能夠再增長一個字段slot做爲隨機的槽,每次執行操做的時候,咱們使用隨機數選擇某個slot,並對其進行+1更新(只用鎖住部分數據,所以效率比較高)。最後統計的時候將所有記錄加起來便可。

三、建立高性能的索引

3.1 基礎

數據庫的索引相似於書的索引,實際的查找某個值的時候,先按照值進行查找,而後返回包含該值的數據行。索引能夠包含一個或多個列的值,若是索引包含多個列,那麼列的順序也很重要——索引對多個列排序的依據是CREATE TABLE時定義索引的順序,因此MySQL只能高效地使用索引的最左前綴列。

在MySQL中,索引是在存儲引擎層而不是服務器層實現的。因此,沒有統一的標準:不一樣存儲引擎工做方式不一樣。MySQL支持的索引類型以下:

3.1.1 B-Tree索引

一般人們所說的索引。實際上不少存儲引擎使用的是B+Tree. B-Tree索引適用於全鍵值、鍵值範圍或鍵前綴查找。類型,以多列索引key(last_name, first_name, dob)爲例:

  1. 全值匹配:指定查詢的人的fitst_name, last_name和dob;
  2. 匹配最左前綴:查找指定了last_name的記錄;
  3. 匹配列前綴:匹配某一列的值的開頭部分,好比last_name以J開頭;
  4. 精確匹配某一列並範圍匹配另外一列:查找last_name爲Allen,而且first_name以k開頭的;
  5. 只訪問索引的查詢:B-Tree一般能夠支持只訪問索引的查詢,即查詢只須要訪問索引,而無需訪問數據行。

B-Tree的一些限制:

  1. 若是不是按照從最左列開始查找,則沒法使用索引。例如沒法查找只指定了first_name或者dob的記錄;
  2. 不能跳過索引中的列:不能在查找的時候只指定了last_name和dob,那麼dob不會使用索引。
  3. 若是查詢的時候有某個列的查詢範圍,則其右邊的全部列都沒法使用索引優化查找。好比對last_name使用了like,那麼first_name和dob將不會使用索引。

3.1.2 哈希索引

基於哈希表實現,只有精確匹配索引全部列的查詢纔有效。由於它對每行中的全部索引列計算出一個哈希碼,做爲哈希表的鍵(原理是基於拉鍊法的解決碰撞的策略)。在MySQL中只有Memory引擎顯式地支持哈希索引,Memory引擎同時也支持B-Tree索引。

哈希索引只須要存儲對應的哈希值,因此索引的結構十分緊湊,這讓哈希索引的查找速度很是快。然而,它也有自身的限制:

  1. 哈希索引只包含哈希值和行指針,而不存儲字段值,因此不能用索引中的值來避免讀取行
  2. 哈希索引數據並非按照索引值順序存儲的,因此也就沒法用於排序
  3. 哈希索引不支持部分列匹配查找,由於它用全部索引列來計算獲得哈希值。
  4. 索引列只支持等值比較,理由同上;
  5. 哈希索引數據查找很是快,除非有不少哈希衝突;
  6. 若是哈希衝突比較高,一些索引維護操做的代價也會很高。

3.1.3 空間數據索引(R-Tree)

MyISAM表支持空間索引,能夠用做地理數據存儲。

3.1.4 全文索引

它查找的是文本中的關鍵詞,而不是直接比較索引中的值。全文索引相似於搜索引擎作的事情,而不是簡單的where條件匹配。在相同的列上建立全文索引和基於B-Tree的索引不會衝突。

其餘索引,還有分型樹索引。

3.2 索引的有點

索引的優勢有:

  1. 索引大大減小了服務器須要掃描的數據量;
  2. 索引能夠大大幫助服務器避免排序和臨時表;
  3. 索引能夠將隨機IO變爲順序IO。

對於小型的表,使用全表掃描更高效;對中到大型的表,使用索引很是有效。對於特大型的表,創建和使用索引的代價會隨之增加。這種狀況下可使用分區來查出一組數據,而不是一條一條地匹配。

3.3 高性能索引的策略

3.3.1 獨立的列

若是查找中的列不是獨立的,則MySQL不會使用索引。獨立的列是指索引列不能是表達式的一部分,也不能是函數的參數。好比

select * from actor where actor_id + 1 = 5;
select * from actor where to_days(current_date) - to_days(col_day) <= 10;
複製代碼

3.3.2 前綴索引和索引的選擇性

索引很長的字符串會讓索引變得大且慢。一般能夠只索引開始部分的字符,這樣能夠節約索引空間,從而提升索引的效率。缺點是會下降索引的選擇性。索引的選擇性是指不重複的索引值和記錄總數的比,顯然越大越好。因此,咱們須要選擇足夠長的前綴來保證選擇性,同時又不能太長以下降索引空間。

咱們可使用語句

select count(distinct left(col_name, 3)) / count(*) from tbl_name;
複製代碼

來統計使用3個字符的前綴選擇性,同理能夠計算出4個,5個等的狀況。最後,選擇一個合理的前綴長度便可。選擇了長度以後能夠像下面這樣設置指定長度的索引:

alter table add key(col_nane(4));
複製代碼

3.3.3 多列索引

常見的錯誤是,爲每一個列建立獨立的索引,或者按照錯誤的順序建立多列索引。

若是一張表在col1和col2列上面存在索引,若是咱們使用col1 and col2做爲where的條件,那麼索引會作相角操做,若是使用col1 or col2,索引會作聯合操做. 相交操做一般意味着須要一個包含全部相關列的多列索引,而不是獨立的單列索引。聯合操做則會消耗CPU和內存在算法的緩存、排序和合並上。若在explain中看到有合併索引,應先檢查查詢和表結構,看看是否是最優的。也能夠經過optimizer_switch來關閉索引合併功能,或使用igonre index提示優化器忽略掉某些索引。

3.3.4 選擇合適的索引順序

若是要對多個列創建一個索引,除了上面的問題以外,還應該考慮所建的索引中列的順序。好比,對col1, col2兩列數據創建索引,那麼咱們的順序應被指定爲(col1, col2)仍是(col2, col1)呢。咱們能夠依然可使用上面的選擇性來解決這個問題,咱們能夠將選擇性比較高的列做爲索引的第一列,另外一列做爲第二列。

3.3.5 聚簇索引

聚簇索引不是一種單獨的索引類型,而是一種數據存儲方式。「聚簇」表示數據行和相鄰的鍵值緊湊地存儲在一塊兒。由於沒法同時把數據行存放在兩個不一樣的地方,因此一個表只能有一個聚簇索引。

InnoDB經過主鍵彙集數據,若是沒有主鍵就選擇一個惟一的非空索引,若是沒有這樣的索引,就隱式定義一個主鍵做爲聚簇索引。

聚餐的優勢:

  1. 將相關數據保存在一塊兒;
  2. 數據訪問更快。由於數據和索引保存在一塊兒。
  3. 使用覆蓋掃描的查詢能夠直接使用頁結點中的主鍵值。

缺點:

  1. 限制了提升IO密集型應用的性能,但若是數據所有放在內存中,則訪問順序就沒那麼重要了,聚簇的優點也沒了;
  2. 插入速度嚴重依賴於插入順序,按主鍵的順序插入是最快的方式,否則就應該在加載完後用opeimize table從新組織一下表;
  3. 代價更高:限制innodB將被更新的行移動到新的位置;
  4. 當主鍵被更新或者新數據插入致使行移動的時候,可能面臨「頁分裂」問題。
  5. 可能致使全表掃描變慢,尤爲樹數據比較稀疏,且數據不連續時;
  6. 二級索引可能比想象更大,因其包含了引用行的主鍵列;
  7. 二級索引須要兩次查找,而不是一次。

關於聚簇索引和非聚簇索引的存儲方式的區別:

假設有數據以下:

若是是聚簇的方式來存儲,那麼它的一級索引是下面的樣子:

也就是它們使用主鍵的值做爲彙集數據,而後每一個葉子包含了每行的所有記錄。聚簇索引的二級索引是下面的樣子:

注意將二級索引中存儲的值和最上面的表中的數據進行對比。從中能夠看出,實際上它的二級索引是先用二級索引的值找到一級索引,而後使用一級索引來查找整個記錄。

非聚簇的存儲方式是下面的樣子:

以上是非聚簇的一級索引的例子,非聚簇的二級索引的狀況與之相同。即它們都是先用指定的值找到行號,而後使用行號來查找完整記錄。

最好避免隨機的聚簇索引,特別是對於IO密集型的應用。由於隨機插入的時候,須要爲新的行尋找合適的位置——一般是已有數據的中間位置——而且分配空間。這回增長不少額外的工做,並致使分佈不夠優化。最好使用自增的主鍵。

3.3.6 覆蓋索引

若是一個索引包含了全部須要查詢的字段的值,就稱之爲覆蓋索引。覆蓋索引就是從索引中直接獲取查詢結果,要使用覆蓋索引須要注意select查詢列中包含在索引列中;where條件包含索引列或者複合索引的前導列;查詢結果的字段長度儘量少。

使用延遲關聯解決索引沒法覆蓋問題:下面的解決方法對效率的提高不是絕對的!

SELECT * FROM products WHERE actor = 'SEAB CARREY' AND title like '%APPOLO%'
複製代碼

上面的SQL中要查詢所有的列,而咱們沒有覆蓋所有列的索引,所以沒有覆蓋索引。另外,like操做沒法使用索引,由於like操做只有在匹配左前綴時才能使用索引。

咱們能夠像下面這樣解決問題:

SELECT * FROM products 
    JOIN (SELECT prod_id FROM products 
          WHERE actor = 'SEAB CARREY' AND title like '%APPOLO%')
AS t1 ON (t1.prod_id = products.prod_id)
複製代碼

這裏,須要先創建(actor, title, prod_id)索引。咱們先在子查詢中找到匹配的prod_id,而後跟外層中數據進行匹配來獲取全部列值。當符合where條件的數據數量遠小於actor過濾出的數據數量的時候,它的效率尤爲高。由於,根據子查詢的where過濾出數據以後才與外層查詢關聯,然後者使用actor讀取出數據以後,再用title進行關聯。前者須要讀取的數據量更少。

3.3.7 按索引掃描來排序

生成有序結果的兩種方式:排序,按索引順序掃描。當explain出的type爲index時,說明使用索引掃描來進行排序。MySQL可使用一個索引既知足排序,又知足查找。只有當索引的列順序和ORDER BY子句順序一致,且列的排序方向都同樣時,才能用索引對結果作排序。

下面是一些例子,假設索引是(col1, col2, col3),那麼:

...where col1 = 1 order by col2, col3;(√)
...where col1 = 1 order by col2;(√)
...where col1 > 1 order by col1, col2;(√)

...where col1 > 1 order by col2, col3;(X)
...where col1 = 1 order by col2 desc, col3 asc;(X)
...where col1 = 1 order by col2, col4;(X)
...where col1 = 1 order by col3;(X)
...where col1 = 1 and col2 in(1,3) order by col3;(X)
複製代碼

3.3.8 冗餘和重複索引

重複索引是指在相同的列上按照相同的順序建立的相同類型的索引。常見的錯誤有:

  1. 使用主鍵和惟一約束時與已有的因此衝突,由於主鍵和惟一約束是經過索引來實現的,若是再定義索引就會冗餘;
  2. 若建立了索引(A,B)再建立索引(A)則冗餘,而索引(B,A)和(B)不是,由於(B)不是最左前綴。

3.3.9 索引與鎖

Inn哦DB只有在訪問行的時候纔會對其加鎖,而索引可以減小訪問行的次數,因此索引能減小鎖的數量。

4 、慢查詢優化

4.1 慢查詢基礎

能夠經過下面兩個步驟來分析慢查詢:

  1. 確認應用程序是否在檢索超過需求的數據,這一般意味着訪問了太多的行或列;
  2. 確認MySQL服務器層是否在分析大量的超過需求的數據行。

4.1.1 請求了超過需求的數據

典型的請求查過須要的數據的場景:

  1. 查詢了不須要的記錄,若是隻須要指定行的記錄,可使用limit語句來只返回部分記錄;

  2. 多表關聯的時候返回了所有的列,好比下面的語句會返回tbl1和tbl2的所有記錄:

    SELECT * FROM tbl1 INNER JOIN tbl2 ...;
    複製代碼

    能夠改爲下面的樣子(若是隻須要tbl1的記錄的話)

    SELECT tb1.* FROM tbl1 INNER JOIN tbl2 ...;
    複製代碼
  3. 總數取出所有的列。缺點是沒有辦法使用覆蓋索引完成優化,並且會爲服務器帶來額外的IO、內存和CPU消耗。

  4. 重複查詢相同的記錄。最好將這些數據緩存起來。

4.1.2 掃描了額外的記錄

能夠經過explain輸出的列type中的值來獲得訪問類型。

4.2 重構查詢的方式

  1. 分解成多個簡單查詢:大查詢會鎖住更多的記錄,阻塞不少小的查詢,可將大的查詢分解成小的查詢,來下降對服務器的應用,還能夠經過設置時間間隔來說一次的壓力分解到更長的時間中去;
  2. 分解關聯查詢

4.3 優化特定類型的查詢

4.3.1 優化COUNT查詢

  1. 統計全部行數時,最好使用COUNT(*),語義清晰,性能更好
  2. 能夠經過相減的方式來下降掃描的行數

4.3.2 優化關聯查詢

  1. 確保ON或者USING子句中的列上面有索引
  2. 確保GROUP BY和ORDER BY的表達式中只涉及到一個表中的列,這樣MySQL纔可能使用索引優化
相關文章
相關標籤/搜索