1. 程式人生 > >MySQL 學習 --- 數據結構和索引

MySQL 學習 --- 數據結構和索引

start unsigned tin blog 查找算法 code 怎麽 磁盤存儲 size

本文參考了多篇文章集成的筆記,希望各位學習之前可以閱讀以下參考資料先

概述

文章分幾個部分 ;第一部分介紹了B-Tree 和 B+Tree 這種數據結構作為索引;第二部分介紹索引的最左前綴原則和覆蓋索引 ;第三部分講了一下主鍵優化及 explain 的相關資料;主要是要結合實例去理解,不然也好難記憶這些概念。同時MySQL 官方的 DOC 真的是大大的良心,可以在實踐使用過程中遇到問題,查詢資料,了解概念過後,作為系統學習的第一手資料!


數據結構

我們知道MySQL InnoDB 引擎和MyISAM 引擎都是以 B+ Tree 作為底層數據結構的,這種數據結構的目的就是建立索引,使我們可以通過索引更快地找到數據。

MySQL官方對索引的定義為:索引(Index)是幫助MySQL高效獲取數據的數據結構。提取句子主幹,就可以得到索引的本質:索引是數據結構。

技術分享圖片


上圖是個索引的示例,左邊是數據庫中的表數據,而右邊是一個二叉樹,每個節點對應於一個行地址位置,我們要找左邊表中的最後一行,我們先通過二叉樹,由34開始找到 23 ,再由23找到 “0xD1”這個行地址,獲取數據。當然實際中我們不是使用二叉樹,而是使用B - Tree 或是 B + Tree。下面我們先來看一下是什麽是 B- Tree 和B + Tree 。

B - Tree 和 B + Tree

技術分享圖片


可以看到上面是 B – Tree 的定義,或是這樣描述

下面來具體介紹一下B-樹(Balance Tree),一個m階的B樹具有如下幾個特征:

  1. 根結點至少有兩個子女。
  2. 每個中間節點都包含k-1個元素和k個孩子,其中 m/2 <= k <= m
  3. 每一個葉子節點都包含k-1個元素,其中 m/2 <= k <= m
  4. 所有的葉子結點都位於同一層。
  5. 每個節點中的元素從小到大排列,節點當中k-1個元素正好是k個孩子包含的元素的值域分劃。

直觀的例子就是如下 :

技術分享圖片

我們再來看一下查找一個元素,邏輯過程應該是怎麽樣的?下面是查找算法的偽代碼,出處

    BTree_Search(node, key) {
        if(node == null) return null;
        foreach(node.key)
        {
            if(node.key[i] == key) return node.data[i];
                if(node.key[i] > key) return BTree_Search(point[i]->node);
        }
        return BTree_Search(point[i+1]->node);
    }
    data = BTree_Search(root, my_key);

一個度為d的B-Tree,設其索引N個key,則其樹高h的上限為logd((N+1)/2),檢索一個key,其查找節點個數的漸進復雜度為O(logdN)。從這點可以看出,B-Tree是一個非常有效率的索引數據結構。

關於其他關於 B-Tree 的操作,可以看 這一篇文章


B+Tree

B+Tree 樹 是 B –Tree 的變種,它的定義如下 :

一個m階的B+樹具有如下幾個特征:

  1. 有k個子樹的中間節點包含有k個元素(B樹中是k-1個元素),每個元素不保存數據,只用來索引,所有數據都保存在葉子節點。
  2. 所有的葉子結點中包含了全部元素的信息,及指向含這些元素記錄的指針,且葉子結點本身依關鍵字的大小自小而大順序鏈接。
  3. 所有的中間節點元素都同時存在於子節點,在子節點元素中是最大(或最小)元素。

記住三點 B+Tree 的特征有利於我們理解下面講的內容。文字似乎很難理解,我們來看一下示例 :


技術分享圖片


那麽設計成這種數據結構有什麽好處呢?

  1. 單一節點存儲更多的元素,使得查詢的IO次數更少。
  2. 所有查詢都要查找到葉子節點,查詢性能穩定。
  3. 所有葉子節點形成有序鏈表,便於範圍查詢。(例如上圖要查找 3 到 8 之間的數據)


為什麽使用B-Tree(B+Tree)

直接原因就是和計算機組成原理有關。下面是一段簡潔的概括

一般來說,索引本身也很大,不可能全部存儲在內存中,因此索引往往以索引文件的形式存儲的磁盤上。這樣的話,索引查找過程中就要產生磁盤I/O消耗,相對於內存存取,I/O存取的消耗要高幾個數量級,所以評價一個數據結構作為索引的優劣最重要的指標就是在查找過程中磁盤I/O操作次數的漸進復雜度。換句話說,索引的結構組織要盡量減少查找過程中磁盤I/O的存取次數。

這一篇文章 ,也給出了使用 B + Tree 的動機,主要是基於以下幾個事實 :

  • 不同的存儲設備讀取速度差異過大
  • 從磁盤中讀寫 1 B , 與讀寫 1 KB 幾乎一樣快
  • 磁盤的預讀

局部性原理與磁盤預讀

下面分別是局部原理和磁盤預讀的介紹。

由於存儲介質的特性,磁盤本身存取就比主存慢很多,再加上機械運動耗費,磁盤的存取速度往往是主存的幾百分分之一,因此為了提高效率,要盡量減少磁盤I/O。為了達到這個目的,磁盤往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個字節,磁盤也會從這個位置開始,順序向後讀取一定長度的數據放入內存。這樣做的理論依據是計算機科學中著名的局部性原理:

當一個數據被用到時,其附近的數據也通常會馬上被使用。

程序運行期間所需要的數據通常比較集中。

預讀的長度一般為頁(page)的整倍數。頁是計算機管理存儲器的邏輯塊,硬件及操作系統往往將主存和磁盤存儲區分割為連續的大小相等的塊,每個存儲塊稱為一頁(在許多操作系統中,頁得大小通常為4k),主存和磁盤以頁為單位交換數據。當程序要讀取的數據不在主存中時,會觸發一個缺頁異常,此時系統會向磁盤發出讀盤信號,磁盤會找到數據的起始位置並向後連續讀取一頁或幾頁載入內存中,然後異常返回,程序繼續運行。

B + Tree 索引的性能分析

以下分析來自 : http://blog.codinglabs.org/articles/theory-of-mysql-index.html

根據B-Tree的定義,可知檢索一次最多需要訪問h個節點。數據庫系統的設計者巧妙利用了磁盤預讀原理,將一個節點的大小設為等於一個頁,這樣每個節點只需要一次I/O就可以完全載入。為了達到這個目的,在實際實現B-Tree還需要使用如下技巧:

每次新建節點時,直接申請一個頁的空間,這樣就保證一個節點物理上也存儲在一個頁裏,加之計算機存儲分配都是按頁對齊的,就實現了一個node只需一次I/O。B-Tree中一次檢索最多需要h-1次I/O(根節點常駐內存),漸進復雜度為O(h)=O(logdN)。

一般實際應用中,出度d是非常大的數字,通常超過100,因此h非常小(通常不超過3)。綜上所述,用B-Tree作為索引結構效率是非常高的。

而紅黑樹這種結構,h明顯要深的多。由於邏輯上很近的節點(父子)物理上可能很遠,無法利用局部性,所以紅黑樹的I/O漸進復雜度也為O(h),效率明顯比B-Tree差很多。上文還說過,B+Tree更適合外存索引,原因和內節點出度d有關。從上面分析可以看到,d越大索引的性能越好,而出度的上限取決於節點內key和data的大小:

技術分享圖片

floor表示向下取整。由於B+Tree內節點去掉了data域,因此可以擁有更大的出度,擁有更好的性能。


MySQL索引實現

MyISAM 索引實現

MyISAM引擎使用B+Tree作為索引結構,葉節點的data域存放的是數據記錄的地址。下圖是MyISAM索引的原理圖:


技術分享圖片


這裏設表一共有三列,假設我們以Col1為主鍵,則上圖是一個MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件僅僅保存數據記錄的地址。在MyISAM中,主索引和輔助索引(Secondary key)在結構上沒有任何區別,只是主索引要求key是唯一的,而輔助索引的key可以重復。如果我們在Col2上建立一個輔助索引,則此索引的結構如下圖所示:

技術分享圖片

MyISAM 這種索引方式(data中放的是行地址引用,而不是真實數據)稱為 “非聚集”


InnoDB 索引實現

InnoDB 也是使用 B+Tree 作為 索引結構 ,最大的區別在於 葉子結點的保存的完整的數據記錄而不是地址引用,而 MyISAM 的做法是data 保存地址引用,地址和數據文件分開的方式。


技術分享圖片



InnoDB 這種索引的方式稱之為 聚集索引,因為InnoDB的數據文件本身要按主鍵聚集,所以InnoDB要求表必須有主鍵(MyISAM可以沒有),如果沒有顯式指定,則MySQL系統會自動選擇一個可以唯一標識數據記錄的列作為主鍵,如果不存在這種列,則MySQL自動為InnoDB表生成一個隱含字段作為主鍵,這個字段長度為6個字節,類型為長整形。

第二個與MyISAM索引的不同是InnoDB的輔助索引data域存儲相應記錄主鍵的值而不是地址。換句話說,InnoDB的所有輔助索引都引用主鍵作為data域。例如,下圖為定義在Col3上的一個輔助索引:


技術分享圖片


例如數據庫中表 Bob 一行對應的主鍵值為 15 。


最左前綴原則

最左前綴原則使用的場景是進行復合索引(存在兩個或兩個以上的構成的索引) 查詢的時候。

來看一下下面的例子 :

CREATE TABLE `student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `cid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `name_cid_INX` (`name`,`cid`),
  KEY `name_INX` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8



//建索引
create INDEX name_cid_INX ON student(name,cid);

create INDEX name_INX ON student(name);


再看一下三條語句


EXPLAIN  SELECT * FROM student WHERE    name=‘小紅‘;

技術分享圖片

EXPLAIN  SELECT * FROM student WHERE   cid=1;

技術分享圖片

EXPLAIN SELECT * FROM student WHERE   cid=1 AND name=‘小紅‘; 

技術分享圖片

這三句會產生不同的效果。我們先來分析 ,type 為 ref 和 index 的區別

作者:沈傑
鏈接:https://www.zhihu.com/question/36996520/answer/93256153
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。


index:這種類型表示是mysql會對整個該索引進行掃描。要想用到這種類型的索引,對這個索引並無特別要求,只要是索引,或者某個復合索引的一部分,mysql都可能會采用index類型的方式掃描。但是呢,缺點是效率不高,mysql會從索引中的第一個數據一個個的查找到最後一個數據,直到找到符合判斷條件的某個索引。所以對於你的第一條語句:

EXPLAIN SELECT * FROM student WHERE   cid=1;

判斷條件是cid=1,而cid是(name,cid)復合索引的一部分,沒有問題,可以進行index類型的索引掃描方式。explain顯示結果使用到了索引,是index類型的方式。

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

ref:這種類型表示mysql會根據特定的算法快速查找到某個符合條件的索引,而不是會對索引中每一個數據都進行一 一的掃描判斷,也就是所謂你平常理解的使用索引查詢會更快的取出數據。而要想實現這種查找,索引卻是有要求的,要實現這種能快速查找的算法,索引就要滿足特定的數據結構。簡單說,也就是索引字段的數據必須是有序的,才能實現這種類型的查找,才能利用到索引。

再來看一下復合索引的索引索引排序,以上面例子

技術分享圖片

可以看到 name 作為第一個索引,是按它先排序的,而cid 此時的索引排序並不能產生作用,那麽為什麽第三語句會用到 use index 呢?依然會使用到這個復合索引呢?不是說應該是最左前綴匹配原則嗎?是的,但是MySQL 底層做了優化,使得先按照 name 索引後再找 cid .


另外一個例子

例子來自參考資料

索引也不是建的多就好,畢竟生成索引也需要消耗內存空間,參考資料就提到了一種不建議建立索引的情況 : 索引的選擇性較低。所謂索引的選擇性(Selectivity),是指不重復的索引值(也叫基數,Cardinality)與表記錄數(#T)的比值:

Index Selectivity = Cardinality / #T

這一點很容易去理解,要是索引是重復的,那麽要查詢出數據,必定花費更多的時間,所以要是可能提高選擇性,那麽查詢到速率一定更高。

假設存在一個表,存在 first name 和 second name 字段, 它們的選擇性如下 :

    SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees.employees;
    +-------------+
    | Selectivity |
    +-------------+
    |      0.0042 |
    +-------------+
    SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM employees.employees;
    +-------------+
    | Selectivity |
    +-------------+
    |      0.9313 |
    +-------------+

<first_name>顯然選擇性太低,<first_name, last_name>選擇性很好,但是first_name和last_name加起來長度為30,有沒有兼顧長度和選擇性的辦法?可以考慮用first_name和last_name的前幾個字符建立索引,例如<first_name, left(last_name, 3)>,看看其選擇性:

SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.7879 |
+-------------+
選擇性還不錯,但離0.9313還是有點距離,那麽把last_name前綴加到4:
SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees.employees;.+-------------+
| Selectivity |
+-------------+
|      0.9007 |
+-------------+



這時選擇性已經很理想了,而這個索引的長度只有18,比<first_name, last_name>短了接近一半,我們把這個前綴索引 建上:

ALTER TABLE employees.employees
ADD INDEX `first_name_last_name4` (first_name, last_name(4));


此時再執行一遍按名字查詢,比較分析一下與建索引前的結果:

SHOW PROFILES;
+----------+------------+---------------------------------------------------------------------------------+
| Query_ID | Duration   | Query                                                                           |
+----------+------------+---------------------------------------------------------------------------------+
|       87 | 0.11941700 | SELECT * FROM employees.employees WHERE first_name=‘Eric‘ AND last_name=‘Anido‘ |
|       90 | 0.00092400 | SELECT * FROM employees.employees WHERE first_name=‘Eric‘ AND last_name=‘Anido‘ |
+----------+------------+---------------------------------------------------------------------------------+


性能的提升是顯著的,查詢速度提高了120多倍。


InnoDB的主鍵選擇與插入優化

在使用InnoDB存儲引擎時,如果沒有特別的需要,請永遠使用一個與業務無關的自增字段作為主鍵。

我們前面說的數據結構是 B+Tree ,數據記錄本身被存於主索引(一顆B+Tree)的葉子節點上。這就要求同一個葉子節點內(大小為一個內存頁或磁盤頁)的各條數據記錄按主鍵順序存放,因此每當有一條新的記錄插入時,MySQL會根據其主鍵將其插入適當的節點和位置,如果頁面達到裝載因子(InnoDB默認為15/16),則開辟一個新的頁(節點)。如果表使用自增主鍵,那麽每次插入新的記錄,記錄就會順序添加到當前索引節點的後續位置,當一頁寫滿,就會自動開辟一個新的頁。如下圖所示:


技術分享圖片


如果使用非自增主鍵(如果身份證號或學號等),由於每次插入主鍵的值近似於隨機,因此每次新紀錄都要被插到現有索引頁得中間某個位置:

技術分享圖片


總的來說就是自增鍵更加適應了 B+ Tree 這種結構,相對於隨機插入,分頁重組節點的概率更低,性能更加好。


覆蓋索引

覆蓋索引是select的數據列只用從索引中就能夠取得,不必讀取數據行,換句話說查詢列要被所建的索引覆蓋。索引的字段不只包含查詢列,還包含查詢條件、排序等。這篇文章遇到的case解決方法就是用到了覆蓋索引。

CREATE TABLE `t_order` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `order_code` char(12) NOT NULL,
  `order_amount` decimal(12,2) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uni_order_code` (`order_code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 


//查詢語句
select order_code, order_amount from t_order order by order_code limit 1000;

下面是 explain 後的參數 :

技術分享圖片

可以看到 Extra : Using fileSort ,那為什麽MySQL沒有利用索引(uni_order_code)掃描完成查詢呢?因為MySQL認為這個場景利用索引掃描並非最優的結果。下面來看一下兩種方式掃描方式 :

1. 全表掃描、文件排序:

雖然是全表掃描,但是掃描是順序的(不管機械硬盤還是SSD順序讀寫性能都是高的),並且數據量不是特別大,所以這部分消耗的時間應該不是特別大,主要的消耗應該是在排序上。

2. 利用索引掃描、利用索引順序:

uni_order_code是二級索引,索引上保存了(order_code,id),每掃描一條索引需要根據索引上的id定位(隨機IO)到數據行上讀取order_amount,需要1000次隨機IO才能完成查詢,而機械硬盤隨機IO的效率是極低的(機械硬盤每秒尋址幾百次)。

根據我們自己的分析選擇全表掃描相對更優。如果把limit 1000改成limit 10,則執行計劃會完全不一樣。既然我們已經知道是因為隨機IO導致無法利用索引,那麽有沒有辦法消除隨機IO呢?

有,覆蓋索引。

ALTER TABLE `t_order` 
ADD INDEX `idx_ordercode_orderamount` USING BTREE (`order_code` ASC, `order_amount` ASC);


技術分享圖片


補充

我們從上面性能優化上看,explain 更多時候給我們提供了很大的幫助,下面這兩個鏈接可以進一步地了解關於 explain 的信息 :

  • https://dev.mysql.com/doc/refman/5.7/en/explain-output.html (官方文檔)
  • https://dev.mysql.com/doc/workbench/en/wb-tutorial-visual-explain-dbt3.html (官方文檔)


總結

  • B+Tree 作為索引和計算甲組合原理相關,和減少 IO次數有關
  • 復合索引需要註意的是最左前綴原則
  • 某些情況下可以適用覆蓋索引來優化 SQL
  • explain 是個好工具
  • 參考資料的內容要好好看!


參考資料

  • http://blog.codinglabs.org/articles/theory-of-mysql-index.html (極力推薦一看!!)
  • https://zhuanlan.zhihu.com/p/40820574
  • https://www.zhihu.com/question/36996520/answer/93256153
  • https://my.oschina.net/loujinhe/blog/1528233
  • https://dev.mysql.com/doc/refman/5.7/en/explain-output.html (官方文檔)
  • https://dev.mysql.com/doc/workbench/en/wb-tutorial-visual-explain-dbt3.html (官方文檔)

MySQL 學習 --- 數據結構和索引