Mysql多表查询效率的研究(一)
Mysql多表查詢效率的研究(一)
本文探究了mysql InnoDB引擎在多表查詢的應用場景下,使用子表、內連接和左聯接運行速度的差別,并且比較了索引使用與否對查詢效率的影響。
第一部分簡略地概括了索引、子表查詢、聯接查詢的算法和數據結構;
第二部分探討索引的使用策略和查詢語句的優化并進行測試;
第三部分在前兩部分的基礎上進一步討論mysql高性能的實現。
一、數據結構基礎
索引原理
索引:INDEX,官方介紹索引是幫助MySQL高效獲取數據的數據結構。筆者理解索引相當于一本書的目錄,通過目錄就知道要的資料在哪里,不用一頁一頁查閱找出需要的資料。
為什么需要索引(Why is it needed)?
當數據保存在磁盤類存儲介質上時,它是作為數據塊存放。這些數據塊是被當作一個整體來訪問的,這樣可以保證操作的原子性。硬盤數據塊存儲結構類似于鏈表,都包含數據部分,以及一個指向下一個節點(或數據塊)的指針,不需要連續存儲。
記錄集只能在某個關鍵字段上進行排序,所以如果需要在一個無序字段上進行搜索,就要執行一個線性搜索(Linear Search)的過程,平均需要訪問N/2的數據塊,N是表所占據的數據塊數目。如果這個字段是一個非主鍵字段(也就是說,不包含唯一的訪問入口),那么需要在N個數據塊上搜索整個表格空間。
但是對于一個有序字段,可以運用二分查找(Binary Search),這樣只要訪問log2 (N)的數據塊。這就是為什么性能能得到本質上的提高。但是,每種查找算法都只能應用于特定的數據結構之上,例如二分查找要求被檢索數據有序,而二叉樹查找只能應用于二叉查找樹上,但是數據本身的組織結構不可能完全滿足各種數據結構(例如,理論上不可能同時將兩列都按順序進行組織),所以,在數據之外,數據庫系統還維護著滿足特定查找算法的數據結構,這些數據結構以某種方式引用(指向)數據,這樣就可以在這些數據結構上實現高級查找算法。這種數據結構,就是索引。
由于索引只是用來加速數據查詢,那么顯然對只是用來輸出的字段建立索引會浪費磁盤空間以及發生插入、刪除操作時的處理時間,所以這種情況下應該盡量避免。此外鑒于二分搜索的特性,數據的基數或獨立性是很重要的。在基數為2的字段上建立索引,將把數據分割一半,而基數為1000則將返回大約1000條記錄。低基數的二分查找效率將降低為一個線性排序,而且查詢優化器可能會在基數小于記錄數某個比例時(如30%)的情況下將避免使用索引而直接查詢原表,所以這種情況下的索引浪費了空間。
B-Tree:B-Tree是一種多路搜索樹(并不是二叉的):
1.定義任意非葉子結點最多只有M個兒子;且M>2;
2.根結點的兒子數為[2, M];
3.除根結點以外的非葉子結點的兒子數為[M/2, M];
4.每個結點存放至少M/2-1(取上整)和至多M-1個關鍵字;(至少2個關鍵字)
5.非葉子結點的關鍵字個數=指向兒子的指針個數-1;
6.非葉子結點的關鍵字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
7.非葉子結點的指針:P[1], P[2], …, P[M];其中P[1]指向關鍵字小于K[1]的子樹,P[M]指向關鍵字大于K[M-1]的子樹,其它P[i]指向關鍵字屬于(K[i-1], K[i])的子樹;
8.所有葉子結點位于同一層;
如:(M=3)
B+Tree:B+樹是B-樹的變體,也是一種多路搜索樹:
1.其定義基本與B-樹同,除了:
2.非葉子結點的子樹指針與關鍵字個數相同;
3.非葉子結點的子樹指針P[i],指向關鍵字值屬于[K[i], K[i+1])的子樹(B-樹是開區間);
5.為所有葉子結點增加一個鏈指針;
6.所有關鍵字都在葉子結點出現;
如:(M=3)
為什么選用B+Tree因為B+樹不在節點中儲存數據,那么一個磁盤塊中可以儲存更多的B+樹非葉子節點,在對相同大小的數據建立索引時,B+樹的度數更少,深度降低,那么查找時間也相應降低。同時B+樹的雙親節點保存的是最小的數值,在SELECT時有效。最重要的是,磁盤讀取數據時會進行一次“預讀”,大小通常為頁的整數倍。即使只需要讀取一個字節,磁盤也會讀取一頁的數據(通常為4K)放入內存,內存與磁盤以頁為單位交換數據。因為局部性原理認為,通常一個數據被用到,其附近的數據也會立馬被用到。每次新建節點時,直接申請一個頁的空間,這樣就保證一個節點物理上也存儲在一個頁里,加之計算機存儲分配都是按頁對齊的,就實現了一個node只需一次I/O。并把B+tree中的m值設的非常大,就會讓樹的高度降低,有利于一次完全載入。
聚簇索引
所謂聚簇索引,就是指主索引文件和數據文件為同一份文件,聚簇索引主要用在Innodb存儲引擎中。在該索引實現方式中B+Tree的葉子節點上的data就是數據本身,key為主鍵,如果是一般索引的話,data便會指向對應的主索引,如下圖所示:
在B+Tree的每個葉子節點增加一個指向相鄰葉子節點的指針,就形成了帶有順序訪問指針的B+Tree。做這個優化的目的是為了提高區間訪問的性能,例如圖4中如果要查詢key為從18到49的所有數據記錄,當找到18后,只需順著節點和指針順序遍歷就可以一次性訪問到所有數據節點,極大提到了區間查詢效率。
在InnoDB的表建立時會默認選擇UNIQUE的Auto-Increase字段作為葉子節點的主索引。如果沒有unique的字段,則MySQL自動為InnoDB表生成一個隱含字段作為主鍵,這個字段長度為6個字節,類型為長整形。并且一些經常會被修改、插入、刪除的字段不適合作為聚簇索引的主索引。因為磁盤上數據保存的位置是和主索引息息相關的。如果數據項之間的大小關系改變了,那么B+樹必須更改葉子節點和相關祖先節點的位置以適應新的數據。
非聚簇索引就是指B+Tree的葉子節點上的data,并不是數據本身,而是數據存放的地址。主索引和輔助索引沒啥區別,只是主索引中的key一定得是唯一的。主要用在MyISAM存儲引擎中,如下圖:
聚簇索引與非聚簇索引的不同
InnoDB的數據文件本身就是索引文件,而MyISAM儲存的是數據的地址;
InnoDB的輔助索引data域存儲相應記錄主鍵的值而不是地址,而MyISAM無論是主鍵索引還是輔助索引都記錄是數據地址;
聚集索引這種實現方式使得按主鍵的搜索十分高效,但是輔助索引搜索需要檢索兩遍索引:首先檢索輔助索引獲得主鍵,然后用主鍵到主索引中檢索獲得記錄。
單列索引與多列索引
索引可以是單列索引也可以是聯合索引(也叫復合索引)。按照上面形式創建出來的索引是單列索引,現在先看看創建聯合(多列)索引:
ALTER TABLE table ADD INDEX index(a,b,c)
注意:INDEX(a, b, c)可以當做a或(a, b)的索引來使用,但和b、c或(b,c)的索引來使用這是一個最左前綴的優化方法。
如果一個查詢where子句中確實不需要聯合索引中的某一列,那就用“補洞”。
聯表查詢原理
笛卡爾積(交叉連接) 在MySQL中可以為CROSS JOIN或者省略CROSS即JOIN,或者使用’,’ 如:
由于其返回的結果為被連接的兩個數據表的乘積,因此當有WHERE, ON或USING條件的時候一般不建議使用,因為當數據表項目太多的時候,會非常慢。一般使用LEFT [OUTER] JOIN或者RIGHT [OUTER] JOIN
MySQL中的外連接,分為左外連接LEFT [OUTER] JOIN和右連接RIGHT [OUTER] JOIN,即除了返回符合連接條件的結果之外,還要返回左表(左連接)或者右表(右連接)中不符合連接條件的結果,相對應的使用NULL對應。
到下一個表中尋打匹配的行,依次下去,直到描述到所表表中匹配的行為止。然后根據各個表匹配的行,返回查詢中需要的各個列。mysql會嘗試在最后一個關聯表中打到所有匹配的行,如果最后一個關聯表無法找到更多的行以后,mysql返回到上一層次關聯表,看是否能夠找到更多的匹配記錄,依此類推迭代執行。
按照這樣的方式查找第一條表記錄,再嵌套查詢下一個關聯表,然后回溯到上一個表,在mysql中是通過嵌套循環的方式來實現的–正如其名‘嵌套循環關聯’。
MySQL如何優化LEFT JOIN和RIGHT JOIN
在MySQL中,A LEFT JOIN B join_condition執行過程如下:
1)· 根據表A和A依賴的所有表設置表B。
2)· 根據LEFT JOIN條件中使用的所有表(除了B)設置表A。
3)· LEFT JOIN條件用于確定如何從表B搜索行。(換句話說,不使用WHERE子句中的任何條件)。
4)· 可以對所有標準聯接進行優化,只是只有從它所依賴的所有表讀取的表例外。如果出現循環依賴關系,MySQL提示出現一個錯誤。
5)· 進行所有標準WHERE優化。
6)· 如果A中有一行匹配WHERE子句,但B中沒有一行匹配ON條件,則生成另一個B行,其中所有列設置為NULL。
7)· 如果使用LEFT JOIN找出在某些表中不存在的行,并且進行了下面的測試:WHERE部分的col_name IS NULL,其中col_name是一個聲明為 NOT NULL的列,MySQL找到匹配LEFT JOIN條件的一個行后停止(為具體的關鍵字組合)搜索其它行。
RIGHT JOIN的執行類似LEFT JOIN,只是表的角色反過來。
8)大表left join小表
9)為經常使用的字段建立索引
使用子查詢可以一次性的完成很多邏輯上需要多個步驟才能完成的SQL操作,同時也可以避免事務或者表鎖死,并且寫起來也很容易。但是,有些情況下,子查詢可以被更有效率的連接JOIN 替代,是因為 mysql不需要在內存中創建臨時表來完成這個邏輯上的需要兩個步驟的查詢工作。
相關子查詢,尤其是使用不到索引時效率或非常低,可改寫成join方式,因為join時內表中的一條記錄可能跟外表中的多條記錄匹配,所以最終會比使用相關子查詢的方式多出一些重復的記錄結果,故使用group by去重復,當然也可以使用distinct關鍵字,兩者原理相同。如果重復值對于最終需求并沒有什么影響則可以移除該從句以避免分組、排序造成的臨時表和文件排序等額外開銷,提高查詢效率。
二、查詢策略與測試
為了探討查詢語句和INDEX的策略,我選用了Mysql官方提供的employees數據庫。下圖是employees的ER圖:
首先下載在mysql的test_db-master.zip,解壓后導入shell> mysql -t < employees.sql。同時為了控制變量,我刪除了所有外鍵和index,如下圖所示:
下一步開啟PROFILES:set profiling=1.之后profile會記錄最近的十條語句與他們的執行狀態。用show profiles和show profile for query 1可以看到具體的信息包括數據分發、統計、打開表格的時間等。mysql官方文檔有具體的語法解釋。
再開啟慢查詢日志:默認情況下slow_query_log的值為OFF,表示慢查詢日志是禁用的,可以通過設置slow_query_log的值來開啟show variables like ‘%slow_query_log%’;
set global slow_query_log=1;
在my.ini或my.cnf(Linux)中可以設置閾值:
slow_query_log =1
slow_query_log_file=/tmp/mysql_slow.log
并且為了測試要求,我們設置read的超時時間:
然后重啟mysql。
設置mysql的buffer:
SHOW VARIABLES LIKE ‘%query_cache%’;
由于my.cnf文件的優化設置是與服務器硬件配置息息相關的,因而我們指定一個假想的服務器硬件環境。
以下只列出my.cnf文件中[mysqld]段落中的內容,其他段落內容對MySQL運行性能影響甚微,因而姑且忽略。
禁止MySQL對外部連接進行DNS解析,使用這一選項可以消除MySQL進行DNS解析的時間。但需要注意,如果開啟該選項,則所有遠程主機連接授權都要使用IP地址方式,否則MySQL將無法正常處理連接請求!
back_log = 384指定MySQL可能的連接數量。當MySQL主線程在很短的時間內接收到非常多的連接請求,該參數生效,主線程花費很短的時間檢查連接并且啟動一個新線程。
back_log參數的值指出在MySQL暫時停止響應新請求之前的短時間內多少個請求可以被存在堆棧中。 如果系統在一個短時間內有很多連接,則需要增大該參數的值,該參數值指定到來的TCP/IP連接的偵聽隊列的大小。不同的操作系統在這個隊列大小上有它自己的限制。
試圖設定back_log高于你的操作系統的限制將是無效的。默認值為50。對于Linux系統推薦設置為小于512的整數。
key_buffer_size = 256M# key_buffer_size指定用于索引的緩沖區大小,增加它可得到更好的索引處理性能。 對于內存在4GB左右的服務器該參數可設置為256M或384M。 注意:該參數值設置的過大反而會是服務器整體效率降低!
max_allowed_packet = 4M
thread_stack = 256K
table_cache = 128K
sort_buffer_size = 6M
查詢排序時所能使用的緩沖區大小。注意:該參數對應的分配內存是每連接獨占!如果有100個連接,那么實際分配的總共排序緩沖區大小為100 × 6 = 600MB。所以,對于內存在4GB左右的服務器推薦設置為6-8M。 `
**EXPLAIN**顯示了MySQL如何使用索引來處理SELECT語句以及連接表。可以幫助選擇更好的索引和寫出更優化的查詢語句。
使用方法,在select語句前加上EXPLAIN就可以了:EXPLAIN format=json SELECT…..“。
EXPLAIN列的解釋:
| select_type | 顯示select的類型(是否使用UNION)。 |
| table | 顯示這一行的數據是關于哪張表的。 |
| type | 這是重要的列,顯示連接使用了何種類型。從最好到最差的連接類型為 const、eq_reg、ref、range、index和ALL。 |
| possible_keys | 顯示可能應用在這張表中的索引。如果為空,沒有可能的索引。可以為相關的域從WHERE語句中選擇一個合適的語句。 |
| key | 實際使用的索引。如果為NULL,則沒有使用索引。很少的情況下,MySQL會選擇優化不足的索引。這種情況下,可以在SELECT語句中使用USE INDEX(indexname) 來強制使用一個索引或者用IGNORE INDEX(indexname)來強制MySQL忽略索引。 |
| key_len | 使用的索引的長度。在不損失精確性的情況下,長度越短越好。 |
| ref | 顯示索引的哪一列被使用了,如果可能的話,是一個常數。 |
| rows | MySQL認為必須檢查的用來返回請求數據的行數。 |
| filtered | 表示表中符合過濾條件的行數所占的百分比 |
| Extra | 關于MySQL如何解析查詢的額外信息。可以查看官方文檔https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain-extra-information,但這里可以看到的壞的例子是Using temporary和Using filesort,意思MySQL根本不能使用索引,結果是檢索會很慢。 |
Mysql WorkBench提供了explain的可視化功能,有興趣的也可以嘗試:
臨時表
在order by 和子查詢時,有可能會使用臨時表暫存數據,explain的Extra會顯示using temporary。或者SHOW STATUS LIKE 'CREATE%'查看。在mysql中,MySQL臨時表分為“內存臨時表”和“磁盤臨時表”,其中內存臨時表使用MySQL的MEMORY存儲引擎,磁盤臨時表使用MySQL的MyISAM存儲引擎;內存臨時表的大小通過SHOW VARIABLES LIKE '%HEAP%'或者SHOW VARIABLES LIKE '%tmp_table_size%';可以找到,如果臨時表的容量超過限制,那么就會在硬盤中儲存臨時表(Linux的/tmp),大大增加IO操作。
同樣,在以下及幾種場景中,mysql會使用臨時表:
直接使用磁盤臨時表的場景:
1)GROUP BY 或者 DISTINCT 子句中包含長度大于512字節的列;
2)使用UNION或者UNION ALL時,SELECT子句中包含大于512字節的列;
使用臨時表一般都意味著性能比較低,特別是使用磁盤臨時表,性能更慢,因此我們在實際應用中應該盡量避免臨時表的使用。 常見的避免臨時表的方法有:
1)創建索引:在ORDER BY或者GROUP BY的列上創建索引;
2)分拆很長的列:一般情況下,TEXT、BLOB,大于512字節的字符串,基本上都是為了顯示信息,而不會用于查詢條件, 因此表設計的時候,應該將這些列獨立到另外一張表。
3 ) 拆分使用union或union all的復雜sql語句,可以使用in或者join。
4)優化DISTINCT,即DISTINCT語句被優化轉換為GROUP BY操作或者利用UNIQUE INDEX消除DISTINCT。
子查詢
為了避免多個索引使事情變復雜(MySQL的SQL優化器在多索引時行為比較復雜),這里我們將輔助索引drop掉:
查看employees所有表的內外鍵約束:
查看數據庫所有表的索引:
select * from information_schema.STATISTICS WHERE TABLE_SCHEMA='employees';刪除指定的鍵:
DROP INDEX indexname ON tablename為表添加索引:
mysql> alter table `departments` add index test(dept_name);以上的操作是一些基本的index操作語句。下面進行正式的測試,我們選擇了dept_emp (331.5K rows)和employees(298.8K rows)作為樣本,首先執行兩張表之間的子查詢語句:
EXPLAIN SELECT t1.to_date FROM dept_emp t1 WHERE EXISTS(SELECT 1FROM employees t2WHERE t1.to_date = t2.hire_date);結果為
發現執行時間為773s。EXPLAIN的結果顯示sql對dept_emp的33萬行employees的30萬進行全表查詢。
聯表查詢
再換成交叉連接語句執行聯合查詢:
SELECT dept_emp.to_date,employees.hire_date FROM dept_emp,employees WHERE dept_emp.to_date=employees.hire_date ;運行時間1.25s。交叉連接的執行計劃為:
我們可以看到,mysql用employees作為驅動表執行block nested loop算法。
再試試left join
duration為1.7s。因為dept_emp的數據量比employees大,那可以用dept_emp RIGHT JOIN employees 讓employees作為驅動表。在mysql用 nested-loop join (NLJ)算法計算時,會使用驅動表A中符合條件的字段作為過濾條件來過濾表B,那么算法復雜度相當于count(A)*E(count(B)|A=B),所以用小表作為驅動,如dept_emp RIGHT JOIN employees,可以降低復雜度。
當然在這里,使用dept_emp RIGHT JOIN employees相當于交叉連接,同樣是小表作為驅動表。
我們可以看到一共執行時間為0.06s,因為dept_emp,employees等表都是數據量10萬+的,所以內鏈接在沒有索引的情況下需要遍歷10萬*10萬*10萬的數據。我們可以再看看profile的記錄:
其中opening tables 耗時0.03左右,極大地占用了查詢時間。
優化:建立索引
下面我們對這三張表建立索引,首先測試字段的唯一性:字段唯一性的公式為Index Selectivity = Cardinality / #T,其中Cardinality為該字段的不重復數據的數量,#T為該字段的總數據量。
to_date的唯一性還不夠(不足0.9),那么再嘗試(to_date,from_date)的組合:
唯一性比單純一條to_date更優化。那么單單針對本次date=date的查詢設定聯合索引(to_data,from_data):
同時為其他兩張表建立index。再刷新緩存:
使用了index后耗時下降了一半,并且我們看到key列里面表示where時使用了index。
opening tables的時間相同但是checking permissions的時間大大降低。
視圖
視圖算法:系統對視圖以及外部查詢視圖的select語句的一種解析方式。
視圖算法分為三種:
undefined:未定義(默認的),這不是一種實際使用的算法,是一種推卸責任的算法—-告訴系統,視圖沒有定義算法,你看著辦。
temptable:臨時表算法;系統應該先執行視圖的select語句,后執行外部查詢的語句。
merge:合并算法;系統應該先將視圖對應的select語句與外部查詢視圖的select語句進行合并,然后執行(效率高),系統會默認嘗試該算法,但是并不是所有的視圖都可以自動條件下推,比如查詢語句中有Aggregate functions (SUM(), MIN(), MAX(), COUNT(), and so forth)、DISTINCT】GROUP BY、HAVING、LIMIT、UNION or UNION ALL、Subquery in the select list、Refers only to literal values (in this case, there is no underlying table)。
在創建視圖的時候指定算法:
create algorithm = 指定算法 view view_name as select ...視圖算法選擇:如果視圖的select語句 中會包含一個查詢子句,而且很有可能順序比外部的查詢語句要靠后;則選擇使用temptable,其他情況可以不用指定(默認即可)。
三、總結
參考
MySQL索引背后的數據結構及算法原理
總結
以上是生活随笔為你收集整理的Mysql多表查询效率的研究(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 旅行网站模板
- 下一篇: adb刷入第三方recovery_PE