Mysql 實戰 (二) 實踐

1、普通索引和惟一索引

查詢過程

對於普通索引來講,查找到知足條件的第一個記錄後,須要查找下一個記錄,直到碰到第一個不知足 條件的記錄。
對於惟一索引來講,因爲索引定義了惟一性,查找到第一個知足條件的記錄後,就會中止繼續檢索。
InnoDB 的數據是按數據頁爲單位來讀寫的。在InnoDB 中,每一個數據頁的大小默認是 16KB。對於整型字段,一個數據頁能夠放近千個 key。mysql

更新過程

當須要更新一個數據頁時,若是數據頁在內存中就直接更新, 而若是這個數據頁尚未在內存中的話,在不影響數據一致性的前提下,InooDB 會將這些更新操做緩存在 changebuffer 中, 這樣就不須要從磁盤中讀入這個數據頁了。在下次查詢須要訪問這個數據頁的時候,將數據頁讀入內存,而後執行 change buffer 中與這個頁有關的操做。

將 change buffer 中的操做應用到原數據頁,獲得最新結果的過程稱爲 merge。除了訪問這個數據頁會觸發 merge 外,系統有後臺線程會按期 merge。在數據庫正常關閉(shutdown)的過程當中,也會執行 merge 操做。
惟一索引的更新操做都要先判斷這個操做是否違反惟一性約束。而這必需要將數據頁讀入內存才能判斷。因此惟一索引的更新不能使用 change buffer,只有普通索引可使用。

change buffer 用的是 buffer pool 裏的內存,所以不能無限增大。change buffer 的大小,能夠經過參數 innodb_change_buffer_max_size 來動態設置。這個參數設置爲 50 的時候,表示 change buffer 的大小最多隻能佔用 buffer pool 的 50%。

若是插入一個新記錄 (4,400) 的話:
sql

  1. 這個記錄要更新的目標頁在內存中
    對於惟一索引來講,找到 3 和 5 之間的位置,判斷到沒有衝突,插入這個值,語句執行結束;對於普通索引來講,找到 3 和 5 之間的位置,插入這個值,語句執行結束。
  2. 這個記錄要更新的目標頁不在內存中
    對於惟一索引來講,須要將數據頁讀入內存,判斷到沒有衝突,插入這個值,語句執行結束;對於普通索引來講,則是將更新記錄在 change buffer,語句執行就結束了。

change buffer 的使用場景

對於寫多讀少的業務來講,頁面在寫完之後立刻被訪問到的機率比較小,此時change buffer 的使用效果最好。這種業務模型常見的就是帳單類、日誌類的系統。
反過來,假設一個業務的更新模式是寫入以後立刻會作查詢,那麼即便知足了條件,將更新先記錄在 change buffer,但以後因爲立刻要訪問這個數據頁,會當即觸發 merge 過程。這樣隨機訪問 IO 的次數不會減小,反而增長了 change buffer 的維護代價。因此,對於這種業務模式來講,change buffer 反而起到了反作用。數據庫

索引選擇和實踐

普通索引和惟一索引在查詢能力上是沒差異的,主要考慮的是對更新性能的影響。

若是全部的更新後面,都立刻伴隨着對這個記錄的查詢,那麼你應該關閉 change buffer。而在其餘狀況下,change buffer 都能提高更新性能。緩存

change buffer 和 redo log

insert into t(id,k) values(id1,k1),(id2,k2)
k1 所在的數據頁在內存 (InnoDBbuffer pool) 中,k2 所在的數據頁不在內存中。下圖是是帶 change buffer 的更新狀態圖。 圖片名稱
分析這條更新語句,你會發現它涉及了四個部分:內存、redo log(ib_log_fileX)、 數據表空間(t.ibd)、系統表空間(ibdata1)
(系統表空間就是用來放系統信息的,好比數據字典什麼的,對應的磁盤文件是ibdata1, 數據表空間就是一個個的表數據文件,對應的磁盤文件就是 表名.ibd)

這條更新語句作了以下的操做(按照圖中的數字順序):
工具

  1. Page 1 在內存中,直接更新內存;
  2. Page 2 沒有在內存中,就在內存的 change buffer 區域,記錄下「我要往 Page 2 插入一行」這個信息
  3. 將上述兩個動做記入 redo log 中(圖中 3 和 4)。作完上面這些,事務就能夠完成了。因此,你會看到,執行這條更新語句的成本很低,就是寫了兩處內存,而後寫了一處磁盤(兩次操做合在一塊兒寫了一次磁盤),並且仍是順序寫的。同時,圖中的兩個虛線箭頭,是後臺操做,不影響更新的響應時間。

redo log 主要節省的是隨機寫磁盤的 IO 消耗(轉成順序寫),而 change buffer 主要節省的則是隨機讀磁盤的 IO 消耗

若是某次寫入使用了 change buffer 機制,以後主機異常重啓,是否會丟失 change buffer 和數據?
不會丟失,雖然是隻更新內存,可是在事務提交的時候,咱們把 change buffer 的操做也記錄到 redo log 裏了,因此崩潰恢復的時候,change buffer 也能找回來。

merge 的過程是否會把數據直接寫回磁盤?
merge 的執行流程是這樣的:性能

  1. 從磁盤讀入數據頁到內存(老版本的數據頁);
  2. 從 change buffer 裏找出這個數據頁的 change buffer 記錄 (可能有多個),依次應用,獲得新版數據頁;
  3. 寫 redo log。這個 redo log 包含了數據的變動和 change buffer 的變動。

到這裏 merge 過程就結束了。這時候,數據頁和內存中 change buffer 對應的磁盤位置都尚未修改,屬於髒頁,以後各自刷回本身的物理數據。測試

2、MySQL爲何有時候會選錯索引?

優化器的邏輯

化器選擇索引的目的,是找到一個最優的執行方案,並用最小的代價去執行語句。在數據庫裏面,掃描行數是影響執行代價的因素之一。固然,掃描行數並非惟一的判斷標準,優化器還會結合是否使用臨時表、是否排序等因素進行綜合判斷。

掃描行數是怎麼判斷的?
MySQL 在真正開始執行語句以前,並不能精確地知道知足這個條件的記錄有多少條,而只能根據統計信息來估算記錄數。這個統計信息就是索引的「區分度」。顯然,一個索引上不一樣的值越多,這個索引的區分度就越好。 而一個索引上不一樣的值的個數,咱們稱之爲「基數」(cardinality)。也就是說,這個基數越大,索引的區分度越好。
咱們可使用 show index 方法,看到一個索引的基數。雖然這個表的每一行的三個字段值都是同樣的,可是在統計信息中,這三個索引的基數值並不一樣,並且其實都不許確。 優化

MySQL 是怎樣獲得索引的基數的呢? MySQL 採樣統計,若是把整張表取出來一行行統計,雖然能夠獲得精確的結果,可是代價過高了,因此只能選擇「採樣統計」。InnoDB 默認會選擇 N 個數據頁,統計這些頁面上的不一樣值,獲得一個平均值,而後乘以這個索引的頁面數,就獲得了這個索引的基數。

其實索引統計只是一個輸入,對於一個具體的語句來講,優化器還要判斷,執行這個語句自己要掃描多少行。接下來,咱們再一塊兒看看優化器預估的,這兩個語句的掃描行數是多少。線程

優化器爲何放着掃描 37000 行的執行計劃不用,卻選擇了掃描行數是 100000 的執行計劃呢?這是由於,若是使用索引 a,每次從索引 a 上拿到一個值, 都要回到主鍵索引上查出整行數據,這個代價優化器也要算進去的。

MySQL 選錯索引,是沒能準確地判斷出掃描行數。既然是統計信息不對,那就修正。 analyze table t 命令,能夠用來從新統計索引信息。咱們來看一下執行效果。

索引選擇異常和處理

一種方法是,採用 force index 強行選擇一個索引。
第二種方法就是,能夠考慮修改語句(語義的邏輯是相同的),引導 MySQL 使用咱們指望的索引。
第三種方法是,在有些場景下,咱們能夠新建一個更合適的索引,來提供給優化器作選擇,或刪掉誤用的索引。日誌

3、怎麼給字符串字段加索引?

使用前綴索引後,可能會致使查詢語句讀數據的次數變多。

使用前綴索引,定義好長度,就能夠作到既節省空間,又不用額外增長太多的查詢成本。

當要給字符串建立前綴索引時,有什麼方法可以肯定我應該使用多長的前綴呢?實際上,咱們在創建索引時關注的是區分度,區分度越高越好。由於區分度越高,意味着重複的鍵值越少。 所以,咱們能夠經過統計索引上有多少個不一樣的值來判斷要使用多長的前綴。
首先,使用下面這個語句,算出這個列上有多少個不一樣的值:
select count(distinct email) as L from SUser;
而後,依次選取不一樣長度的前綴來看這個值,好比咱們要看一下 4~7 個字節的前綴索引,能夠用這個語句:
select count(distinct left(email,4))as L4, count(distinct left(email,5))as L5, count(distinct left(email,6))as L6, count(distinct left(email,7))as L7,from SUser;

前綴索引對覆蓋索引的影響

使用前綴索引就用不上覆蓋索引對查詢性能的優化了

區分度很差的字符串怎麼建索引?

如身份證號碼
第一種方式是使用倒序存儲。存儲身份證號的時候把它倒過來存。
第二種方式是使用 hash 字段。能夠在表上再建立一個整數字段,來保存身份證的校驗碼,同時在這個字段上建立索引。
它們的相同點是,都不支持範圍查詢。

4、爲何MySQL會「抖」一下?

一條 SQL 語句,正常執行的時候特別快,可是有時也不知道怎麼回事,它就會變得特別慢,而且這樣的場景很難復現,它不僅隨機,並且持續時間還很短。
InnoDB 在處理更新語句的時候,只作了寫日誌( redo log)這一個磁盤操做。而後把內存裏的數據寫入磁盤的過程,術語就是flush。在這個 flush 操做執行以前,內存和硬盤數據是不一致的。
當內存數據頁跟磁盤數據頁內容不一致的時候,咱們稱這個內存頁爲「髒頁」。內存數據寫入到磁盤後,內存和磁盤上的數據頁的內容就一致了,稱爲「乾淨頁」。
平時執行很快的更新操做,其實就是在寫內存和日誌,而 MySQL 偶爾「抖」一下的那個瞬間,可能就是在刷髒頁(flush)。

什麼狀況會引起數據庫的 flush 過程呢?

  1. InnoDB 的 redo log 寫滿了。這時候系統會中止全部更新操做。 若是你從監控上看,這時候更新數會跌爲 0。
  2. 系統內存不足。當須要新的內存頁,而內存不夠用的時候,就要淘汰一些數據頁,空出內存給別的數據頁使用。若是淘汰的是「髒頁」,就要先將髒頁寫到磁盤。
    InnoDB 用緩衝池(buffer pool)管理內存, 緩衝池中的內存頁有三種狀態:尚未使用的;使用了而且是乾淨頁;使用了而且是髒頁。。
    而當要讀入的數據頁沒有在內存的時候,就必須到緩衝池中申請一個數據頁。這時候只能把最久不使用的數據頁從內存中淘汰掉:若是要淘汰的是一個乾淨頁,就直接釋放出來複用;但若是是髒頁呢,就必須將髒頁先刷到磁盤,變成乾淨頁後才能複用。
    一個查詢要淘汰的髒頁個數太多,會致使查詢的響應時間明顯變長。
  3. MySQL 認爲系統「空閒」的時候。
  4. MySQL 正常關閉的狀況。這時候,MySQL 會把內存的髒頁都flush 到磁盤上,這樣下次 MySQL 啓動的時候,就能夠直接從磁盤上讀數據,啓動速度會很快。

InnoDB 刷髒頁的控制策略

首先,你要正確地告訴 InnoDB 所在主機的 IO 能力,這樣 InnoDB 才能知道須要全力刷髒頁的時候,能夠刷多快。這就要用到 innodb_io_capacity 這個參數了,它會告訴 InnoDB 你的磁盤能力。這個值我建議你設置成磁盤的 IOPS。磁盤的 IOPS 能夠經過 fio 這個工具來測試。

想要儘可能避免「一下」這種狀況,**就要合理地設置 innodb_io_capacity 的值,而且平時要多關注髒頁比例,不要讓它常常接近 75%。**其中,髒頁比例是經過Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total 獲得的。

補充:mysql在準備刷一個髒頁的時候,若是這個數據頁旁邊的數據頁恰好是髒頁,就會把這個「鄰居」也帶着一塊兒刷掉。
在 InnoDB 中,innodb_flush_neighbors 參數就是用來控制這個行爲的,值爲 1 的時候會有上述的「連坐」機制,值爲 0 時表示不找鄰居,本身刷本身的。 找「鄰居」這個優化在機械硬盤時代是頗有意義的,能夠減小不少隨機 IO。而若是使用的是 SSD 這類 IOPS 比較高的設備的話,由於這時候 IOPS 每每不是瓶頸,因此能夠設置爲0。

相關文章
相關標籤/搜索