Schema與數據類型優化
選擇優化的數據類型
有幾個簡單的原則:
- 更小的通常更好
一般情況下使用可以正確存儲數據的最小數據類型。 - 簡單的更好
例如整型比字符操作代價更低。應當使用Mysql的日期類型而不是字符串,應當用整型存儲IP地址 - 儘量避免NULL
查詢中如果包含NULL的列,對於Mysql來說更難優化,這樣使得索引,索引統計,值都比較複雜。NULL的列會使用更多的存儲空間,在Mysql裏也需要特殊處理。當可爲NULL的列被索引時,每個索引記錄需要一個額外的字節
整數類型
無符號的數字上限可以提高一倍
爲整數類型指定寬度,如INT(11),不會限制值的合法範圍,只是規定了Mysql的一些交互工具(命令行或客戶端)用來顯示的字符個數。對於存儲和計算來講,int(1) 和 int(20)是相同的。
實數類型
浮點類型在存儲同樣範圍的值時,通常比Decimal使用更少的空間,Float使用4個字節,Double使用8個字節相比Float有更高的精度和更大的範圍。這裏能選擇的是存儲類型,Mysql內部使用Double作爲內部浮點計算的類型。
字符串類型
CHAR和VARCHAR
VARCHAR節省了存儲空間,如果行佔用存儲空間增長,並且在頁內沒有更多的空間存儲,MyISAM拆成不同的片段存儲,InnoDB則需要分裂頁來使行可以放進頁內。
下列情況使用Varchar是合適的:
- 字符串最大長度比平均長度大很多;
- 列的更新很少,所以碎片不是問題;
- 使用了UTF-8字符集,每個字符都使用不同的字節數進行存儲。
InnoDB把過長的VARCHAR存儲爲BLOB
CHAR是定長的,會刪除末尾的空格。CHAR(1)需要一個字節,VARCHAR(1)需要2個字節,因爲還需要多一個字節存儲長度。
類似的還有BINNARY和VARBINARY,填充使用的\0(0字節)
BLOB和TEXT
都是爲了存儲很大的數據設計的字符串數據類型,分別採用二進制和字符方式存儲。不同在於BLOB存儲的是二進制數據,沒有排序規則或者字符集。
排序也只是對每個列的max_sort_length字節而不是整個字符串排序。
查詢如果涉及BLOB,服務器不能在內存臨時表中存儲BLOB,必須要使用磁盤臨時表,無論它多小。
日期和時間類型
DATETIME可以存儲1001到9999年,精度爲秒,與時區無關,使用8個字節的存儲空間。TIMESTAMP保存了1970年1月1日以來的秒數。只使用4個字節的存儲空間。從1970到2038年。
位數據類型
這些類型,不管底層存儲格式和處理方式如何,從技術上來說都是字符串類型。
BIT
5.0之前BIT是TINYINT的同義詞。之後則完全不同。MyISAM會打包所有的BIT列,InnoDB和Memory使用足夠存儲最小整數類型來存放BIT,所以不能節省存儲空間。Mysql把BIT當作字符串類型而不是數字,會造成一些混亂。例如 a bit(8),值爲b'00111001'二進制等於57(ascii顯示值等於9),a=9,a+0=57。應該謹慎使用,如果想存儲true/false,可以使用CHAR(0)
選擇標識符(identifier)
整數類型是最好的選擇,很快並且可以使用AUTO_INCREMENT。避免使用字符串作爲標識列,很耗空間,通常比數字類型慢,MyISAM默認對字符串使用壓縮索引,會導致查詢慢很多。
- 隨機值如MD5,SHA1,UUID會導致INSERT和一些SELECT語句變慢,因爲可能導致隨機寫入索引不同位置,導致頁分裂,磁盤隨機訪問,對於聚簇存儲引擎產生聚簇索引碎片。
- SELECT語句變慢因爲邏輯上相鄰的行會分佈在磁盤和內存的不同地方。
- 隨機值導致緩存對所有類型的查詢語句效果都很差。
Scheme設計中的陷阱
- 太多的列
Mysql的存儲引擎API工作時需要在服務器層和存儲引擎層之間通過行緩衝格式拷貝數據,然後在服務器層將緩衝內容解碼成各個列。從行緩衝中將編碼過的列轉換成行數據結構的操作代價是非常高的。非常寬的表可能會使得CPU佔用非常高。 - 太多的關聯
“實體-屬性-值”(EAV)設計模式在Mysql下不能靠譜的工作,限制了每個關聯操作最多隻能有61張表。單個查詢最好在12個表內做關聯。 - 全能的枚舉
枚舉列表增加數據需要使用到ALTER TABLE,若不是加在最後可能會有影響 - 變相的枚舉
範式和反範式
在範式化的數據庫中,每個事實數據會出現並且只出現一次,相反,在反範式化的數據庫中,信息是冗餘的。
第一範式
確保數據表中每列(字段)的原子性。
如果數據表中每個字段都是不可再分的最小數據單元,則滿足第一範式。
例如:user用戶表,包含字段id,username,password第二範式
在第一範式的基礎上更進一步,目標是確保表中的每列都和主鍵相關。
如果一個關係滿足第一範式,並且除了主鍵之外的其他列,都依賴於該主鍵,則滿足第二範式。
例如:一個用戶只有一種角色,而一個角色對應多個用戶。則可以按如下方式建立數據表關係,使其滿足第二範式。
user用戶表,字段id,username,password,role_id
role角色表,字段id,name
用戶表通過角色id(role_id)來關聯角色表第三範式
在第二範式的基礎上更進一步,目標是確保表中的列都和主鍵直接相關,而不是間接相關。
例如:一個用戶可以對應多個角色,一個角色也可以對應多個用戶。則可以按如下方式建立數據表關係,使其滿足第三範式。
user用戶表,字段id,username,password
role角色表,字段id,name
user_role用戶-角色中間表,id,user_id,role_id
像這樣,通過第三張表(中間表)來建立用戶表和角色表之間的關係,同時又符合範式化的原則,就可以稱爲第三範式。反範式化
反範式化指的是通過增加冗餘或重複的數據來提高數據庫的讀性能。
例如:在上例中的user_role用戶-角色中間表增加字段role_name。
反範式化可以減少關聯查詢時,join表的次數。
範式的優點
- 範式化的更新操作更快
- 更新需要變更的數據更少
- 表比較小,可以更好放在內存裏
缺點是通常需要關聯,代價相對昂貴,也可能使得一些索引策略無效。
反範式的優點
避免關聯
查詢相對高效(當索引合理)
創建高性能索引
索引可以包含一個或多個列,如果索引包含多個列,那列的順序也十分重要,因爲Mysql只能最高效的使用索引的最左前綴列。
B-Tree的索引列是順序組織存儲的,很適合查找範圍數據。適用於全鍵值、鍵值範圍或鍵前綴查找。
紅黑樹是一種含有紅黑結點並能自平衡的二叉查找樹。它必須滿足下面性質:
性質1:每個節點要麼是黑色,要麼是紅色。
性質2:根節點是黑色。
性質3:每個葉子節點(NIL)是黑色。
性質4:每個紅色結點的兩個子結點一定都是黑色。
性質5:任意一結點到每個葉子結點的路徑都包含數量相同的黑結點。
從性質5又可以推出:
性質5.1:如果一個結點存在黑子結點,那麼該結點肯定有兩個子結點
哈希索引(hash index)只有精確匹配索引所有列的查詢纔有效。只包含哈希值和行指針,不存儲字段值,所以不能避免讀取行。
並不是按照索引值順序存儲,所以無法用於排序。
也不支持部分索引列匹配查找。只支持等值查詢,不支持範圍查詢。
高性能的索引策略
獨立的列才能使用到索引,列不能使用操作符或者表達式
多列索引,當使用到多個單列索引時,會進行多個索引的聯合操作(索引合併)
選擇合適的索引列順序
正確的順序依賴於使用該索引的查詢,並且同時需要考慮如何更好地滿足排序和分組的需要。
在一個多列B-Tree索引中,索引列的順序意味着索引首先按照最左列進行排序,其次是第二列。
聚簇索引
並非一種單獨的索引類型,而是一種數據存儲方式。InnoDB在同一個結構中保存了B-Tree索引和數據行。
InnoDB使用主鍵聚集數據,如果沒有定義主鍵,會選擇一個唯一的非空索引代替,如果沒有這樣的索引,會隱式定義一個主鍵作爲聚簇索引。InnoDB只聚集同一個頁面的記錄。
優點:
- 把相關數據保存再一起。
- 數據訪問更快
- 使用覆蓋索引掃描的查詢可以直接使用節點中的主鍵值。
缺點:
- 插入速度依賴於插入順序,如果不是按照主鍵加載數據,加載完成後最好使用OPTIMIZE TABLE重新組織表
- 更新聚簇索引的代價很高,因爲會將被更新的行移動到新位置
- 插入新航或者主鍵更新需要移動行時,可能面臨“頁分裂(Page Split)”問題
- 可能導致全表掃描變慢,尤其是行比較稀疏
- 二級索引(非聚簇索引)可能比想象的要更大,因爲葉子節點包含了引用行的主鍵列。
- 二級索引需要兩次索引查找,而不是一次
覆蓋索引
如果索引的葉子節點中已經包含要查詢的數據,那麼還有什麼必要再回表查詢呢?所以一個索引包含(或者覆蓋)所有需要查詢的字段的值,我們就稱之爲覆蓋索引。
索引排序
只有索引的列順序和orderby的順序完全一致,並且列的正序,逆序都一樣時,才能使用索引對結果進行排序。如果查詢需要關聯多張表,則只有當orderby的引用字段全部爲第一個表時,才能使用索引進行排序。
索引和數據的碎片化
B-Tree索引可能會碎片化。
表的數據存儲也可能碎片化:
行碎片
這種碎片指的時數據行被存儲到多個地方的多個片段中。即使只查詢一行記錄,也會導致性能下降。
行間碎片
邏輯上順序的頁,或者行再磁盤上不是順序存儲的。行間碎片對諸如全表掃描和聚簇索引掃描之類的操作有很大影響。
剩餘空間碎片
指數據頁中有大量的空餘空間,會導致服務器讀取大量不需要的數據造成浪費。
查詢性能優化
查詢的聲明週期大致按照順序:
從客戶端,到服務器,然後在服務器上進行解析,生成執行計劃,執行,並返回結果給客戶端。執行時最重要的階段,包含了大量爲檢索數據到存儲引擎的調用以及調用後的數據處理,包括排序,分組等。
慢查詢基礎:優化數據訪問
是否請求了不需要的數據
- 查詢不需要的記錄
- 查詢不需要的列 (多表關聯 * )
- 總是取出全部列(select * )
- 重複查詢相同的數據
是否在掃描額外的記錄
衡量查詢開銷的三個指標如下:
- 響應時間
- 掃描的行數
- 返回的行數
響應時間是 服務時間 和 排隊時間 之和。
掃描的行數和返回的行數理想情況下應該是相同的,一般在1:1到10:1之間
掃描的行數和訪問類型:在EXPAIN語句中的type列反應了訪問類型。訪問類型有很多中,包括全表掃描,索引掃描,範圍掃描,唯一索引查詢,常數引用等。這些速度是從慢到快,掃描行數也是從多到少。
重構查詢的方式
一個複雜查詢還是多個簡單查詢
Mysql支持多個簡單查詢,一個通用服務器上可以支持每秒10萬的查詢,一個千兆網卡滿足每秒2000次的查詢。Mysql內部每秒能掃描內存中上百萬行數據,相比之下響應數據給客戶端就慢得多了
切分查詢
將一個大查詢分而治之,例如一個刪除大量數據的語句,拆分爲多個小的刪除。
分解關聯查詢
有很多好處:
- 讓緩存的效率更高。無論是應用程序的緩存和Mysql的緩存,都會在單表的情況下更容易命中。
- 查詢分解後減少了鎖的競爭
- 應用層關聯,更容易對數據庫進行拆分,做到高性能和可擴展
- 減少冗餘記錄的查詢
- 在應用中實現的哈希關聯,而不是使用Mysql的嵌套查詢。
執行查詢的基礎
執行查詢的過程:
- 客戶端發送一條查詢給服務器
- 服務器先檢查緩存,如果命中了緩存,則立刻返回存儲在緩存中的結果。否則進入下一個階段。
- 服務器進行SQL解析,預處理,再由優化器生成對應的執行計劃。
- Mysql根據優化器生成的執行計劃,調用存儲引擎的API執行查詢。
- 將結果返回給客戶端
Mysql客戶端/服務器通信協議
通信協議是“半雙工”的,意味着任何一個時刻,要麼是服務端向客戶端發送數據,要麼是客戶端向服務端發送數據。這種協議讓MySQL通信簡單快速。但是也意味着沒法進行流量控制,一旦一端開始發送消息,另一端要完整接收完整個消息才能響應它。客戶端用一個單獨的數據包將查詢傳給服務器,所以查詢語句特別長的時候,參數max_allowed_packet特別重要。
查詢狀態
最簡單使用SHOW FULL PROCESSLIST查看當前狀態,狀態值有如下幾種:
- Sleep:線程正在等待客戶端發送新的請求。
- Query:線程正在執行查詢或者將查詢結果返回客戶端。
- Locked:服務器層線程等待表鎖。在存儲引擎基本實現的鎖,例如InnoDB的行所,不會體現在線程狀態中。
- Analyzing and statistics:線程收集存儲引擎的統計信息,並生成查詢的執行計劃。
- Copying to tmp table [on disk]:線程執行查詢,並將其結果集複製到一個臨時表中,這種狀態一般要麼是做GROUP BY操作,或者文件排序操作,或者UNION操作。如果後面有“on disk”標記表示MySQL將內存臨時表放到磁盤上。
- Sorting result:線程在對結果集排序。
- Sending data:線程可能在多個狀態之間傳送數據,或者在生成結果集,或者在向客戶端返回數據。
查詢緩存
檢查緩存是通過一個對大小寫敏感的哈希查找實現的。查詢和緩存中的查詢即使只有一個字節不同頁不會匹配,如果命中在返回結果集之前MySQL會檢查一次用戶權限,這是無需解析SQL的,因爲查詢緩存中有保存當前查詢需要的表信息。
查詢優化處理
語法解析器和預處理
MySQL通過關鍵字將SQL語句解析,生成語法解析樹,使用MySQL語法規則驗證和解析查詢。例如是否使用了錯誤的關鍵字,關鍵字順序是否正確,引號前後是否正確匹配。
預處理根據MySQL規則進一步檢查解析樹是否合法。例如數據表、列是否存在,名字和別名是否有歧義。
下一步預處理器會驗證權限。
查詢優化器
語法樹已經合法,優化器將其轉爲了執行計劃。優化器作用就是找到最好的執行計劃。
可以通過查詢當前回話的Last_query_cost的值來得知MySQL計算當前查詢成本。
根據一系列統計信息計算得來:每個表或者索引的頁面個數,索引的基數(索引中不同值的數量),索引和數據行的長度,索引分佈的情況。
優化器在評估成本的時候不考慮任何緩存,假設讀取任何數據都需要一次磁盤IO
MySQL的查詢優化器是一個複雜部件,使用了很多優化的執行策略。優化策略簡單分爲兩種:靜態優化和動態優化。
靜態優化直接對解析樹進行優化,靜態優化在第一次萬能充後就一直有效,使用不同參數執行查詢頁不會發生變化,可以認爲是一種“編譯時優化”。
動態優化和查詢的上下文有關,例如WHERE條件中的取值、索引中條目對應的數據行數等。可以認爲時“運行時優化”。
MySQL能夠處理的優化類型:
- 重新定義關聯表的順序:數據表的關聯並不總是按照查詢中指定的順序執行
- 將外連接轉爲內連接:MySQL識別並重寫查詢,讓其可以調整關聯順序。
- 使用等價變化規則:通過等價變換來簡化並規範表達式。合併減少一些比較,一定一些恆等或者恆不等的判斷。
- 優化Count() Max() Min():min和max可以直接查詢b-tree的最左或者最右端。
- 預估並轉化位常數表達式:
- 覆蓋索引掃描
- 子查詢優化;某些情況下可以將子查詢轉換爲效率更高的形式
- 提前終止查詢:在發現已經滿足查詢需求的時候,MySQL總是能夠立刻終止查詢。
- 等值傳播:兩個列的值通過等值關聯,MySQL能夠傳遞where條件。
- 列表in()的比較:MySQL將in()列表中的數據先進行排序,然後通過二分查找的方式來確定列表中的值是否滿足條件。這是一個O(log n)的操作。等價轉換爲Or的複雜度時O(n)。
MySQL執行關聯查詢
MySQL先從一個表中循環取出單條數據,在嵌套循環到下一個表中尋找匹配的行,依次直到找到所有表中匹配的行,然後根據各個表匹配的行返回查詢中需要的各個列。MySQL會嘗試在最後一個關聯表中找到所有匹配的行,如果不行就返回上一層次關聯表。
MySQL多表關聯的指令樹時一顆左側深度優先的樹。
關聯查詢優化器
MySQL的最優執行計劃中的關聯表的順序,通過預估需要讀取的數據頁來選擇,讀取的數據頁越少越好。
關聯順序的調整,可能會讓查詢進行更少的嵌套循環和回溯操作。
可以使用STRAIGHT_JOIN關鍵字重寫查詢,讓優化器按照查詢順序執行。
排序優化
排序時成本很高的操作,從性能角度考慮,應該儘量避免排序,或者避免對大量數據進行排序。
當不能用索引生成排序結果時,MySQL需要字節進行排序,如果數據量小使用內存,數據量大使用磁盤。不過統一都稱爲文件排序(filesort)。
MySQL有兩種排序算法:
- 兩次傳輸排序(舊版本):讀取指針和需要排序的字段,排序之後,再根據排序結果讀取所需要的數據行。第二次讀取數據的時候可能產生大量隨機IOS,成本很高,不過在排序時加載的數據較少,所以在內存中就可以讀取更多的行數進行排序。
- 單次傳輸排序(新版本):查詢所有需要列,根據給定列進行排序直接返回結果。在MySQL4.1之後引入。
查詢執行引擎
查詢執行階段就根據執行計劃,調用存儲引擎的實現接口來完成。
查詢結果返回時,即使不需要返回結果集給客戶端,MySQL返回查詢信息,例如影響到的行數。
查詢優化的侷限性
關聯子查詢(in+子查詢)
使用join,或者使用函數GROUP_CONCAT()在in中構造一個由分好分隔的列表,有時候比關聯更快,in加子查詢性能糟糕,一般建議使用exists等效改寫。
優化特定類型的查詢
優化count查詢
MyISAM的count函數非常快,只有在沒有條件的前提下。
近似值:某些不需要精確值的情況下,可以使用EXPLAIN出來的優化器估算行數。
優化關聯查詢
- 確保on或者using子句中的列上有索引。
- 確保任何的group by和order by中的表達式只設計一個表中的列,這樣MySQL纔有可能使用索引來優化過程
優化子查詢
在5.6之前儘量轉換使用join,5.6之後沒有太多差別
優化group by和distinct
groupby 使用主鍵列效率更高。
優化limit
“延遲關聯”,首先使用索引覆蓋來選取範圍內的主鍵,接下來根據這些主鍵獲取對應數據。
分區表
分區表限制:
- 一個表最多隻能有1024個分區
- 5.1中分區表達式必須是整數,或者是返回整數的表達式。5.5中某些場景可以直接使用列進行分區。
- 如果分區字段中有主鍵或者唯一索引列,那麼所有的主鍵列和唯一索引列都必須包含進來。
- 分區表中無法使用外鍵約束。
在數據量超大的時候B-Tree就無法起作用了,除非是索引覆蓋查詢,否則數據庫服務器需要根據索引掃描的結果回表,查詢所有符合條件的記錄。如果數據量巨大,這將產生大量隨機IO,數據庫的響應時間將大到不可接受的程度。
MySQL優化服務器配置
MySQL配置的工作原理
MySQL從 命令行參數和配置文件中獲取配置信息。配置文件一般是在 /etc/my.cnf 或 /etc/mysql/my.cnf。
確認配置文件路徑,可以使用下列命令
$ which mysql
/bin/mysql
$/bin/mysql --verbose --help|grep -A 1 'Default options'
Default options are read from the following files in the given order:
/etc/my.cnf /etc/mysql/my.cnf /usr/etc/my.cnf ~/.my.cnf
配置文件分爲多個部分,每個部分的開頭是用方括號括起來的分段名稱。客戶端會讀取client部分,服務器通常讀取mysqld部分。
配置項都使用小鞋,單次之間用下劃線或者橫線隔開。
常用變量及其效果
- key_buffer_size
一次性爲鍵緩衝區(key buffer)分配所有的指定空間。操作系統會在使用時才真正分配。 - table_cache_size
這個變量會等到下次有線程打開表纔有效果,會變更緩存中表的數量。 - thread_cache_size
MySQL只有再關閉連接時纔在緩存中增加線程,只在創建新連接時才從緩存中刪除線程。 - query_cache_size
修改這個變量會立刻刪除所有緩存的查詢,重新分配這片緩存到指定大小,並且重新初始化內存。 - read_buffer_size
MySQL只會在查詢需要使用時纔會爲該緩存分配內存,並且一次性分配該參數指定大小的全部內存。 - read_rnd_buffer_size
MySQL只會在查詢需要使用時纔會爲該緩存分配內存,並且只會分配該參數需要大小的內存。 - sort_buffer_size
MySQL只會在查詢排序需要使用時纔會爲該緩存分配內存,並且一次性分配該參數指定大小的全部內存,不管排序是否需要這想·麼大的內存。
InnoDB事務日誌
InnoDB使用日誌來減少提交事務時的開銷。因爲日誌中已經記錄了事務,無需在每個事務提交時把緩衝池的髒塊刷新到磁盤中。
InnoDB用日誌把隨機IO變成順序IO,一旦日誌寫入磁盤,事務就持久化了,即使變更還沒有寫到數據文件。
InnoDB最後是要把變更寫入數據文件,日誌有固定大小。InnoDB的日誌是環形方式寫的:當寫到日誌的尾部,會重新跳轉到開頭繼續寫,但不會覆蓋到還沒應用到數據文件的日誌記錄,因爲這樣會清掉已經提交事務的唯一持久化記錄。
InnoDB使用一個後臺線程只能地刷新這些變更到數據文件。這個線程可以批量組合寫入,是的數據寫入更順序,以提高效率。事務日誌把數據文件的隨機IO轉換爲幾乎順序的日誌文件和數據文件IO,把刷新操作轉移到後臺使得查詢可以更快完成,並且緩和查詢高峯時IO的壓力。
InnoDB表空間
InnoDB把數據保存在表空間內,本質上是一個由一或多個磁盤文件組成的虛擬文件系統。InnoDB用表空間實現很多功能,不只是存儲表和索引。它還保存了回滾日誌(舊版本號),插入緩衝(Insert Buffer)、雙寫緩衝(Doublewrite Buffer),以及其他內部數據結構。
InnoDB使用雙寫緩衝來避免頁沒寫完整鎖導致的數據損壞。這是一個特殊的保留區域,再一些連續的塊中足夠保存100個頁。本質上是一個最近寫回的頁面的備份拷貝。當InnoDB從緩衝池刷新頁面到磁盤時,首先把他們寫到雙寫緩衝,然後再把他們寫到其所屬的數據區域中,可以保證每個頁面的寫入都是原子並且持久化的。頁面在末尾都有校驗值(Checksum)來確認是否損壞。
InnoDB的多線程
- Master Thread
非常核心的後臺線程,主要負責將緩衝池中的數據異步刷新到磁盤,保證數據的一致性,包括髒頁的刷新、合併插入緩衝(INSERT BUFFER)、UNDO頁的回收等。 - IO Thread
InnoDB中大量使用了AIO(Async IO)來處理IO請求,可以極大提高數據庫性能,IO Thread主要是負責這些IO請求的回調(call back)處理。InnoDB1.0之前工有4個IO Thread,分別是write、read、insert buffer、log IO thread。 - Purge Thread
事務提交後,其使用的undolog可能不再需要,因此需要PurgeThread來回收已經使用並分配的undo頁。
InnoDB的內存
-
緩衝池
InnoDB基於磁盤存儲,記錄按照頁的方式進行管理。在數據庫中進行讀取頁的操作,首先將磁盤讀到的頁存放在緩衝池中,下次讀取先判斷頁是否在緩衝池則直接讀取,否則讀取磁盤上的頁。對頁的修改首先修改緩衝池,然後再以一定的頻率刷新到磁盤(通過checkpoint機制)。緩衝池配置通過innodb_buffer_pool_size來設置。
緩衝池中緩存的數據頁類型有:索引頁、數據頁、undo頁、插入緩衝(insert buffer)、自適應哈希索引(adaptive hash index)、InnoDB存儲的鎖信息(lock info)、數據字典信息(data dictionary)等
LRU List、Free List和Flush List
InnoDB的LRU添加了midpoint位置,新讀取的頁不是放到首部,而是放到midpoint位置。默認是放在LRU列表長度的5/8處。有些操作可能會全表掃描加載大量的頁,如果直接加載到首部則可能刷出有效頁。數據庫開始時,LRU是空的,頁都在FreeList中,查找時從Free列表中查找是否有可用空閒頁,若有則從Free列表中刪除放入LRU。當頁從LRU的old部分假如到new時,稱之爲page made young,因爲innodb_old_blocks_time設置導致頁沒有從old部分移動到new部分稱爲page not made young。重做日誌緩衝(redo log buffer)
三種情況會講redo log buffer中的內容刷新到日誌文件
- Master Thread每秒刷新一次
- 每個事務提交時會刷新
- redo log buffer剩餘空間小於1/2時
- 額外的內存池
在對數據庫結構本身的內存進行分配的時候,需要從額外的內存池進行申請。
Checkpoint技術
InnoDB存儲引擎內部有兩種:
- Sharp Checkpoint
數據庫關閉時將所有髒頁刷回磁盤,默認工作方式,參數innodb_fast_shuthown=1 - Fuzzy Checkpoint
刷新一部分髒頁。(Master Thread Checkpoint,FLUSH_LRU_LIST Checkpoint,Async/Sync Flush Checkpoint,Dirty Page too much Checkpoint)
InnoDB關鍵特性
插入緩衝
- Insert Buffer
對於非聚集索引的插入或者更新操作,不是每一次直接插入到索引頁,而是先判斷插入的非聚集索引是否在緩衝池中,若在則插入,若不在則放入到一個Insert Buffer中。以一定的頻率進行Insert Buffer和非聚集索引子節點的合併操作。需要滿足兩個條件:1.索引是輔助索引。2.索引不是唯一的。 - Change Buffer
InnoDB 1.0.x開始可以對DML操作進行緩衝 (Insert,Delete,Update)分別是:Insert Buffer,Delete Buffer,Purge Buffer。
Insert Buffer是一顆B+樹,全局唯一,負責對所有表的輔助索引進行Insert Buffer。
Merge Insert Buffer是合併到真正的輔助索引中的操作,在下面幾種情況時發生:
- 輔助索引頁被讀取到緩衝池中
- Insert Buffer Bitmap 頁追蹤到該輔助索引頁已經沒有空間可用
- Master Thread 觸發
自適應Hash索引(Adaptive Hash Index)
InnoDB 會監控各種索引列的查詢,如果判斷建立哈希索引可以提高訪問速度,則會自動建立。AHI是通過緩衝池的B+樹構建而來,不需要對整張表結構建立哈希索引。有如下要求:
- 以相同模式訪問了100次
- 頁通過該模式訪問了N次:N=頁中記錄*1/16
異步IO
異步IO(Asychronous IO,AIO)
文件
- 參數文件:
初始化參數文件 - 日誌文件:
例如錯誤日誌文件(error log),二進制日誌文件(binlog),慢查詢日誌文件(slow query log),查詢日誌文件(log) - socket文件:
UNIX域套接字方式進行連接是需要的文件。 - pid文件:
MySQL實例的進程ID文件 - MySQL表結構文件:
用來存放MySQL表結構定義的文件 - 存儲引擎文件:
二進制日誌(binlog)
記錄了對MySQL數據庫執行更改的所有操作,不包括SELECT和SHOW。
mysql> mysqlmaster status;
File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
---|---|---|---|---|
binlog.001663 | 5924141 |
mysql> show binlog events in 'binlog.001663' limit 5;
binlog文件名(Log_name) | 日誌開始位置(Pos) | 事件類型(Event_type) | 服務器編號(Server_id) | 日誌結束位置(End_log_pos) | 信息 |
---|---|---|---|---|---|
binlog.001663 | 5878887 | Anonymous_Gtid | 1 | 5878966 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
binlog.001663 | 5878966 | Query | 1 | 5879057 | BEGIN |
binlog.001663 | 5879057 | Table_map | 1 | 5879148 | table_id: 8291 (vv_oaxxljob.XXL_JOB_QRTZ_SCHEDULER_STATE) |
binlog.001663 | 5879148 | Update_rows | 1 | 5879340 | table_id: 8291 flags: STMT_END_F |
binlog.001663 | 5879340 | Xid | 1 | 5879371 | COMMIT /* xid=4800934 */ |
binlog.001663 | 5879371 | Anonymous_Gtid | 1 | 5879450 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
binlog.001663 | 5879450 | Query | 1 | 5879541 | BEGIN |
binlog.001663 | 5879541 | Table_map | 1 | 5879632 | table_id: 8291 (vv_oaxxljob.XXL_JOB_QRTZ_SCHEDULER_STATE) |
binlog.001663 | 5879632 | Update_rows | 1 | 5879824 | table_id: 8291 flags: STMT_END_F |
binlog.001663 | 5879824 | Xid | 1 | 5879855 | COMMIT /* xid=4800956 */ |
binlog.001663 | 5879855 | Anonymous_Gtid | 1 | 5879934 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
binlog.001663 | 5879934 | Query | 1 | 5880025 | BEGIN |
binlog.001663 | 5880025 | Table_map | 1 | 5880116 | table_id: 8291 (vv_oaxxljob.XXL_JOB_QRTZ_SCHEDULER_STATE) |
binlog.001663 | 5880116 | Update_rows | 1 | 5880308 | table_id: 8291 flags: STMT_END_F |
binlog.001663 | 5880308 | Xid | 1 | 5880339 | COMMIT /* xid=4800988 */ |
MySQL5.1引入了binlog_format參數,參數有STATEMENT、ROW、MIXED三種。
- STATEMENT
和之前的MySQL版本一樣,二進制日誌文件記錄的是日誌的邏輯SQL語句。 - ROW
記錄的是表的行更改情況。如果設置爲ROW,可以將InnoDB事務隔離設置爲READ COMMITTED獲取更好的併發性。 - MIXED
默認使用STATEMENT,某些情況下使用MIXED。
- 表的存儲引擎爲NDB,對錶的DML操作以ROW格式記錄。
- 使用了UUID() USER() CURRENT_USER() FOUND_ROWS() ROW_COUNT()
- 使用了INSERT DELAY語句
- 使用了用戶自定義函數
- 使用了臨時表
要查看binlog日誌文件的內容,必須使用MySQL提供的工具mysqlbinlog。
表結構定義文件
MySQL定義了frm爲後綴名的文件,記錄了表結構(視圖)定義。
InnoDB存儲引擎文件
表空間文件(tablespace file)
默認有一個初始大小爲10MB,名爲ibdata1的文件
重做日誌文件(redo log file)
默認情況下會有 ib_logfile0和ib_logfile1作爲 redo log file 。每個InnoDB至少有一個重做日誌文件組(group),文件組下有兩個重做日誌文件,用戶可以設置多個鏡像日誌組(mirrored log groups)
表
索引組織表(index organized table)
MySQL默認創建一個6字節大小的指針(_rowid)
InnoDB邏輯存儲結構
所有的數據都被邏輯地存放在一個空間內,稱之爲表空間(tablespace),表空間又由段(segment),區(extent)、頁(page)組成,頁在某些文檔中也成爲塊(block)
表空間
如果啓用了 innodb_file_per_table的參數,每張表的數據可以單獨放到一個表空間內 ,其中存放的是數據、索引、和插入緩衝Bitmap頁,其他類的數據如回滾(undo)信息、插入緩衝索引頁、系統事務信息、二次寫緩衝還是放在原本的共享表空間。
段
表空間是由各個段組成的,包括數據段、索引段、回滾段等。數據段就是B+樹的葉子節點(Leaf node segment),索引段即B+樹的非索引節點(Non-leaf node segment)。
區
區是連續頁組成的空間,任何情況下每個區的大小都爲1MB,爲了保證區中頁的連續性,InnoDB一次從磁盤申請4-5個區,默認情況頁大小爲16KB,一個區中一共有64個連續的頁。InnoDB1.0.x引入壓縮頁,每個頁的大小可以通過key_block_size設置爲2k、4k、8k。1.2.x版本新增了參數innodb_page_size,通過該參數可以將默認頁的大小設置爲4k、8k。
頁
InnoDB中常見的頁類型有:
- 數據頁(B-tree Node)
- Undo頁(Undo Log Page)
- 系統頁(System Page)
- 事務數據頁(Transaction system Page)
- 插入緩衝頁位圖(Insert Buffer Bitmap)
- 插入緩衝空閒列表頁(Insert Buffer Free List)
- 未壓縮的二進制大對象頁(Uncompressed BLOB Page)
行
MySQL的存儲是面向列的(row-oriented),數據是按行存儲的。頁存放的記錄有硬性定義最多存放16KB/2 - 200行,即7992行。
InnoDB數據頁結構
數據頁由下面7個部分組成:
- File Header(文件頭)固定
- Page Header(頁頭)固定
- Infimun 和 Supremun Record 固定
頁中兩個虛擬的行記錄,Infimun是指比頁中任何主鍵更小的值,Supremun指比任何值都大的值,這兩個值在頁創建的時候創建,在任何時候情況下都不會刪除。 - User Record(用戶記錄,即行記錄)
存儲實際記錄,B+樹索引組織。 - Free Space(空閒空間)
空閒空間,鏈表數據結構。一條記錄被刪除後會放到空閒空間。 - Page Directory(頁目錄)
存放了記錄的相對位置,這些記錄指針稱之爲槽(slots)或者目錄槽(dictionary slots),稀疏目錄,可能包含多條記錄。
B+樹索引不能找到實際的記錄,而是找到記錄的頁。 -
File Trailer(文件結尾信息)
檢測頁是否完整寫入了磁盤,checksum值。
行溢出數據
InnoDB會將一條記錄中的某些列存儲在真正的數據列之外,BLOB,LOB字段可能不一定會將字段放在溢出頁面,VARCHAR也有可能會放進溢出頁面。
Oracle VarCHAR2最多存放4000字節,MSSQL最多8000字節,MySQL最多65535(存在其他開銷,最長65532)。當發生行溢出時,數據存放在頁類型Uncompress BLOB頁面。數據頁只保存數據的前768字節。
鎖
lock與latch
latch一般稱爲閂鎖,輕量級,要求鎖定的時間非常短。在InnoDB中,分爲mutex(互斥量)與rwlock(讀寫鎖)。用來保證併發線程操作臨界資源的正確性,並且通常沒有死鎖檢測的機制。
lock的對象是事務,用來鎖定的是數據庫中的對象,如表、頁、行。在commit或者rollback之後釋放,有死鎖檢測機制。
鎖的類型
- 共享鎖(S Lock):允許事務讀一行數據
- 排他鎖(X Lock):允許事務更新或刪除一行數據
上述兩種都是悲觀鎖,樂觀鎖就是CAS(Compare and Swap)
一致性非鎖定讀(consistent nonlocking read)
是指InnoDB通過MVCC(Multi Version Concurrency Control)讀取數據庫當前行的方式。如果讀取的行正在進行update或者delete操作,則讀取一個快照。在Read Committed和Repeatedable Read中使用。前者讀取最新的快照,後者使用事務開始時的快照。
一致性鎖定讀(locking read)
也可以顯式的對讀取加鎖,有兩種操作:
- select ... for update(加一個X鎖)
- select ... lock in share mode(加一個S鎖)
行鎖的3種算法
- Record Lock:單個行記錄的鎖
- Gap Lock:鎖定一個範圍,不包括記錄本身
- Next-Key Lock:Gap+Record,鎖定範圍以及記錄本身。用來解決幻影相關問題(Phantom)
針對的是索引的區間,但是當查詢條件指定唯一索引值(只針對主鍵索引/聚集索引)時,會降級爲Record Lock,若是二級索引則不會。而且InnoDB還會對二級索引的下一個鍵值加上Gap Lock。
例如,二級索引b列有1,3,6,9。當使用X鎖鎖定3時(where b<=3 for update),會NKL鎖定了範圍(1-3),同時會使用GL鎖定下一個鍵值(3-6)。
利用這個機制可以用一個事務,首先select id from t where col=xxx lock in share mode,接下來insert t (col) values (xxx),能夠保證一定插入不存在的值。
死鎖
兩個事務執行時,因爭奪鎖資源互相等待的場景。
解決死鎖最簡單的就是超時,通過innodb_lock_wait_timeout控制超時時間。
當前普遍使用的是wait-for graph(主動檢測的方式),這要求數據庫保存兩種信息:
- 鎖的信息鏈表
- 事務的等待列表
通過上述信息,可以在事務請求鎖併發生等待時都進行判斷,在上述兩個信息構造的圖中是否存在迴路,如果存在就表示存在死鎖。
採用深度優先算法實現,InnoDB1.2之前採用遞歸方式,之後採用非遞歸提高了性能。
事務的實現(ACID)
事務的隔離性由鎖來實現,redo log(重做日誌)保證事務的原子性和持久性,undo log()保證事務的一致性。
redo恢復提交事務修改的頁操作,是物理日誌,記錄的是頁的物理修改操作。
undo回滾某個行記錄到特定版本,是邏輯日誌,記錄的是行的修改記錄。
redo
存在 redo log buffer和redo log file,buffer寫入file時需要調用fsync操作,此操作取決於磁盤性能,決定了事務提交的性能也就是數據庫的性能。
UNIX的寫操作
一般情況下,對硬盤(或者其他持久存儲設備)文件的write操作,更新的只是內存中的頁緩存(page cache),而髒頁面不會立即更新到硬盤中,而是由操作系統統一調度,如由專門的flusher內核線程在滿足一定條件時(如一定時間間隔、內存中的髒頁達到一定比例)內將髒頁面同步到硬盤上(放入設備的IO請求隊列)。
因爲write調用不會等到硬盤IO完成之後才返回,因此如果OS在write調用之後、硬盤同步之前崩潰,則數據可能丟失。雖然這樣的時間窗口很小,但是對於需要保證事務的持久化(durability)和一致性(consistency)的數據庫程序來說,write()所提供的“鬆散的異步語義”是不夠的,通常需要OS提供的同步IO(synchronized-IO)原語來保證
fsync的功能是確保文件fd所有已修改的內容已經正確同步到硬盤上,該調用會阻塞等待直到設備報告IO完成。除了同步文件的修改內容(髒頁),fsync還會同步文件的描述信息(metadata,包括size、訪問時間st_atime & st_mtime等等),因爲文件的數據和metadata通常存在硬盤的不同地方,因此fsync至少需要兩次IO寫操作
undo
delete和update操作產生的刪除語句並不是馬上執行,而是將delete_flag標記爲1,最後有purge操作來統一完成。用undo log來執行,執行之後的空間不會回收,只會用於重用。