hello,小夥伴們,很久不見,MySQL系列停更了差很少兩個月了,也有小夥伴問我爲啥不更了呢?其實我去看了MySQL的全集,準備憋個大招,更新篇長文(我不會告訴你是由於我懶的)。html
好了,話很少說,直接開始吧。這篇文章將從查詢緩存,索引,優化器,explain,redo日誌,undo日誌,事務隔離級別,鎖等方面來說,若是想了解某個方面,直接跳到指定目錄。mysql
這張圖是重點
!!!咱要先對MySQL有一個宏觀的瞭解,知道他的執行流程。面試
一條SQL語句過來的流程是什麼樣的?那就follow me。哈哈哈哈,皮一下很開心。sql
1.當客戶端鏈接到MySQL服務器時,服務器對其進行認證。能夠經過用戶名與密碼認證,也能夠經過SSL證書進行認證。登陸認證後,服務器還會驗證客戶端是否有執行某個查詢的操做權限。數據庫
2.在正式查詢以前,服務器會檢查查詢緩存,若是能找到對應的查詢,則沒必要進行查詢解析,優化,執行等過程,直接返回緩存中的結果集。緩存
3.MySQL的解析器會根據查詢語句,構造出一個解析樹,主要用於根據語法規則來驗證語句是否正確,好比SQL的關鍵字是否正確,關鍵字的順序是否正確。bash
而預處理器主要是進一步校驗,好比表名,字段名是否正確等服務器
4.查詢優化器將解析樹轉化爲查詢計劃,通常狀況下,一條查詢能夠有不少種執行方式,最終返回相同的結果,優化器就是根據成本
找到這其中最優的執行計劃網絡
5.執行計劃調用查詢執行引擎,而查詢引擎經過一系列API接口查詢到數據session
6.獲得數據以後,在返回給客戶端的同時,會將數據存在查詢緩存中
咱們先經過show variables like '%query_cache%'
來看一下默認的數據庫配置,此爲本地數據庫的配置。
have_query_cache:當前的MYSQL版本是否支持「查詢緩存」功能。
query_cache_limit:MySQL可以緩存的最大查詢結果,查詢結果大於該值時不會被緩存。默認值是1048576(1MB)
query_cache_min_res_unit:查詢緩存分配的最小塊(字節)。默認值是4096(4KB)。當查詢進行時,MySQL把查詢結果保存在query cache,可是若是保存的結果比較大,超過了query_cache_min_res_unit的值,這時候MySQL將一邊檢索結果,一邊進行保存結果。他保存結果也是按默認大小先分配一塊空間,若是不夠,又要申請新的空間給他。若是查詢結果比較小,默認的query_cache_min_res_unit可能形成大量的內存碎片,若是查詢結果比較大,默認的query_cache_min_res_unit又不夠,致使一直分配塊空間,因此能夠根據實際需求,調節query_cache_min_res_unit的大小。
注:若是上面說的內容有點彎彎繞,那舉個現實生活中的例子,好比咱如今要給運動員送水,默認的是500ml的瓶子,若是過來的是少年運動員,可能500ml太大了,他們喝不完,形成了浪費,那咱們就能夠選擇300ml的瓶子,若是過來的是成年運動員,可能500ml不夠,那他們一瓶喝完了,又開一瓶,直接不渴爲止。那麼那樣開瓶子也要時間,咱們就能夠選擇1000ml的瓶子。
query_cache_size:爲緩存查詢結果分配的總內存。
query_cache_type:默認爲on,能夠緩存除了以select sql_no_cache開頭的全部查詢結果。
query_cache_wlock_invalidate:若是該表被鎖住,是否返回緩存中的數據,默認是關閉的。
MYSQL的查詢緩存實質上是緩存SQL的hash值和該SQL的查詢結果,若是運行相同的SQL,服務器直接從緩存中去掉結果,而再也不去解析,優化,尋找最低成本的執行計劃等一系列操做,大大提高了查詢速度。
可是萬事有利也有弊。
好比一張表裏面只有兩個字段,分別是id和name,數據有一條爲1,張三。我使用select * from 表名 where name=「張三」來進行查詢,MySQL發現查詢緩存中沒有此數據,會進行一系列的解析,優化等操做進行數據的查詢,查詢結束以後將該SQL的hash和查詢結果緩存起來,並將查詢結果返回給客戶端。可是這個時候我有新增了一條數據2,張三。若是我還用相同的SQL來執行,他會根據該SQL的hash值去查詢緩存中,那麼結果就錯了。因此MySQL對於數據有變化的表來講,會直接清空關於該表的全部緩存。這樣實際上是效率是不好的。
咱們都知道hash值的規則,就算很小的查詢,哈希出來的結果差距是不少的,因此select * from 表名 where name=「張三」和SELECT * FROM 表名 WHERE NAME=「張三」和select * from 表名 where name = 「張三」,三個SQL哈希出來的值是不同的,大小寫和空格影響了他們,因此並不能命中緩存,但其實他們搜索結果是徹底同樣的。
先來看線上參數:
咱們發現將query_cache_type設置爲OFF,其實網上資料和各大雲廠商提供的雲服務器都是將這個功能關閉的,從上面的原理來看,在通常狀況下,他的弊端大於優勢
。
建立一個名爲user的表,其包括id,name,age,sex等字段信息。此外,id爲主鍵聚簇索引,idx_name爲非聚簇索引。
CREATE TABLE `user` (
`id` varchar(10) NOT NULL DEFAULT '',
`name` varchar(10) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`sex` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;複製代碼
咱們將其設置10條數據,便於下面的索引的理解。
INSERT INTO `user` VALUES ('1', 'andy', '20', '女');
INSERT INTO `user` VALUES ('10', 'baby', '12', '女');
INSERT INTO `user` VALUES ('2', 'kat', '12', '女');
INSERT INTO `user` VALUES ('3', 'lili', '20', '男');
INSERT INTO `user` VALUES ('4', 'lucy', '22', '女');
INSERT INTO `user` VALUES ('5', 'bill', '20', '男');
INSERT INTO `user` VALUES ('6', 'zoe', '20', '男');
INSERT INTO `user` VALUES ('7', 'hay', '20', '女');
INSERT INTO `user` VALUES ('8', 'tony', '20', '男');
INSERT INTO `user` VALUES ('9', 'rose', '21', '男');複製代碼
先來一張圖鎮樓,接下來就是看圖說話。
他包含兩個特色:
1.使用記錄主鍵值的大小來進行記錄和頁的排序。
頁內的記錄是按照主鍵的大小順序排成一個單項鍊表。
各個存放用戶記錄的頁也是根據頁中用戶記錄的主鍵大小順序排成一個雙向鏈表。
2.葉子節點存儲的是完整的用戶記錄
。
注:聚簇索引不須要咱們顯示的建立,他是由InnoDB存儲引擎自動爲咱們建立的。若是沒有主鍵,其也會默認建立一個。複製代碼
上面的聚簇索引只能在搜索條件是主鍵時才能發揮做用,由於聚簇索引能夠根據主鍵進行排序的。若是搜索條件是name,在剛纔的聚簇索引上,咱們可能遍歷,挨個找到符合條件的記錄,可是,這樣真的是太蠢了,MySQL不會這樣作的。
若是咱們想讓搜索條件是name的時候,也能使用索引,那能夠多建立一個基於name的二叉樹。以下圖。
他與聚簇索引的不一樣:
1.葉子節點內部使用name字段排序,葉子節點之間也是使用name字段排序。
2.葉子節點再也不是完整的數據記錄,而是name和主鍵值。
爲何再也不是完整信息?
MySQL只讓聚簇索引的葉子節點存放完整的記錄信息,由於若是有好幾個非聚簇索引,他們的葉子節點也存放完整的記錄績效,那就不浪費空間啦。
若是我搜索條件是基於name,須要查詢全部字段的信息,那查詢過程是啥?
1.根據查詢條件,採用name的非聚簇索引,先定位到該非聚簇索引某些記錄行。
2.根據記錄行找到相應的id,再根據id到聚簇索引中找到相關記錄。這個過程叫作回
表
。
圖就不畫了,簡單來講,若是name和age組成一個聯合索引,那麼先按name排序,若是name同樣,就按age排序。
1.最左前綴原則。一個聯合索引(a,b,c),若是有一個查詢條件有a,有b,那麼他則走索引,若是有一個查詢條件沒有a,那麼他則不走索引。
2.使用惟一索引。具備多個重複值的列,其索引效果最差。例如,存放姓名的列具備不一樣值,很容易區分每行。而用來記錄性別的列,只含有「男」,「女」,無論搜索哪一個值,都會得出大約一半的行,這樣的索引對性能的提高不夠高。
3.不要過分索引。每一個額外的索引都要佔用額外的磁盤空間,並下降寫操做的性能。在修改表的內容時,索引必須進行更新,有時可能須要重構,所以,索引越多,所花的時間越長。
四、索引列不能參與計算,保持列「乾淨」,好比from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,緣由很簡單,b+樹中存的都是數據表中的字段值,但進行檢索時,須要把全部元素都應用函數才能比較,顯然成本太大。因此語句應該寫成create_time = unix_timestamp(’2014-05-29’);
5.必定要設置一個主鍵。前面聚簇索引說到若是不指定主鍵,InnoDB會自動爲其指定主鍵,這個咱們是看不見的。反正都要生成一個主鍵的,還不如咱們設置,之後在某些搜索條件時還能用到主鍵的聚簇索引。
6.主鍵推薦用自增id,而不是uuid。上面的聚簇索引說到每頁數據都是排序的,而且頁之間也是排序的,若是是uuid,那麼其確定是隨機的,其可能從中間插入,致使頁的分裂,產生不少表碎片。若是是自增的,那麼其有從小到大自增的,有順序,那麼在插入的時候就添加到當前索引的後續位置。當一頁寫滿,就會自動開闢一個新的頁。
注:若是自增id用完了,那將字段類型改成bigint,就算每秒1萬條數據,跑100年,也沒達到bigint的最大值。複製代碼
一、 B+樹的磁盤讀寫代價更低:B+樹的內部節點並無指向關鍵字具體信息的指針,所以其內部節點相對B樹更小,若是把全部同一內部節點的關鍵字存放在同一盤塊中,那麼盤塊所能容納的關鍵字數量也越多,一次性讀入內存的須要查找的關鍵字也就越多,相對IO讀寫次數就下降
了。
二、因爲B+樹的數據都存儲在葉子結點中,分支結點均爲索引,方便掃庫,只須要掃一遍葉子結點便可,可是B樹由於其分支結點一樣存儲着數據,咱們要找到具體的數據,須要進行一次中序遍歷按序來掃,因此B+樹更加適合在區間查詢
的狀況,因此一般B+樹用於數據庫索引。
在開篇的圖裏面,咱們知道了SQL語句從客戶端經由網絡協議到查詢緩存,若是沒有命中緩存,再通過解析工做,獲得準確的SQL,如今就來到了咱們這模塊說的優化器。
首先,咱們知道每一條SQL都有不一樣的執行方法,要不經過索引,要不經過全表掃描的方式。
那麼問題就來了,MySQL是如何選擇時間最短,佔用內存最小的執行方法呢?
1.I/O成本。數據存儲在硬盤上,咱們想要進行某個操做須要將其加載到內存中,這個過程的時間被稱爲I/O成本。默認是1。
2.CPU成本。在內存對結果集進行排序的時間被稱爲CPU成本。默認是0.2。
先來建一個用戶表dev_user,裏面包括主鍵id,用戶名username,密碼password,外鍵user_info_id,狀態status,外鍵main_station_id,是否外網訪問visit,這七個字段。索引有兩個,一個是主鍵的聚簇索引,另外一個是顯式添加的以username爲字段的惟一索引uname_unique。
若是搜索條件是select * from dev_user where username='XXX',那麼MySQL是如何選擇相關索引呢?
1.使用全部可能用到的索引
咱們能夠看到搜索條件username,因此可能走uname_unique索引。也能夠作聚簇索引,也就是全表掃描。
2.計算全表掃描代價
咱們經過show table status like ‘dev_user’
命令知道rows
和data_length
字段,以下圖。
rows:表示表中的記錄條數,可是這個數據不許確,是個估計值。
data_length:表示表佔用的存儲空間字節數。
data_length=聚簇索引的頁面數量X每一個頁面的大小
反推出頁面數量=1589248÷16÷1024=97
I/O成本:97X1=97
CPU成本:6141X0.2=1228
總成本:97+1228=1325
3.計算使用不一樣索引執行查詢的代價
由於要查詢出知足條件的全部字段信息,因此要考慮回表成本。
I/O成本=1+1X1=2(範圍區間的數量+預計二級記錄索引條數)
CPU成本=1X0.2+1X0.2=0.4(讀取二級索引的成本+回表聚簇索引的成本)
總成本=I/O成本+CPU成本=2.4
4.對比各類執行方案的代價,找出成本最低的那個
上面兩個數字一對比,成本是採用uname_unique索引成本最低。
對於兩錶鏈接查詢來講,他的查詢成本由下面兩個部分構成:
若是前面的搜索條件不是等值,而是區間,如select * from dev_user where username>'admin' and username<'test'
這個時候咱們是沒法看出須要回表的數量。
步驟1:先根據username>'admin'這個條件找到第一條記錄,稱爲區間最左記錄
。
步驟2:再根據username<'test'這個條件找到最後一條記錄,稱爲區間最右記錄
。
步驟3:若是區間最左記錄和區間最右記錄相差不是很遠,能夠準確統計出須要回表的數量。若是相差很遠,就先計算10頁有多少條記錄,再乘以頁面數量,最終模糊統計出來。
產品:爲何這個頁面出來這麼慢?
開發:由於你查的數據多唄,他就是這麼慢
產品:我無論,我要這個頁面快點,你這樣,客戶怎麼用啊
開發:。。。。。。。你行你來
哈哈哈哈,不瞎BB啦,若是有些SQL賊慢,咱們須要知道他有沒有走索引,走了哪一個索引,這個時候我就須要經過explain關鍵字來深刻了解MySQL內部是如何執行的。
通常來講一個select一個惟一id,若是是子查詢,就有兩個select,id是不同的,可是凡事有例外,有些子查詢的,他們id是同樣的。
這是爲何呢?
那是由於MySQL在進行優化的時候已經將子查詢改爲了鏈接查詢,而鏈接查詢的id是同樣的。
顯示這一行是關於哪張表的。
對某表進行單表查詢時可能用到的索引
通過查詢優化器計算不一樣索引的成本,最終選擇成本最低的索引
InnoDB存儲引擎是以頁爲單位來管理存儲空間的,咱們進行的增刪改查操做都是將頁的數據加載到內存中,而後進行操做,再將數據刷回到硬盤上。
那麼問題就來了,若是我要給張三轉帳100塊錢,事務已經提交了,這個時候InnoDB把數據加載到內存中,這個時候還沒來得及刷入硬盤,忽然停電了,數據庫崩了。重啓以後,發現個人錢沒有轉成功,這不是尷尬了嗎?
解決方法很明顯,咱們在硬盤加載到內存以後,進行一系列操做,一頓操做猛如虎,還未刷新到硬盤以前,先記錄下,在XXX位置個人記錄中金額減100,在XXX位置張三的記錄中金額加100,而後再進行增刪改查操做,最後刷入硬盤。若是未刷入硬盤,在重啓以後,先加載以前的記錄,那麼數據就回來了。
這個記錄就叫作重作日誌,即redo日誌。他的目的是想讓已經提交的事務對數據的修改是永久的,就算他重啓,數據也能恢復出來。
爲了解決磁盤速度過慢的問題,redo日誌不能直接寫入磁盤,咱先整一大片連續的內存空間給他放數據。這一大片內存就叫作日誌緩衝區,即log buffer。到了合適的時候,再刷入硬盤。至於何時是合適的,這個下一章節說。
咱們能夠經過show VARIABLES like 'innodb_log_buffer_size'
命令來查看當前的日誌緩存大小,下圖爲線上的大小。
因爲redo日誌一直都是增加的,且內存空間有限,數據也不能一直待在緩存中, 咱們須要將其刷新至硬盤上。
那何時刷新到硬盤呢?
咱們能夠經過show variables like 'datadir'
命令找到相關目錄,底下有兩個文件, 分別是ib_logfile0和ib_logfile1,以下圖所示。
咱們將緩衝區log buffer裏面的redo日誌刷新到這個兩個文件裏面,他們寫入的方式 是循環寫入的,先寫ib_logfile0,再寫ib_logfile1,等ib_logfile1寫滿了,再寫ib_logfile0。 那這樣就會存在一個問題,若是ib_logfile1寫滿了,再寫ib_logfile0,以前ib_logfile0的內容 不就被覆蓋而丟失了嗎? 這就是checkpoint的工做啦。
redo日誌是爲了系統崩潰後恢復髒頁用的,若是這個髒頁能夠被刷新到磁盤上,那麼 他就能夠功成身退,被覆蓋也就沒事啦。
衝突補習
從系統運行開始,就不斷的修改頁面,會不斷的生成redo日誌。redo日誌是不斷 遞增的,MySQL爲其取了一個名字日誌序列號Log Sequence Number,簡稱lsn。 他的初始化的值爲8704,用來記錄當前一共生成了多少redo日誌。
redo日誌是先寫入log buffer,以後纔會被刷新到磁盤的redo日誌文件。MySQL爲其 取了一個名字flush_to_disk_lsn。用來講明緩存區中有多少的髒頁數據被刷新到磁盤上啦。 他的初始值和lsn同樣,後面的差距就有了。
作一次checkpoint分爲兩步
undo log有兩個做用:提供回滾和多個行版本控制(MVCC
)。
undo log和redo log記錄物理日誌不同,它是邏輯日誌。能夠認爲當delete一條記錄時,undo log中會記錄一條對應的insert記錄,反之亦然,當update一條記錄時,它記錄一條對應相反的update記錄。
舉個例子:
insert into a(id) values(1);(redo)
這條記錄是須要回滾的。
回滾的語句是delete from a where id = 1;(undo)
試想一想看。若是沒有作insert into a(id) values(1);(redo)
那麼delete from a where id = 1;(undo)這句話就沒有意義了。
如今看下正確的恢復:
先insert into a(id) values(1);(redo)
而後delete from a where id = 1;(undo)
系統就回到了原先的狀態,沒有這條記錄了
是存在段之中。
事務中有一個隔離性特徵,理論上在某個事務對某個數據進行訪問時,其餘事務應該排序,當該事務提交以後,其餘事務才能繼續訪問這個數據。
可是這樣子對性能影響太大,咱們既想保持事務的隔離性,又想讓服務器在出來多個事務時性能儘可能高些,因此只能捨棄一部分隔離性而去性能。
sessionB:修改了同一條數據,提交掉
對於sessionB來講,明明數據更新了也提交了事務,不能說本身啥都沒幹
session B:修改某條數據,可是最後回滾掉啦
session A:在sessionB修改某條數據以後,在回滾以前,讀取了該條記錄
對於session A來講,讀到了session回滾以前的髒數據
數據庫都有的四種隔離級別,MySQL事務默認的隔離級別是可重複讀,並且MySQL能夠解決了幻讀的問題。
舉個例子:
session A:查詢某條不存在的記錄。
session B:新增該條不存在的記錄,並提交事務。
session A:再次查詢該條不存在的記錄,是查詢不出來的,可是若是我嘗試修改該條記錄,並提交,其實他是能夠修改爲功的。
版本鏈:對於該記錄的每次更新,都會將值放在一條undo日誌中,算是該記錄的一箇舊版本,隨着更新次數的增多,全部版本都會被roll_pointer屬性鏈接成一個鏈表,即爲版本鏈。
readview:
小老弟不容易,忙了好幾天,終於寫好。