從I/O到索引的那些事

前言

大多數項目的查詢操做佔據了數據處理很大的比例,關於查詢的優化成爲了不少數據庫一直研究的重點。當前的數據庫產品一旦涉及到超大庫數據的查詢都會採用索引技術,如MySql、Oracle、SqlServer、Hive...在知足不一樣的產品特性和應用場景裏有着不一樣的實現方案。mysql

一般來講,索引的目的主要是提升查詢速度,不一樣數據庫對索引的技術細節作了很好的封裝,在實際應用中對於開發或維護人員來講是徹底透明的。然而,筆者認爲,深刻理解查詢技術對於開發者來講是面向大型數據處理的必要過程;對於架構人員來講,索引技術的充分理解每每能夠在數據處理方案的選擇上進行全面的分析,作出良好判斷。算法

本文從操做系統層面結合硬件I/O過程理解查詢所帶來的關鍵問題,從而引伸出索引查詢技術的設計思路,整個過程將對索引關於I/O層面的技術細節作出分析。sql

I/O的認識

計算機通過幾十年發展各方面性能都獲得了顯著提升,然而卻一直有個因素限制着計算機的高速運轉,這就是I/O的問題。數據庫

不少開發者彷佛並不對I/O有刻意的認識,但在數據處理領域這是一個繞不開的話題,關於I/O維基百科是這樣解釋的:緩存

I/O(英語: Input/ Output),即 輸入/輸出,一般指數據在 內部存儲器和外部存儲器或其餘周邊設備之間的輸入和輸出。I/O是信息處理系統(例如計算機)與外部世界(多是人類或另外一信息處理系統)之間的通訊。輸入是系統接收的信號或數據,輸出則是從其發送的信號或數據。

在瞭解I/O前,咱們須要認識的是,現代計算機,磁盤和內存一直佔據着主要角色,大部分持久化的數據都會選擇容量較大、成本較低的磁盤中,而內存則做爲高速緩存的角色爲計算機提供存儲支持。在數據查詢中,一次I/O能夠理解爲從磁盤亦或是從內存讀取一次數據的過程,在涉及到查詢性能的分析中,就必須對I/O的成本具有必定的敏感性。bash

什麼是I/O的成本?數據結構

計算機性能的大幅提高離不開存儲器和CPU在處理速度上的不斷提升,但關於性能提高的速度在CPU和存儲器間卻形成了必定的差距,CPU自1980年來在普通計算機中的處理速度已經實現了10000倍的提高,而磁盤只提高了30倍左右。這形成了一種現象,就是大多數時候CPU須要等待磁盤的運轉,一個完整的數據處理過程,磁盤每每佔據了主要的操做時間,在這裏咱們能夠把I/O的成本理解爲每一次從磁盤、內存讀取的時間消耗。架構

以前一篇文章【理解進程的存在】筆者分析了CPU處理運算的基本過程,從CPU每秒億萬次的時鐘週期中咱們具有了必定的感知,就是CPU對指令的處理速度大大超越了常規理解,底有多快?當前4核CPU的時鐘週期已經達到了0.4ns左右,也就是說每0.4ns就能完成一次指令操做,而內存一次訪問時間大概是9ns(建議理解下ns、ms這些維度的時間差距),儘管差了20來倍,但彷佛還維持在同一量級的水平上。磁盤就離譜了,機械磁盤當前還維持在29ms的訪問時間,和內存、CPU分別是百萬和千萬量級的巨大差別!磁盤相較於內存爲什麼在I/O中有如此糟糕的表現?參考以前文章【磁盤和內存的基本認識】的相關描述,咱們這能夠從存儲器的存儲原理和存儲介質兩方面作好了解。post

所以,數據處理的瓶頸每每在於I/O,而I/O的瓶頸每每在於驅動硬盤的過程,只有儘量減小對磁盤的I/O依賴,咱們才能從性能上完成突破。性能

須要清楚的是,磁盤較差的I/O表現不表明不能被市場接收。由於磁盤的制形成本相較於其它高速存儲器有着巨大優點,因此接下來很長一段時間磁盤的I/O依舊是一個須要面對的課題。

從OS層面看磁盤I/O過程

在硬件角度上咱們認識到存儲器自己帶來的侷限性,那麼大部分場景下關於查詢的優化將主要從軟件層面作好處理,不一樣操做系統在底層上都對I/O作了良好支持,量化I/O成本必須從OS層面進行細緻瞭解。

早期計算機系統的存儲層次只有三層:CPU寄存器、DRAM主存儲器(內存)和磁盤存儲,OS圍繞這三種存儲介質進行相應優化,因爲CPU和存儲器在發展過程當中性能差距不斷變大,目前計算機在寄存器和內存之間又置入了多層高速緩存存儲器,部分狀況下內存和磁盤中間還會置入SSD(固態硬盤)。當前OS利用必定的算法在緩存命中方面作好工做,儘量提升總體I/O的性能,但這並不妨礙咱們核心問題的分析,大多數狀況下,OS須要解決的問題依舊是數據從磁盤到內存的過程,爲了簡化分析模型,咱們將問題從內存、磁盤二者進行思考。

在磁盤中,數據是以塊爲單位進行管理的,每塊通常設定爲4Kb,咱們能夠抽象出磁盤就是不少塊按序號排列的存儲結構:


當OS須要獲取磁盤某個數據時,將產生一個I/O中斷,本次I/O中斷將帶上具體的塊號去驅動磁盤進行塊數據查找和讀取操做,這些操做都是以某塊做爲起點,每次讀取數據的最小單位也是4Kb,這意味着若是你獲取的數據小於4Kb或者數據放置在某塊特定偏移處,那麼磁盤依然會從該塊開始將整個塊內容傳遞到內存中:


對於小於4Kb的數據,若是恰好放置在某塊中,那麼每每只需對磁盤進行一次驅動,也就是一次I/O過程,但若是這數據恰好橫跨兩個塊,那麼就須要驅動磁盤讀取兩個塊數據了(下圖查詢目標內容須要讀取塊N、塊N+1):

所以,咱們應該儘可能避免上述狀況,將目標數據以塊爲單位進行對齊,這樣能減小磁盤I/O過程當中塊的讀取次數。

但若是目標數據自己就很大,必須佔用多個塊呢?顯然,假設目標數據連續佔用N個塊,那麼磁盤將進行N次讀取操做:


但若是目標數據並不順序存放,而是分散在各個塊區域:


一樣是進行三個塊讀取操做,但從磁盤的物理構造來說,磁盤必須進程兩次塊定位操做,也就是I/O中的磁頭定位過程。磁盤在讀取目標數據前必須將磁頭定位到指定的塊起始處,若是中間目標數據分散在塊的不一樣區域,那麼磁頭必須進行必定距離的物理旋轉工做,這顯然會佔用讀取時間,也就是順序讀和隨機讀的關鍵區別!所以,最理想的狀況是,目標數據在磁盤中的存放位置不只塊對齊並且是連續存放,至少要控制數據在磁盤的離散程度在較低水平。

CPU在執行任何指令時,但凡涉及到磁盤I/O,OS都會將數據先預讀到內存緩存空間,經過上文的介紹,內存的存取週期和CPU的處理週期在量級上並沒有太大區別,所以數據庫在管理數據方法上,更多的精力花在減小磁盤的I/O。

數據的管理和檢索

數據庫對不一樣表會有專門的文件管理,一個文件能夠理解爲記錄的一個序列,不一樣表在磁盤中的文件組織方式可能有必定區別。常規來說,諸如mysql的關係型數據庫是一種行式數據庫,表中每條記錄能夠理解爲一行,每行不一樣字段的內容在磁盤中是順序存放的,而不一樣行在磁盤的的位置則不必定是按序的,有可能分散在不一樣區域的塊中。

咱們考慮在數據庫中建立一個用戶表user:

CREATE TABLE `user` (
  `id` varchar(20),
  `name` varchar(20),
  `age` numeric(3,0)
)複製代碼

假設數據庫給每一個字段分配了最大容量,即id(20個字節)、name(20字節)、age(2字節),咱們建立了以下3條記錄:

記錄1 1 cary 25
記錄2 2 harry 26
記錄3 3 marry 23

咱們可知每行記錄都佔用了固定大小42Byte,一開始3條記錄仍是順序存放的:


當咱們執行查詢操做:

select * from user where id=2;複製代碼

顯然, 本次查詢將對應塊內容加載到內存後開始按行處理,篩選出全部id=2的數據,CPU執行模型以下 :

do begin
    for each row in user {
         if row.id=2 {
             select row;
         }
    }
end複製代碼

整個過程的I/O複雜度爲O(1)、CPU計算複雜度爲O(3)。如今咱們假設每行數據依然按序存放,但行數擴增爲100萬行:

記錄1 1 cary 25
記錄2 2 harry 26
記錄3 3 marry 23
.......
.......
.......
記錄999999 999999 joke 28
記錄1000000 1000000 zerui 26

在不考慮塊對齊的狀況下,100萬行數據將佔用max(1000000*42Byte/4Kb)=42000個塊,一樣執行上述sql查詢的狀況下,I/O的複雜度爲O(42000),CPU計算複雜度爲O(1000000)。在如此量級條件下機器處理壓力將大幅上升,而且隨着表數據的不斷增長,複雜度呈線性增長狀態,這絕對是不可接受的!

所以,必需要有一種技術來解決大量數據的檢索問題,咱們關注兩個核心需求,一個是減小磁盤的I/O、一個是下降CPU的處理複雜度,索引應運而生。

索引的介紹

咱們如今已經對數據查詢有了基本的成本概念,這個成本體如今I/O和CPU處理上,索引如何解決這兩個問題呢?

上文示例的user表數據結構中,咱們假設了行數據在磁盤中按id順序存放,當咱們按id條件進行數據檢索時,最簡單的方式其實能夠新增一個數據結構來作目標區間的定位:

id值 目標記錄塊號
1 N
10000 N+K
20000 N+2K
……
1000000 N+100K
 (N 、K 爲整數)

該結構描述了一個映射條件,左邊是表id屬性值,右邊是對應記錄在磁盤中的塊號,當咱們要查詢id=100000的記錄時,經過映射表咱們能夠預判目標記錄放置在磁盤塊區間[N+10K,N+11K],所以接下來OS只需驅動磁盤進行最多1K個塊的讀取操做,整個過程I/O複雜度最大爲O(1K),CPU處理複雜度最大爲O(10000),處理性能實現了10000倍的提升!

這樣簡單設計的數據結構咱們稱爲順序索引,索引信息一樣放置在磁盤空間管理,但凡對id字段的檢索,數據庫會優先考慮從該索引表進行目標塊定位,而後從目標塊中檢索目標信息。按照上訴設計的索引結構,區間劃分的粒度爲10000,可知總共有100行,假設每行記錄咱們設定10Byte空間存儲,那麼整個索引結構將佔用1000Byte,不到1Kb,所以有些時候咱們能夠直接將索引結構預加載到內存,這樣關於索引的查詢過程將不涉及到磁盤的I/O消耗,這爲咱們優化查詢速度提供了新的思路!

可是,順序索引結構的簡單是基於行記錄在磁盤存儲上的苛刻要求下所支持的,行記錄必須按照id值在磁盤中按序存放。現實業務環境下,數據有較高的複雜度和變更頻率,一些行數據會被刪除,騰出的空間會被其它後續插入的行給佔用,後續插入的記錄在磁盤中也可能呈現隨機狀態。而一旦目標行數據不按順序存放,那麼順序索引關於塊區間的劃分將毫無心義!

記錄1 1 cary 25
記錄2 2 harry 26
記錄3 897 karry 24 原來id=3的記錄被刪除,後續插入了id爲897的記錄

這裏,咱們介紹新的一種數據結構:二叉樹,當目標數據在磁盤的存放呈現離散隨機狀態時,咱們依然但願能很好地實現快速檢索。

在二叉樹的基礎上咱們增長以下條件:左子節點小於根節點,右子節點大於或等於根節點,假設user表有id爲二、三、五、六、七、8的記錄,那麼將有以下形態:


由此構建的索引結構包含了id屬性全部值的狀況,對100萬行的數據來講,該索引結構就有100萬個節點,每一個節點還包含了該id對應記錄在磁盤中的具體位置,當查詢id=3的記錄時,咱們發現通過根節點的判斷後,左指針恰好指向節點爲3的內容,由此可獲取記錄最終的磁盤位置。

那麼如何分析二叉樹索引結構帶來的成本問題?經過該結構規則,其實至關於對記錄進行了二分法操做,從數學的角度講,每次判斷都是一次半數的數量級檢索。上述示例總共6條記錄咱們最多隻需進行log2N=6即N=3的判斷次數,對於100萬個節點的索引結構,咱們查詢id最多須要log2N=1000000即N=20的節點獲取,也就是磁盤I/O複雜度最大爲O(20),假設目標記錄增加爲1000萬行,I/O複雜度也不過是O(23),I/O複雜度和目標記錄行數呈現出的指數關係極大緩解了I/O成本上升的趨勢!

I/O複雜度和二叉樹的高度是存在直接關係的,100萬個節點須要構造最多20層高度的二叉樹,假設每一個節點內容包含:左指針(4Byte)、id值(8Byte)、右指針(4Byte)、目標記錄指針(4Byte),總共佔據20Byte,那麼索引將至少佔據20Byte*1000000=20Mb的存儲空間,最理想的狀況是將索引結構直接加載到內存,這樣只需在內存消耗O(20),但區別於不一樣的機器性能,20Mb對於珍貴的內存資源仍是稍顯奢侈,可否在磁盤上減小索引層面的I/O次數呢?

咱們在二叉樹索引構造中,每次I/O的成本帶來的是一半數據的過濾功效,在此基礎上咱們但願能更大限度地提高下過濾的量級。咱們把思路擴展到n叉樹中,對於n叉樹咱們能獲得lognN的指數模型,其中n表明每一個節點的下級指針個數,假設n=10那麼log10N=1000000就有N=5即O(5)的複雜度,這相對於O(20)又是4倍的I/O性能提高,關於n叉數咱們設計了以下形態:


每一個節點將有四個指針,每一個指針一樣遵循上訴二叉樹的設定規則:id值左邊指針指向小於該id的下級範圍,id值右邊指針指向大於該id值的下級範圍。整個n叉樹咱們直觀上明顯更「胖」了,意味着一次節點I/O後咱們對目標數據能作出更大量級的細分!經過上文關於磁盤I/O的介紹,咱們瞭解到磁盤以每一個塊做爲最小操做單位,所以當前不少數據庫產品在設計n叉樹時有意地將一個塊大小做爲一個葉節點的大小,假設上訴葉節點不一樣元素佔用狀況爲:左右指針各佔4Byte,id值8Byte,目標記錄指針4Byte,那麼一個4Kb的磁盤塊將大體能夠容納250個下級指針,100萬行目標記錄只需log250N=1000000即N=3的I/O次數,充分提高了每次節點I/O帶來的檢索效用!

所以,瞭解當前數據庫表採用的索引構造,經過數學問題咱們就能夠很好的對實際項目中的一些查詢作好成本分析,特別是大數據環境下,在預估查詢時間時能很好地得出相應的性能表現。

當前不一樣數據庫產品在設計索引時考慮了實際的應用場景,數據庫表文件在磁盤的存放方式決定了須要採用怎樣的索引結構。對於磁盤中按序存放的目標數據每每經過順序索引就能實現快速檢索,像一些大數據產品自己專一於查詢性能,表數據每每只能進行數據追加而不支持刪改操做,每次表數據的刪改變更必須進行全局的格式化,這就是爲了要保證數據在磁盤中的按序存放。而諸如mysql事務性數據庫,針對的更可能是常規業務操做,數據的增刪改查是必須充分支持的,數據在磁盤的分佈較爲複雜,索引的設計上每每採用的是樹形結構。

後語

關於索引的設計是一個較爲複雜的過程,本文更可能是但願在大腦中構造出數據查詢中I/O成本分析的思考模型。索引極大的提高了數據庫數據的查詢速度,但咱們也應該清楚的認識到,關於索引自己結構的維護也是一個不小的工程,不一樣索引結構自己的複雜性直接關係到索引結構自己維護過程當中須要承擔的I/O複雜度和CPU計算複雜度,關於索引的構建和維護能夠另行查閱相關資料進行了解。

相關文章
相關標籤/搜索