【精選面試題】數據庫系列

一、mysql 聚集索引、非聚集索引

聚集索引:

給表上了主鍵,那麼表在內存上的由整齊排列的結構轉變成了樹狀結構,也就是「平衡樹」結構,換句話說,就是整個表就變成了一個索引。沒錯, 再說一遍, 整個表變成了一個索引,也就是所謂的「聚集索引」。這就是爲什麼一個表只能有一個主鍵, 一個表只能有一個「聚集索引」,因爲主鍵的作用就是把「表」的數據格式轉換成「索引(平衡樹)」的格式放置。

非聚集索引:

非聚集索引和聚集索引一樣, 同樣是採用平衡樹作爲索引的數據結構。索引樹結構中各節點的值來自於表中的索引字段, 假如給user表的name字段加上索引 , 那麼索引就是由name字段中的值構成,在數據改變時, DBMS需要一直維護索引結構的正確性。如果給表中多個字段加上索引 , 那麼就會出現多個獨立的索引結構,每個索引(非聚集索引)互相之間不存在關聯。

區別在於,通過聚集索引可以查到需要查找的數據, 而通過非聚集索引可以查到記錄對應的主鍵值 , 再使用主鍵的值通過聚集索引查找到需要的數據。

總結:

非聚集索引就是一般常用的索引,索引樹的根節點是表的主鍵;聚集索引就是主鍵組成的樹,根節點是數據庫真實數據的位置。許多數據庫的文檔會告訴讀者:聚集索引按照順序物理的存儲數據到磁盤。但是試想下,如果聚集索引必須按照特定順序存放物理記錄,則維護成本顯得非常高。所以,聚集索引的磁盤存儲並不是物理上連續的,而是邏輯上連續。

 

二、mysql 索引爲什麼會選擇B+樹存儲實現

概念:

mysql底層使用的是B+樹,mysql索引是放在磁盤上面的,因此每次讀取索引時通過IO從磁盤讀取。

1、hash索引:無規則、不能排序

2、二叉樹:解決hash索引不能排序問題,但是當數據有序時會出現線性排列,樹的深度會變得很深,會消耗大量IO。

3、平衡二叉樹:解決二叉樹數據有序時出現的線性插入樹太深問題,樹的深度會明顯降低,極大提高性能,但是當數據量很大時,一般mysql中一張表達到3-5百萬條數據是很普遍的,因此平衡二叉樹的深度還是非常大,mysql讀取時還是會消耗大量IO,不僅如此,計算從磁盤讀取數據時以頁(4KB)爲單位的,及每次讀取4096byte。平衡二叉樹每個節點只保存了一個關鍵字(如int即4byte),浪費了4092byte,極大的浪費了讀取空間。

4、B-樹:解決平衡二叉樹樹深度的問題,解決了平衡二叉樹讀取消耗大量內存空間的問題。因爲B-樹每個節點可以存放多個關鍵字,最大限度的利用了從磁盤讀取的內存空間,單節點存放多個關鍵字同時也大大減少了樹的深度。極大的提高了mysql的查詢性能。但是B-樹還是有缺點,B-樹對有範圍查找的查詢(如age>20)時採用的還是中序排序法,因此也需要多遍歷,並且查詢性能不穩定,比如查詢(select * from table where id = 222 和 select * from table where id = 223)時在查詢效率(耗時)上可能會存在一定的差別,因爲B-樹還是將關鍵字,這裏爲id,存放在根節點和葉節點的,如果運氣好,可能id=222這個關鍵字就在第一個節點,消耗一次IO就找到了,而id=223可能在葉節點,需要消耗3次IO才能找到。因此B-樹對同一條sql語句的查詢性能可能會有很大影響(確實感覺有點扯,但是事實是這樣)。

5、B+樹:將關鍵字全部存放在葉子節點(查詢更穩定,同一條mysql語句執行效率時相同的,都會消耗3次IO),將相鄰葉子節點的地址保存起來(相比於B-樹,對於mysql的範圍查找不用再使用中序查找,而是可以直接快讀獲取到。)

B+樹是B-樹的變種(PLUS版)多路絕對平衡查找數,他擁有B-樹的優勢。

B+樹掃庫、表能力更強(因爲B+樹只在葉子節點保存數據了,因此每次IO讀取的數據會更多。)

B+樹的磁盤讀寫能力更強(因爲B+樹只在葉子節點保存數據了,因此每次IO讀取的數據會更多。)

B+樹的排序能力更強。(因爲葉子節點添加了左邊最大的指向右邊最小的,有天然的排序。)

B+樹的查詢效率更加穩定。(B-樹可能一次IO就命中查詢,但是同一類查詢,不同的值可能一次IO就命中、可能3次IO才命中,查詢效率不穩定。而B+樹IO消耗次數是固定的,因此葉子節點才保存數據地址,更加穩定。)

 

三、mysql 執行計劃

1.執行計劃的概念:

我們都知道mysql對於一條sql在執行過程中會對它進行優化,而對於查詢語句的來說最好的優化方案就是使用索引。而執行計劃就是顯示mysql執行sql時的詳細執行情況。其中包含了是否使用索引,使用了哪些索引…

2.執行計劃的語法:

  • 常規執行計劃語法

explain select * from user;
  • 擴展執行計劃的語法

explain 的extended 擴展能夠在原本explain的基礎上額外的提供一些查詢優化的信息,這些信息可以通過mysql的show warnings命令得到。

explain extended select * from user;
  • 分區表的執行計劃語法
explain partitions select * from user;

3.執行計劃包含的信息:

不同版本的Mysql和不同的存儲引擎執行計劃不完全相同,但基本信息都差不多。mysql執行計劃主要包含以下信息:

id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE user ALL NULL NULL NULL NULL 1 NULL
  • id:查詢的順序

由一組數字組成,表示一個查詢中各個子查詢的執行順序。

id id不同情況 執行順序
1 id相同  執行順序由上至下
2 id不同  值越大優先級越高,越先被執行 
3 id爲null  表示一個結果集,不需要使用它查詢,常出現在包含union等查詢語句中
  • select_type:查詢類型

id select_type description 舉例
1 SIMPLE 不包含任何子查詢或union等查詢 EXPLAIN SELECT * FROM user WHERE id=1;
2 PRIMARY 包含子查詢最外層查詢就顯示爲 PRIMARY EXPLAIN SELECT * FROM user WHERE id=(SELECT user_id FROM address WHERE id=1);
3 SUBQUERY select或 where字句中包含的查詢 EXPLAIN SELECT * FROM user WHERE id=(SELECT user_id FROM address WHERE id=1);
4 DERIVED from字句中包含的查詢(衍生查詢) EXPLAIN SELECT * FROM (SELECT * FROM user WHERE id>1) t;
5 UNION 出現在union後的查詢語句中(當前執行計劃的中間記錄就是 UNION RESULT)) EXPLAIN SELECT * FROM user WHERE id=1 UNION SELECT * FROM user WHERE id=2;
6 UNION RESULT 從UNION中獲取結果集當前執行計劃的最後一條記錄就是 UNION RESULT) EXPLAIN SELECT * FROM user WHERE id=1 UNION SELECT * FROM user WHERE id=2;
  • table:查詢涉及到的表

如果查詢使用了別名,那麼這裏顯示的是別名,如果不涉及對數據表的操作,那麼這顯示爲null,如果顯示爲尖括號括起來的就表示這個是臨時表,後邊的N就是執行計劃中的id,表示結果來自於這個查詢產生。如果是尖括號括起來的<union M,N>,與類似,也是一個臨時表,表示這個結果來自於union查詢的id爲M,N的結果集。

  • type:訪問類型

id type 含義 舉例
1 ALL 全表掃描(沒有使用到索引) EXPLAIN SELECT * FROM user WHERE PASSWORD=‘1’;
2 index 遍歷索引(只查詢索引列) EXPLAIN SELECT id FROM user ;
3 range 索引範圍查找(在索引列添加範圍查詢) EXPLAIN SELECT id FROM user where id>1;
4 index_subquery 在子查詢中使用 ref 暫時沒有合適例子
5 unique_subquery 在子查詢中使用 eq_ref 暫時沒有合適例子
6 ref_or_null 對Null進行索引的優化的 ref 暫時沒有合適例子
7 fulltext 使用全文索引 暫時沒有合適例子
8 ref 使用非唯一索引查找數據 EXPLAIN SELECT id FROM user WHERE username=‘p1’;
9 eq_ref 在join查詢中使用 PRIMARY KEY or UNIQUE NOT NULL索引關聯 (這裏需要把address的user_id設置爲unique類型的索引) EXPLAIN SELECT * FROM user u left JOIN address a on u.id=a.user_id WHERE a.id>1;
10 const 使用主鍵或者唯一索引,且匹配的結果只有一條記錄 EXPLAIN SELECT * FROM user WHERE id=1;
11 system const 連接類型的特例,查詢的表爲系統表  
  • possible_keys:可能使用的索引

注意不一定會使用。查詢涉及到的字段上若存在索引,則該索引將被列出來。當該列爲 NULL時就要考慮當前的SQL是否需要優化了。

  • key:實際使用的索引

顯示MySQL在查詢中實際使用的索引,若沒有使用索引,顯示爲NULL。

TIPS:查詢中若使用了覆蓋索引(覆蓋索引:索引的數據覆蓋了需要查詢的所有數據),則該索引僅出現在key列表中。select_type爲index_merge時,這裏可能出現兩個以上的索引,其他的select_type這裏只會出現一個。

  • key_length:索引長度

char()、varchar()索引長度的計算公式:

(Character Set:utf8mb4=4,utf8=3,gbk=2,latin1=1) * 列長度 + 1(允許null) + 2(變長列)
  • ref:連接匹配條件

表示上述表的連接匹配條件,即哪些列或常量被用於查找索引列上的值,如果是使用的常數等值查詢,這裏會顯示const,如果是連接查詢,被驅動表的執行計劃這裏會顯示驅動表的關聯字段,如果是條件使用了表達式或者函數,或者條件列發生了內部隱式轉換,這裏可能顯示爲func。

  • rows:估算的結果集數量

返回估算的結果集數目,注意這並不是一個準確值。

  • extra:額外信息

extra的信息非常豐富,常見的有如下4種類型:

id extra 含義
1 Using index 使用覆蓋索引
2 Using where 使用了用where子句來過濾結果集
3 Using filesort 使用文件排序,使用非索引列進行排序時出現,非常消耗性能,儘量優化
4 Using temporary 使用了臨時表

 

四、mysql 優化

  • 儘量少 join

MySQL 的優勢在於簡單,但這在某些方面其實也是其劣勢。MySQL 優化器效率高,但是由於其統計信息的量有限,優化器工作過程出現偏差的可能性也就更多。對於複雜的多表 Join,一方面由於其優化器受限,再者在 Join 這方面所下的功夫還不夠,所以性能表現離 Oracle 等關係型數據庫前輩還是有一定距離。但如果是簡單的單表查詢,這一差距就會極小甚至在有些場景下要優於這些數據庫前輩。

  • 儘量少排序

排序操作會消耗較多的 CPU 資源,所以減少排序可以在緩存命中率高等 IO 能力足夠的場景下會較大影響 SQL 的響應時間。對於MySQL來說,減少排序有多種辦法,比如:上面誤區中提到的通過利用索引來排序的方式進行優化、減少參與排序的記錄條數。非必要不對數據進行排序…

  • 儘量避免 select *

很多人看到這一點後覺得比較難理解,上面不是在誤區中剛剛說 select 子句中字段的多少並不會影響到讀取的數據嗎?

是的,大多數時候並不會影響到 IO 量,但是當我們還存在 order by 操作的時候,select 子句中的字段多少會在很大程度上影響到我們的排序效率,這一點可以通過我之前一篇介紹 MySQL ORDER BY 的實現分析的文章中有較爲詳細的介紹。此外,上面誤區中不是也說了,只是大多數時候是不會影響到 IO 量,當我們的查詢結果僅僅只需要在索引中就能找到的時候,還是會極大減少 IO 量的。

  • 儘量用 join 代替子查詢

雖然 Join 性能並不佳,但是和 MySQL 的子查詢比起來還是有非常大的性能優勢。MySQL 的子查詢執行計劃一直存在較大的問題,雖然這個問題已經存在多年,但是到目前已經發布的所有穩定版本中都普遍存在,一直沒有太大改善。雖然官方也在很早就承認這一問題,並且承諾儘快解決,但是至少到目前爲止我們還沒有看到哪一個版本較好的解決了這一問題。

  • 儘量少 or

當 where 子句中存在多個條件以“或”並存的時候,MySQL 的優化器並沒有很好的解決其執行計劃優化問題,再加上 MySQL 特有的 SQL 與 Storage 分層架構方式,造成了其性能比較低下,很多時候使用 union all 或者是union(必要的時候)的方式來代替“or”會得到更好的效果。

  • 儘量用 union all 代替 union

union 和 union all 的差異主要是前者需要將兩個(或者多個)結果集合並後再進行唯一性過濾操作,這就會涉及到排序,增加大量的 CPU 運算,加大資源消耗及延遲。所以當我們可以確認不可能出現重複結果集或者不在乎重複結果集的時候,儘量使用 union all 而不是 union。

  • 儘量早過濾

這一優化策略其實最常見於索引的優化設計中(將過濾性更好的字段放得更靠前)。在 SQL 編寫中同樣可以使用這一原則來優化一些 Join 的 SQL。比如我們在多個表進行分頁數據查詢的時候,我們最好是能夠在一個表上先過濾好數據分好頁,然後再用分好頁的結果集與另外的表 Join,這樣可以儘可能多的減少不必要的 IO 操作,大大節省 IO 操作所消耗的時間。

  • 避免類型轉換

這裏所說的“類型轉換”是指 where 子句中出現 column 字段的類型和傳入的參數類型不一致的時候發生的類型轉換:①認爲在column_name 上通過轉換函數進行轉換;②直接導致 MySQL(實際上其他數據庫也會有同樣的問題)無法使用索引,如果非要轉換,應該在傳入的參數上進行轉換;③由數據庫自己進行轉換;⑤如果我們傳入的數據類型和字段類型不一致,同時我們又沒有做任何類型轉換處理,MySQL 可能會自己對我們的數據進行類型轉換操作,也可能不進行處理而交由存儲引擎去處理,這樣一來,就會出現索引無法使用的情況而造成執行計劃問題。

  • 優先優化高併發的 SQL,而不是執行頻率低某些“大”SQL

對於破壞性來說,高併發的 SQL 總是會比低頻率的來得大,因爲高併發的 SQL 一旦出現問題,甚至不會給我們任何喘息的機會就會將系統壓跨。而對於一些雖然需要消耗大量 IO 而且響應很慢的 SQL,由於頻率低,即使遇到,最多就是讓整個系統響應慢一點,但至少可能撐一會兒,讓我們有緩衝的機會。

  • 從全局出發優化,而不是片面調整

SQL 優化不能是單獨針對某一個進行,而應充分考慮系統中所有的 SQL,尤其是在通過調整索引優化 SQL 的執行計劃的時候,千萬不能顧此失彼,因小失大。

  • 儘可能對每一條運行在數據庫中的SQL進行 explain

優化 SQL,需要做到心中有數,知道 SQL 的執行計劃才能判斷是否有優化餘地,才能判斷是否存在執行計劃問題。在對數據庫中運行的 SQL 進行了一段時間的優化之後,很明顯的問題 SQL 可能已經很少了,大多都需要去發掘,這時候就需要進行大量的 explain 操作收集執行計劃,並判斷是否需要進行優化。

 

四、mysql 四種隔離級別和MVCC

MySQL四種隔離級別如下:

  • 未提交讀(READ UNCOMMITTED)

這就是上面所說的例外情況了,這個隔離級別下,其他事務可以看到本事務沒有提交的部分修改.因此會造成髒讀的問題(讀取到了其他事務未提交的部分,而之後該事務進行了回滾).這個級別的性能沒有足夠大的優勢,但是又有很多的問題,因此很少使用.

  • 已提交讀(READ COMMITTED)

其他事務只能讀取到本事務已經提交的部分.這個隔離級別有 不可重複讀的問題,在同一個事務內的兩次讀取,拿到的結果竟然不一樣,因爲另外一個事務對數據進行了修改.

  • REPEATABLE READ(可重複讀)

可重複讀隔離級別解決了上面不可重複讀的問題(看名字也知道),但是仍然有一個新問題,就是 幻讀,當你讀取id> 10 的數據行時,對涉及到的所有行加上了讀鎖,此時例外一個事務新插入了一條id=11的數據,因爲是新插入的,所以不會觸發上面的鎖的排斥,那麼進行本事務進行下一次的查詢時會發現有一條id=11的數據,而上次的查詢操作並沒有獲取到,再進行插入就會有主鍵衝突的問題.

這個隔離級別也是Innodb存儲引擎默認的隔離級別.

  • SERIALIZABLE(可串行化)

這是最高的隔離級別,可以解決上面提到的索引問題,因爲他強制將所以的操作串行執行,這會導致併發性能極速下降,因此也不是很常用.Mysql中實施的是自動提交,也就是說默認一個語句未一個事務,當然你可以通過設置AUTOCOMMIT變量來關閉自動提交,也可以通過begin來顯式的開啓一個事務.

MVCC:

MVCC, Multiversion Concurrency Control多版本併發控制。MVCC是行級鎖的一個變種,但是它在很多情況下避免了加鎖操作, 因此服務器的開銷更低(減少了鎖的生產和分配)。雖然實現機制有所不同, 但大都實現了非阻塞的讀操作,寫操作也只鎖定必要的行。

總結:

在實現上,MySQL通過三個列實現對版本的控制,即6字節的事務ID(DB_TRX_ID)字段,7字節的回滾指針(DB_ROLL_PTR)字段 ,6字節的DB_ROW_ID字段。

而實際上InnoDB並非完全意義上的MVCC,因爲沒有實現多版本並存。關鍵在於並存兩字,即在事務執行對數據操作時同時存在多個版本。而無論如何MySQL在事務執行的時候是串行化的,即使這兩個事務幾乎同時發生。其實這正是對數據安全性的一個保障。

MVCC只在 READ  COMMITTED 和 REPEATABLE  READ 兩個隔離級別下工作。其他兩個隔離級別和MVCC不兼容。

MySQL的事務隔離級別定義都是針對讀操作,並且讀操作指的是“當前讀”;MySQL的默認隔離級別RR使用Gap-Lock來解決幻讀,Record-Lock解決髒讀和可重複讀;因此RR級別是通過Next-Key Lock(Gap-Lock + Record-Lock)實現的;MVCC機制是MySQL爲實現一致性非鎖定讀,提高部分讀寫效率而引入的機制。

 

五、mysql 樂觀鎖和悲觀鎖

數據庫管理系統中併發控制的任務是確保在多個事務同時存取數據庫中同一數據不破壞事務的隔離性和統一性以及數據庫的統一性,樂觀鎖和悲觀鎖式併發控制主要採用的技術手段。

1.悲觀鎖:

在關係數據庫管理系統中,悲觀併發控制(悲觀鎖,PCC)是一種併發控制的方法。它可以阻止一個事務以影響其他用戶的方式來修改數據。如果一個事務執行的操作的每行數據應用了鎖,那只有當這個事務鎖釋放,其他事務才能夠執行與該鎖衝突的操作。

悲觀併發控制主要應用於數據爭用激烈的環境,以及發生併發衝突時使用鎖保護數據的成本要低於回滾事務的成本環境。

悲觀鎖,它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度(悲觀),因此在整個暑假處理過程中,將數據處於鎖定狀態。悲觀鎖的實現,一般依靠數據庫提供的鎖機制(推薦教程:MySQL教程)。

數據庫中,悲觀鎖的流程如下:

  • 在對任何記錄進行修改之前,先嚐試爲該記錄加上排他鎖

  • 如果加鎖失敗,說明該記錄正在被修改,那麼當前查詢可能要等待或拋出異常

  • 如果成功加鎖,則就可以對記錄做修改,事務完成後就會解鎖

  • 其間如果有其他對該記錄做修改或加排他鎖的操作,都會等待我們解鎖或直接拋出異常

MySQL InnoDB中使用悲觀鎖:

要使用悲觀鎖,必須關閉mysql數據庫的自動提交屬性,因爲MySQL默認使用autocommit模式,也就是當你執行一個更新操作後,MySQL會立即將結果進行提交

//開始事務begin;/begin work;/start transaction;(三者選一個)
select status from t_goods where id=1 for update;
//根據商品信息生成訂單insert into t_orders (id,goods_id) values (null,1);
//修改商品status爲2update t_goods set status=2;
// 提交事務commit;/commit work;

以上查詢語句中,使用了select...for update方式,通過開啓排他鎖的方式實現了悲觀鎖。則相應的記錄被鎖定,其他事務必須等本次事務提交之後才能夠執行。

我們使用select ... for update會把數據給鎖定,不過我們需要注意一些鎖的級別,MySQL InnoDB默認行級鎖。行級鎖都是基於索引的,如果一條SQL用不到索引是不會使用行級鎖的,會使用表級鎖把整張表鎖住。

特點:

爲數據處理的安全提供了保證。效率上,由於處理加鎖的機制會讓數據庫產生額外開銷,增加產生死鎖機會。在只讀型事務中由於不會產生衝突,也沒必要使用鎖,這樣會增加系統負載,降低並行性。

2.樂觀鎖:

樂觀併發控制也是一種併發控制的方法。

假設多用戶併發的事務在處理時不會彼此互相影響,各事務能夠在不產生鎖的情況下處理各自影響的那部分數據,在提交數據更新之前,每個事務會先檢查在該事務讀取數據後,有沒其他事務修改該數據,如果有則回滾正在提交的事務。

樂觀鎖相對悲觀鎖而言,是假設數據不會發生衝突,所以在數據進行提交更新的時候,纔會正式對數據的衝突與否進行檢測,如果發現衝突了,則讓返回用戶錯誤信息,讓用戶決定如何做。

樂觀鎖實現一般使用記錄版本號,爲數據增加一個版本標識,當更新數據的時候對版本標識進行更新。

實現:

使用版本號時,可以在數據初始化時指定一個版本號,每次對數據的更新操作都對版本號執行+1操作。並判斷當前版本號是不是該數據的最新版本號

1.查詢出商品信息select (status,status,version) from t_goods where id=#{id}
2.根據商品信息生成訂單
3.修改商品status爲2update t_goodsset status=2,version=version+1where id=#{id} and version=#{version};

特點:

樂觀併發控制相信事務之間的數據競爭概率是較小的,因此儘可能直接做下去,直到提交的時候纔去鎖定,所以不會產生任何鎖和死鎖

整理了學習資料以及學習視頻,送給小夥伴們。點擊【學習資料】自行領取。和一些小夥伴們建了一個技術交流羣,一起探討技術、分享技術資料,旨在共同學習進步,如果感興趣就掃碼加入我們吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章