第7章:最佳化

目錄

7.1. 最佳化概述
7.1.1. MySQL設計局限與折衷
7.1.2. 為可移植性設計應用程式
7.1.3. 我們已將MySQL用在何處?
7.1.4. MySQL基準套件
7.1.5. 使用自己的基準
7.2. 最佳化SELECT語句和其它查詢
7.2.1. EXPLAIN語法(獲取SELECT相關訊息)
7.2.2. 估計查詢性能
7.2.3. SELECT查詢的速度
7.2.4. MySQL怎樣最佳化WHERE子句
7.2.5. 範圍最佳化
7.2.6. 索引合併最佳化
7.2.7. MySQL如何最佳化IS NULL
7.2.8. MySQL如何最佳化DISTINCT
7.2.9. MySQL如何最佳化LEFT JOIN和RIGHT JOIN
7.2.10. MySQL如何最佳化嵌套Join
7.2.11. MySQL如何簡化外部聯合
7.2.12. MySQL如何最佳化ORDER BY
7.2.13. MySQL如何最佳化GROUP BY
7.2.14. MySQL如何最佳化LIMIT
7.2.15. 如何避免資料表掃瞄
7.2.16. INSERT語句的速度
7.2.17. UPDATE語句的速度
7.2.18. DELETE語句的速度
7.2.19. 其它最佳化技巧
7.3. 鎖定事宜
7.3.1. 鎖定方法
7.3.2. 資料表鎖定事宜
7.4. 最佳化資料庫結構
7.4.1. 設計選擇
7.4.2. 使您的數據盡可能小
7.4.3. 列索引
7.4.4. 多列索引
7.4.5. MySQL如何使用索引
7.4.6. MyISAM鍵高速緩衝
7.4.7. MyISAM索引統計集合
7.4.8. MySQL如何計算打開的資料表
7.4.9. MySQL如何打開和關閉資料表
7.4.10. 在同一個資料庫中建立多個資料表的問題
7.5. 最佳化MySQL伺服器
7.5.1. 系統因素和啟動參數的調節
7.5.2. 調節伺服器參數
7.5.3. 控制查詢最佳化器的性能
7.5.4. 編譯和連結怎樣影響MySQL的速度
7.5.5. MySQL如何使用內存
7.5.6. MySQL如何使用DNS
7.6. 磁盤事宜
7.6.1. 使用符號連結

最佳化是一個複雜的任務,因為最終要求瞭解整個待最佳化的系統。儘管可以進行局部最佳化而不需要瞭解系統或應用程式,為了最佳化得更好,您必須知道更多的訊息。

本章解釋並給出不同的最佳化MySQL的方法示範。但要記住總有一些其它方法使系統更快,儘管需要更多的工作。

7.1. 最佳化概述

使一個系統更快的最重要因素當然是基本設計。此外,還需要知道系統正做什麼樣的事情,以及瓶頸是什麼。

最常見的系統瓶頸是:

·         內存帶寬。當CPU需要的數據超出CPU緩存時,主緩存帶寬就成為內存的一個瓶頸。這在大多數系統正是一個不常見的瓶頸但是您應該知道它。

7.1.1. MySQL設計局限與折衷

當使用MyISAM儲存引擎時,MySQL使用極快速的資料表鎖定,以便允許多次讀或一次寫。使用該儲存引擎的最大問題出現在同一個資料表中進行混合穩定數據流更新與慢速選擇。如果這只是某些資料表的問題,您可以使用另一個儲存引擎。參見第15章:儲存引擎和資料表類型

MySQL可以使用事務資料表和非事務資料表。為了更容易地讓非事務資料表順利工作(如果出現問題不能回滾)MySQL採用下述規則。請注意這些規則只適用於不運行在嚴格模式下或為INSERTUPDATE使用IGNORE規定程式時。

·         所有列有預設值。請注意當運行在嚴格SQL模式(包括TRADITIONAL SQL模式)時,必須為NOT NULL列指定預設值。

·         如果向列內插入不合適的或超出範圍的值,MySQL將該列設定為「最好的可能的」,而不是報告錯誤。對於數字值,為0、可能的最小值或最大值。對於字串,為空字串或列內可以保存的字串。請注意當運行在嚴格模式或TRADITIONAL SQL模式時該行為不 適用。

·         所有資料表達式的計算結果返回一個資料表示錯誤狀況的信號。例如,1/0返回NULL(使用ERROR_FOR_DIVISION_BY_ZERO SQL模式可以更改該行為)

如果正使用非事務資料表,不應該使用MySQL來檢查列的內容。一般情況,最安全的(通常是最快的)方法徑是讓應用程式確保只向資料庫傳遞合法值。

相關詳細訊息參見1.8.6節,「MySQL處理約束的方式」13.2.4節,「INSERT語法」5.3.2節,「SQL伺服器模式」

7.1.2. 為可移植性設計應用程式

因為不同SQL伺服器實現了標準SQL的不同部分,需要花功夫來編寫可移植的SQL應用程式。對很簡單的選擇/插入,很容易實現移植,但是需要的功能越多則越困難。如果想要應用程式對很多資料庫系統都快,它變得更難!

為了使一個複雜應用程式可移植,您需要選擇它應該工作的SQL伺服器,並確定這些伺服器支援什麼特性。

所有資料庫都有一些弱點。這就是它們不同的設計折衷導致的不同行為。

可以使用MySQLcrash-me程式來找出能用於資料庫伺服器選擇的函數、類型和限制。crash-me並不能找出所有的特性,但是其廣度仍然很合理,可以進行大約450個測試。

crash-me可以提供的一種類型的訊息的例子:如果想要使用InformixDB2,不應該使用超過18個字元的列名。

crash-me程式和MySQL基準程式是獨立於資料庫的。通過觀察它們是如何編寫的,編可以知道必須為編寫獨立於資料庫的應用程式做什麼。基準本身可在MySQL原始碼分發的「sql-bench」目錄下找到。它們用DBI資料庫接口以Perl寫成。使用DBI本身即可以解決部分移植性問題,因為它提供與資料庫無關的的存取方法。

關於crash-me結果,訪問http://dev.mysql.com/tech-resources/crash-me.php。到http://dev.mysql.com/tech-resources/benchmarks/看這個基準的結果。

如果您為資料庫的獨立性而努力,需要很好地瞭解每個SQL伺服器的瓶頸。例如,MySQL在檢索和更新MyISAM資料表記錄方面很快,但是在同一個資料表上混合慢速讀者和寫者方面有一個問題。另一方面,當您試圖訪問最近更新了(直到它們被刷新到磁盤上)的行時,在Oracle中有一個很大的問題。事務資料庫總的來說在從記錄檔案資料表中生成總結資料表方面不是很好,因為在這種情況下,行鎖定幾乎沒有用。

為了使應用程式「確實」獨立於資料庫,需要定義一個容易延伸的接口,用它可操縱數據。因為C++在大多數系統上可以適用,使用資料庫的一個C++ 類接口是有意義的。

如果您使用某個資料庫特定的功能(例如MySQL專用的REPLACE語句),應該為SQL伺服器編碼一個方法以實現同樣的功能。儘管慢些,但確允許其它伺服器執行同樣的任務。

MySQL,可以使用/*! */語法把MySQL特定的關鍵詞加到查詢中。在/**/中的代碼將被其它大多數SQL伺服器視為註釋(並被忽略)

如果高性能真的比準確性更重要,就像在一些web應用程式那樣,一種可行的方法是建立一個應用層,緩存所有的結果以便得到更高的性能。通過只是讓舊的結果在短時間後『過期』,能保持緩存合理地刷新。這在極高負載的情況下是相當不錯的,在此情況下,能動態地增加緩存並且設定較高的過期時限直到一切恢復正常。

在這種情況下,資料表建立訊息應該包含緩存初始大小和資料表刷新頻率等訊息。

實施應用程式緩存的一種方法是使用MySQL查詢緩存。啟用查詢緩存後,伺服器可以確定是否可以重新使用查詢結果。這樣簡化了您的應用程式。參見5.13節,「MySQL查詢高速緩衝」

7.1.3. 我們已將MySQL用在何處?

該節描述了Mysql的早期應用程式。

MySQL最初開發期間,MySQL的功能適合大多數客戶。MySQL為瑞典的一些最大的零售商處理數據倉庫。

我們從所有商店得到所有紅利卡交易的每週總結,並且我們期望為所有店主提供有用的訊息以幫助他們得出他們的廣告戰如何影響他們的顧客。

數據是相當巨量的(大約每月7百萬宗交易總結)並且我們保存4-10年來的數據需要呈現給用戶。我們每週從顧客那裡得到請求,他們想要「立刻」訪問來自該數據的新報告。

我們通過每月將所有訊息儲存在壓縮的「交易」資料表中來解決它。我們有一套簡單的宏/指令用來生成來自交易資料表的不同條件( 產品組、顧客id,商店...)的總結資料表。報告是由一個進行語法分析網頁的小perl指令動態生成的網頁,在指令中執行SQL語句並且插入結果。我們很想使用PHPmod_perl,但是那時它們還不可用。

對圖形數據,我們用C語言編寫了一個簡單的工具,它能基於那些結果處理SQL查詢結果並生成GIF圖形。該工具也從分析Web網頁的perl指令中動態地執行。

在大多數情況下,一個新的報告通過簡單地複製一個現有指令並且修改其中的SQL查詢來完成。在一些情況下,我們將需要把更多的列加到一個現有的總結資料表中或產生一個新的,但是這也相當簡單,因為我們在磁盤上保存所有交易資料表。(目前我們大約有50G的交易資料表和200G的其它顧客數據)

我們也讓我們的顧客直接用ODBC訪問總結資料表以便高級用戶能自己用這些數據進行試驗。

該系統工作得很好,我們可以毫無問題地用很適度的Sun Ultra SPARC工作站硬件(2x200MHz)來處理數據。該系統被逐步移植到了Linux中。

7.1.4. MySQL基準套件

本節應該包含MySQL基準套件(crash-me)的技術描述,但是該描述還沒寫成。目前,您可以通過在MySQL原始碼分發中的「sql-bench」目錄下的代碼和結果瞭解基準套件是如何工作的。

通過基準用戶可以瞭解一個給定的SQL實現在哪方面執行得很好或很糟糕。

注意,這個基準是單線程的,它可以測量操作執行的最小時間。我們計劃將來在基準套件中新增多線程測試。

要使用基準套件,必須滿足下面的要求:

·         基準套件隨MySQL原始碼分發提供。可以從http://dev.mysql.com/downloads/下載分發,或者使用當前的開發原始碼樹(參見2.8.3節,「從開發原始碼樹安裝」)

·         基準指令用Perl編寫而成,使用Perl DBI模塊訪問資料庫伺服器,因此必須安裝DBI。還需要為每個待測試的伺服器提供伺服器專用DBD驅動程式。例如,要測試MySQLPostgreSQLDB2,必須安裝DBD::mysqlDBD::PgDBD::DB2模塊。參見2.13節,「Perl安裝注意事項」

獲得MySQL原始碼分發後,可以在sql-bench目錄找到基準套件。要運行基準測試,應構建MySQL,然後進入sql-bench目錄並執行run-all-tests指令:

shell> cd sql-bench

shell> perl run-all-tests --server=server_name

server_name是一個支援的伺服器。要獲得所有選項和支援的伺服器,使用命令:

shell> perl run-all-tests --help

crash-me指令也位於sql-bench目錄。crash-me嘗試通過實際運行查詢確定資料庫支援的特性以及其功能和限制。例如,它確定:

·         支援什麼列類型

·         支援多少索引

·         支援什麼函數

·         查詢可以多大

·         VARCHAR列可以多大

可以從http://dev.mysql.com/tech-resources/crash-me.php發現許多不同資料庫伺服器的crash-me的結果。關於基準測試結果的詳細訊息,訪問http://dev.mysql.com/tech-resources/benchmarks/

7.1.5. 使用自己的基準

一定要測試應用程式和資料庫,以發現瓶頸在哪兒。通過修正它(或通過用一個「啞模塊」代替瓶頸),可以很容易地確定下一個瓶頸。即使您的應用程式的整體性能目前可以接受,至少應該對每個瓶頸做一個計劃,如果某天確實需要更好的性能,應知道如何解決它。

關於一些可移植的基準程式的例子,參見MySQL基準套件。請參見7.1.4節,「MySQL基準套件」。可以利用這個套件的任何程式並且根據您的需要修改它。通過這樣做,可以嘗試不同的問題的解決方案並測試哪一個是最好的解決方案。

另一個免費基準套件是開放原始碼資料庫基準套件,參見http://osdb.sourceforge.net/

在系統負載繁重時出現一些問題是很普遍的,並且很多客戶已經與我們聯繫了,他們在生產系統中有一個(測試)系統並且有負載問題。大多數情況下,性能問題經證明是與基本資料庫設計有關的問題(例如,資料表掃瞄在高負載時資料表現不好)或作業系統或庫問題。如果系統已經不在生產系統中,它們大多數將容易修正。

為了避免這樣的問題,應該把工作重點放在在可能最壞的負載下測試您的整個應用程式。您可以使用Super Smack。該工具可以從http://jeremy.zawodny.com/mysql/super-smack/獲得。正如它的名字所建議,它可以根據您的需要提供合理的系統,因此確保只用於您的開發系統。

7.2. 最佳化SELECT語句和其它查詢

首先,影響所有語句的一個因素是:您的授權設置得越複雜,所需要的開銷越多。

執行GRANT語句時使用簡單的授權,當客戶執行語句時,可以使MySQL降低授權檢查開銷。例如,如果未授予任何資料表級或列級權限,伺服器不需要檢查tables_privcolumns_priv資料表的內容。同樣地,如果不對任何 帳號進行限制,伺服器不需要對資源進行統計。如果查詢量很高,可以花一些時間使用簡化的授權結構來降低授權檢查開銷。

如果您的問題是與具體MySQL資料表達式或函數有關,可以使用mysql客戶程式所帶的BENCHMARK()函數執行定時測試。其語法為BENCHMARK(loop_count,expression)。例如:

mysql> SELECT BENCHMARK(1000000,1+1)
+------------------------+
| BENCHMARK(1000000,1+1) |
+------------------------+
|                      0 |
+------------------------+
1 row in set (0.32 sec)

上面結果在PentiumII 400MHz系統上獲得。它顯示MySQL在該系統上在0.32秒內可以執行1,000,000個簡單的+資料表達式運算。

所有MySQL函數應該被高度最佳化,但是總有可能有一些例外。BENCHMARK()是一個找出是否查詢有問題的優秀的工具。

7.2.1. EXPLAIN語法(獲取SELECT相關訊息)

EXPLAIN tbl_name

或:

EXPLAIN [EXTENDED] SELECT select_options

EXPLAIN語句可以用作DESCRIBE的一個同義詞,或獲得關於MySQL如何執行SELECT語句的訊息:

·         EXPLAIN tbl_nameDESCRIBE tbl_nameSHOW COLUMNS FROM tbl_name的一個同義詞。

·         如果在SELECT語句前放上關鍵詞EXPLAINMySQL將解釋它如何處理SELECT,提供有關資料表如何聯接和聯接的次序。

該節解釋EXPLAIN的第2個用法。

借助於EXPLAIN,可以知道什麼時候必須為資料表加入索引以得到一個使用索引來尋找記錄的更快的SELECT

如果由於使用不正確的索引出現了問題,應運行ANALYZE TABLE更新資料表的統計(例如關鍵字集的勢),這樣會影響最佳化器進行的選擇。參見13.5.2.1節,「ANALYZE TABLE語法」

還可以知道最佳化器是否以一個最佳次序聯接資料表。為了強制最佳化器讓一個SELECT語句按照資料表命名順序的聯接次序,語句應以STRAIGHT_JOIN而不只是SELECT開頭。

EXPLAIN為用於SELECT語句中的每個資料表返回一行訊息。資料表以它們在處理查詢過程中將被MySQL讀入的順序被列出。MySQL用一遍掃瞄多次聯接(single-sweep multi-join)的方式解決所有聯接。這意味著MySQL從第一個資料表中讀一行,然後找到在第二個資料表中的一個匹配行,然後在第3個資料表中等等。當所有的資料表處理完後,它輸出選中的列並且返回資料表清單直到找到一個有更多的匹配行的資料表。從該資料表讀入下一行並繼續處理下一個資料表。

當使用EXTENDED關鍵字時,EXPLAIN產生附加訊息,可以用SHOW WARNINGS瀏覽。該訊息顯示最佳化器限定SELECT語句中的資料表和列名,重寫並且執行最佳化規則後SELECT語句是什麼樣子,並且還可能包括最佳化過程的其它註解。

EXPLAIN的每個輸出行提供一個資料表的相關訊息,並且每個行包括下面的列:

·         id

SELECT識別符。這是SELECT的查詢序列號。

·         select_type

SELECT類型,可以為以下任何一種:

o        SIMPLE

簡單SELECT(不使用UNION或子查詢)

o        PRIMARY

最外面的SELECT

o        UNION

UNION中的第二個或後面的SELECT語句

o        DEPENDENT UNION

UNION中的第二個或後面的SELECT語句,取決於外面的查詢

o        UNION RESULT

UNION的結果。

o        SUBQUERY

子查詢中的第一個SELECT

o        DEPENDENT SUBQUERY

子查詢中的第一個SELECT,取決於外面的查詢

o        DERIVED

導出資料表的SELECT(FROM子句的子查詢)

·         table

輸出的行所引用的資料表。

·         type

聯接類型。下面給出各種聯接類型,按照從最佳類型到最壞類型進行排序:

o        system

資料表僅有一行(=系統資料表)。這是const聯接類型的一個特例。

o        const

資料表最多有一個匹配行,它將在查詢開始時被讀取。因為僅有一行,在這行的列值可被最佳化器剩餘部分認為是常數。const資料表很快,因為它們只讀取一次!

const用於用常數值比較PRIMARY KEYUNIQUE索引的所有部分時。在下面的查詢中,tbl_name可以用於const資料表:

SELECT * from tbl_name WHERE primary_key=1
 
SELECT * from tbl_name
WHERE primary_key_part1=1primary_key_part2=2

o        eq_ref

對於每個來自於前面的資料表的行組合,從該資料表中讀取一行。這可能是最好的聯接類型,除了const類型。它用在一個索引的所有部分被聯接使用並且索引是UNIQUEPRIMARY KEY

eq_ref可以用於使用= 操作符比較的帶索引的列。比較值可以為常量或一個使用在該資料表前面所讀取的資料表的列的資料表達式。

在下面的例子中,MySQL可以使用eq_ref聯接來處理ref_tables

SELECT * FROM ref_table,other_table
  WHERE ref_table.key_column=other_table.column;
 
SELECT * FROM ref_table,other_table
  WHERE ref_table.key_column_part1=other_table.column
    AND ref_table.key_column_part2=1;

o        ref

對於每個來自於前面的資料表的行組合,所有有匹配索引值的行將從這張資料表中讀取。如果聯接只使用鍵的最左邊的前綴,或如果鍵不是UNIQUEPRIMARY KEY(換句話說,如果聯接不能基於關鍵字選擇單個行的話),則使用ref。如果使用的鍵僅僅匹配少量行,該聯接類型是不錯的。

ref可以用於使用=<=>操作符的帶索引的列。

在下面的例子中,MySQL可以使用ref聯接來處理ref_tables

SELECT * FROM ref_table WHERE key_column=expr;
 
SELECT * FROM ref_table,other_table
  WHERE ref_table.key_column=other_table.column;
 
SELECT * FROM ref_table,other_table
  WHERE ref_table.key_column_part1=other_table.column
    AND ref_table.key_column_part2=1;

o        ref_or_null

該聯接類型如同ref,但是新增了MySQL可以專門搜索包含NULL值的行。在解決子查詢中經常使用該聯接類型的最佳化。

在下面的例子中,MySQL可以使用ref_or_null聯接來處理ref_tables

SELECT * FROM ref_table
WHERE key_column=expr OR key_column IS NULL;

參見7.2.7節,「MySQL如何最佳化IS NULL

o        index_merge

該聯接類型資料表示使用了索引合併最佳化方法。在這種情況下,key列包含了使用的索引的清單,key_len包含了使用的索引的最長的關鍵元素。詳細訊息參見7.2.6節,「索引合併最佳化」

o        unique_subquery

該類型替換了下面形式的IN子查詢的ref

value IN (SELECT primary_key FROM single_table WHERE some_expr)

unique_subquery是一個索引搜尋函數,可以完全替換子查詢,效率更高。

o        index_subquery

該聯接類型類似於unique_subquery。可以替換IN子查詢,但只適合下列形式的子查詢中的非唯一索引:

value IN (SELECT key_column FROM single_table WHERE some_expr)

o        range

只檢索給定範圍的行,使用一個索引來選擇行。key列顯示使用了哪個索引。key_len包含所使用索引的最長關鍵元素。在該類型中ref列為NULL

當使用=<>>>=<<=IS NULL<=>BETWEEN或者IN操作符,用常量比較關鍵字列時,可以使用range

SELECT * FROM tbl_name
WHERE key_column = 10;
 
SELECT * FROM tbl_name
WHERE key_column BETWEEN 10 and 20;
 
SELECT * FROM tbl_name
WHERE key_column IN (10,20,30);
 
SELECT * FROM tbl_name
WHERE key_part1= 10 AND key_part2 IN (10,20,30);

o        index

該聯接類型與ALL相同,除了只有索引樹被掃瞄。這通常比ALL快,因為索引檔案通常比數據檔案小。

當查詢只使用作為單索引一部分的列時,MySQL可以使用該聯接類型。

o        ALL

對於每個來自於先前的資料表的行組合,進行完整的資料表掃瞄。如果資料表是第一個沒標記const的資料表,這通常不好,並且通常在它情況下差。通常可以增加更多的索引而不要使用ALL,使得行能基於前面的資料表中的常數值或列值被檢索出。

·         possible_keys

possible_keys列指出MySQL能使用哪個索引在該資料表中找到行。注意,該列完全獨立於EXPLAIN輸出所示的資料表的次序。這意味著在possible_keys中的某些鍵實際上不能按生成的資料表次序使用。

如果該列是NULL,則沒有相關的索引。在這種情況下,可以通過檢查WHERE子句看是否它引用某些列或適合索引的列來提高您的查詢性能。如果是這樣,創造一個適當的索引並且再次用EXPLAIN檢查查詢。參見13.1.2節,「ALTER TABLE語法」

為了看清一張資料表有什麼索引,使用SHOW INDEX FROM tbl_name

·         key

key列顯示MySQL實際決定使用的鍵(索引)。如果沒有選擇索引,鍵是NULL。要想強制MySQL使用或忽視possible_keys列中的索引,在查詢中使用FORCE INDEXUSE INDEX或者IGNORE INDEX。參見13.2.7節,「SELECT語法」

對於MyISAMBDB資料表,運行ANALYZE TABLE可以幫助最佳化器選擇更好的索引。對於MyISAM資料表,可以使用myisamchk --analyze。參見13.5.2.1節,「ANALYZE TABLE語法」5.9.4節,「資料表維護和崩潰恢復」

·         key_len

key_len列顯示MySQL決定使用的鍵長度。如果鍵是NULL,則長度為NULL。注意通過key_len值我們可以確定MySQL將實際使用一個多部關鍵字的幾個部分。

·         ref

ref列顯示使用哪個列或常數與key一起從資料表中選擇行。

·         rows

rows列顯示MySQL認為它執行查詢時必須檢查的行數。

·         Extra

該列包含MySQL解決查詢的詳細訊息。下面解釋了該列可以顯示的不同的文本字串:

o        Distinct

MySQL發現第1個匹配行後,停止為當前的行組合搜索更多的行。

o        Not exists

MySQL能夠對查詢進行LEFT JOIN最佳化,發現1個匹配LEFT JOIN標準的行後,不再為前面的的行組合在該資料表內檢查更多的行。

下面是一個可以這樣最佳化的查詢類型的例子:

SELECT * t1 LEFT JOIN t2 ON t1.id=t2.id
  WHERE t2.id IS NULL

假定t2.id定義為NOT NULL。在這種情況下,MySQL使用t1.id的值掃瞄t1並搜尋t2中的行。如果MySQLt2中發現一個匹配的行,它知道t2.id絕不會為NULL,並且不再掃瞄t2內有相同的id值的行。換句話說,對於t1的每個行,MySQL只需要在t2中搜尋一次,無論t2內實際有多少匹配的行。

o        range checked for each record (index map: #)

MySQL沒有發現好的可以使用的索引,但發現如果來自前面的資料表的列值已知,可能部分索引可以使用。對前面的資料表的每個行組合,MySQL檢查是否可以使用rangeindex_merge訪問方法來索取行。關於適用性標準的描述參見7.2.5節,「範圍最佳化」7.2.6節,「索引合併最佳化」,不同的是前面資料表的所有列值已知並且認為是常量。

這並不很快,但比執行沒有索引的聯接要快得多。

o        Using filesort

MySQL需要額外的一次傳遞,以找出如何按排序順序檢索行。通過根據聯接類型瀏覽所有行並為所有匹配WHERE子句的行保存排序關鍵字和行的指針來完成排序。然後關鍵字被排序,並按排序順序檢索行。參見7.2.12節,「MySQL如何最佳化ORDER BY

o        Using index

從只使用索引樹中的訊息而不需要進一步搜索讀取實際的行來檢索資料表中的列訊息。當查詢只使用作為單一索引一部分的列時,可以使用該策略。

o        Using temporary

為了解決查詢,MySQL需要建立一個臨時資料表來容納結果。典型情況如查詢包含可以按不同情況列出列的GROUP BYORDER BY子句時。

o        Using where

WHERE子句用於限制哪一個行匹配下一個資料表或發送到客戶。除非您專門從資料表中索取或檢查所有行,如果Extra值不為Using where並且資料表聯接類型為ALLindex,查詢可能會有一些錯誤。

如果想要使查詢盡可能快,應找出Using filesort Using temporaryExtra值。

o        Using sort_union(...), Using union(...), Using intersect(...)

這些函數說明如何為index_merge聯接類型合併索引掃瞄。詳細訊息參見7.2.6節,「索引合併最佳化」

o        Using index for group-by

類似於訪問資料表的Using index方式,Using index for group-by資料表示MySQL發現了一個索引,可以用來查詢GROUP BYDISTINCT查詢的所有列,而不要額外搜索硬盤訪問實際的資料表。並且,按最有效的方式使用索引,以便對於每個組,只讀取少量索引條目。詳情參見7.2.13節,「MySQL如何最佳化GROUP BY

通過相乘EXPLAIN輸出的rows列的所有值,您能得到一個關於一個聯接如何的提示。這應該粗略地告訴您MySQL必須檢查多少行以執行查詢。當您使用max_join_size變數限制查詢時,也用這個乘積來確定執行哪個多資料表SELECT語句。參見7.5.2節,「調節伺服器參數」

下列例子顯示出一個多資料表JOIN如何能使用EXPLAIN提供的訊息逐步被最佳化。

假定您有下面所示的SELECT語句,計劃使用EXPLAIN來檢查它:

EXPLAIN SELECT tt.TicketNumber, tt.TimeIn,
               tt.ProjectReference, tt.EstimatedShipDate,
               tt.ActualShipDate, tt.ClientID,
               tt.ServiceCodes, tt.RepetitiveID,
               tt.CurrentProcess, tt.CurrentDPPerson,
               tt.RecordVolume, tt.DPPrinted, et.COUNTRY,
               et_1.COUNTRY, do.CUSTNAME
        FROM tt, et, et AS et_1, do
        WHERE tt.SubmitTime IS NULL
          AND tt.ActualPC = et.EMPLOYID
          AND tt.AssignedPC = et_1.EMPLOYID
          AND tt.ClientID = do.CUSTNMBR;

對於這個例子,假定:

·         被比較的列聲明如下:

資料表

列類型

tt

ActualPC

CHAR(10)

tt

AssignedPC

CHAR(10)

tt

ClientID

CHAR(10)

et

EMPLOYID

CHAR(15)

do

CUSTNMBR

CHAR(15)

·         資料表有下面的索引:

資料表

索引

tt

ActualPC

tt

AssignedPC

tt

ClientID

et

EMPLOYID(主鍵)

do

CUSTNMBR(主鍵)

·         tt.ActualPC值不是均勻分佈的。

開始,在進行最佳化前,EXPLAIN語句產生下列訊息:

 

table type possible_keys key  key_len ref  rows  Extra
et    ALL  PRIMARY       NULL NULL    NULL 74
do    ALL  PRIMARY       NULL NULL    NULL 2135
et_1  ALL  PRIMARY       NULL NULL    NULL 74
tt    ALL  AssignedPC,   NULL NULL    NULL 3872
           ClientID,
           ActualPC
      range checked for each record (key map: 35)
 

因為type對每張資料表是ALL,這個輸出顯示MySQL正在對所有資料表產生一個笛卡爾乘積;即每一個行的組合!這將花相當長的時間,因為必須檢查每張資料表的行數的乘積!對於一個實例,這是74 * 2135 * 74 * 3872 = 45,268,558,720行。如果資料表更大,您只能想像它將花多長時間……

這裡的一個問題是MySQL能更高效地在聲明具有相同類型和尺寸的列上使用索引。在本文中,VARCHARCHAR是相同的,除非它們聲明為不同的長度。因為tt.ActualPC被聲明為CHAR(10)並且et.EMPLOYID被聲明為CHAR(15),長度不匹配。

為了修正在列長度上的不同,使用ALTER TABLEActualPC的長度從10個字元變為15個字元:

mysql> ALTER TABLE tt MODIFY ActualPC VARCHAR(15);

現在tt.ActualPCet.EMPLOYID都是VARCHAR(15),再執行EXPLAIN語句產生這個結果:

 

table type   possible_keys key     key_len ref         rows    Extra
tt    ALL    AssignedPC,   NULL    NULL    NULL        3872    Using
             ClientID,                                         where
             ActualPC
do    ALL    PRIMARY       NULL    NULL    NULL        2135
      range checked for each record (key map: 1)
et_1  ALL    PRIMARY       NULL    NULL    NULL        74
      range checked for each record (key map: 1)
et    eq_ref PRIMARY       PRIMARY 15      tt.ActualPC 1
 

這不是完美的,但是好一些了:rows值的乘積少了一個因子74。這個版本在幾秒內執行完。

2種方法能消除tt.AssignedPC = et_1.EMPLOYIDtt.ClientID = do.CUSTNMBR比較的列的長度失配問題:

mysql> ALTER TABLE tt MODIFY AssignedPC VARCHAR(15),
    ->                MODIFY ClientID   VARCHAR(15);

EXPLAIN產生的輸出顯示在下面:

table type   possible_keys key      key_len ref           rows Extra
et    ALL    PRIMARY       NULL     NULL    NULL          74
tt    ref    AssignedPC,   ActualPC 15      et.EMPLOYID   52   Using
             ClientID,                                         where
             ActualPC
et_1  eq_ref PRIMARY       PRIMARY  15      tt.AssignedPC 1
do    eq_ref PRIMARY       PRIMARY  15      tt.ClientID   1
 

這幾乎很好了。

剩下的問題是,預設情況,MySQL假設在tt.ActualPC列的值是均勻分佈的,並且對tt資料表不是這樣。幸好,很容易告訴MySQL來分析關鍵字分佈:

mysql> ANALYZE TABLE tt

現在聯接是「完美」的了,而且EXPLAIN產生這個結果:

table type   possible_keys key     key_len ref           rows Extra
tt    ALL    AssignedPC    NULL    NULL    NULL          3872 Using
             ClientID,                                        where
             ActualPC
et    eq_ref PRIMARY       PRIMARY 15      tt.ActualPC   1
et_1  eq_ref PRIMARY       PRIMARY 15      tt.AssignedPC 1
do    eq_ref PRIMARY       PRIMARY 15      tt.ClientID   1

注意在從EXPLAIN輸出的rows列是一個來自MySQL聯接最佳化器的「教育猜測」。您應該檢查數字是否接近事實。如果不是,可以通過在SELECT語句裡面使用STRAIGHT_JOIN並且試著在FROM子句以不同的次序列出資料表,可能得到更好的性能。

7.2.2. 估計查詢性能

在大多數情況下,可以通過計算磁盤搜索來估計性能。對小的資料表,通常能在1次磁盤搜索中找到行(因為索引可能被緩存)。對更大的資料表,可以使用B-樹索引進行估計,將需要log(row_count)/log(index_block_length/3 * 2/(index_length + data_pointer_length))+1次搜索才能找到行。

MySQL中,索引塊通常是1024個字節,數據指針通常是4個字節,這對於有一個長度為3(中等整數)的索引的500,000行的資料表,通過公式可以計算出log(500,000)/log(1024/3*2/(3+4))+1= 4次搜索。

上面的索引需要大約500,000 * 7 * 3/2 = 5.2MB(假設典型情況下索引緩存區填充率為2/3),可以將大部分索引保存在內存中,僅需要1-2使用從OS讀數據來找出行。

然而對於寫,將需要4次搜索請求(如上)來找到在哪兒存放新索引,並且通常需要2次搜索來更新這個索引並且寫入行。

注意,上述討論並不意味著應用程式的性能將緩慢地以logN 退化!當資料表格變得更大時,所有內容緩存到OSSQL伺服器後,將僅僅或多或少地更慢。在數據變得太大不能緩存後,將逐漸變得更慢,直到應用程式只能進行磁盤搜索(logN增加)。為了避免這個問題,隨數據增加而增加 鍵高速緩衝區大小。對於MyISAM資料表, key_buffer_size系統變數控制 鍵高速緩衝區大小。參見7.5.2節,「調節伺服器參數」

7.2.3. SELECT查詢的速度

總的來說,要想使一個較慢速SELECT ... WHERE更快,應首先檢查是否能增加一個索引。不同資料表之間的引用通常通過索引來完成。您可以使用EXPLAIN語句來確定SELECT語句使用哪些索引。參見7.4.5節,「MySQL如何使用索引」7.2.1節,「EXPLAIN語法(獲取關於SELECT的訊息)

下面是一些加速對MyISAM資料表的查詢的一般建議:

·         為了幫助MySQL更好地最佳化查詢,在一個裝載數據後的資料表上運行ANALYZE TABLEmyisamchk --analyze。這樣為每一個索引更新指出有相同值的行的平均行數的值(當然,如果只有一個索引,這總是1。)MySQL使用該方法來決定當您聯接兩個基於非常量資料表達式的資料表時選擇哪個索引。您可以使用SHOW INDEX FROM tbl_name並檢查Cardinality值來檢查資料表分析結果。myisamchk --description --verbose可以顯示索引分佈訊息。

·         要想根據一個索引排序一個索引和數據,使用myisamchk --sort-index --sort-records=1(如果您想要在索引1上排序)。如果只有一個索引,想要根據該索引的次序讀取所有的記錄,這是使查詢更快的一個好方法。但是請注意,第一次對一個大資料表按照這種方法排序時將花很長時間!

7.2.4. MySQL怎樣最佳化WHERE子句

該節討論為處理WHERE子句而進行的最佳化。例子中使用了SELECT語句,但相同的最佳化也適用DELETEUPDATE語句中的WHERE子句。

請注意對MySQL最佳化器的工作在不斷進行中,因此該節並不完善。MySQL執行了大量的最佳化,本文中所列的並不詳盡。

下面列出了MySQL執行的部分最佳化:

·         去除不必要的括號:

·                        ((a AND b) AND c OR (((a AND b) AND (c AND d))))
·                -> (a AND b AND c) OR (a AND b AND c AND d)

·         常量重疊:

·                   (a<b AND b=c) AND a=5
·                -> b>5 AND b=c AND a=5

·         去除常量條件(由於常量重疊需要)

·                   (B>=5 AND B=5) OR (B=6 AND 5=5) OR (B=7 AND 5=6)
·                -> B=5 OR B=6

·         索引使用的常數資料表達式僅計算一次。

  • 對於MyISAMHEAP資料表,在一個單個資料表上的沒有一個WHERECOUNT(*)直接從資料表中檢索訊息。當僅使用一個資料表時,對NOT NULL資料表達式也這樣做。
  • 無效常數資料表達式的早期檢測。MySQL快速檢測某些SELECT語句是不可能的並且不返回行。
  • 如果不使用GROUP BY或分組函數(COUNT()MIN()……)HAVINGWHERE合併。
  • 對於聯接內的每個資料表,構造一個更簡單的WHERE以便更快地對資料表進行WHERE計算並且也盡快跳過記錄。
  • 所有常數的資料表在查詢中比其它資料表先讀出。常數資料表為:
    • 空資料表或只有1行的資料表。
    • 與在一個PRIMARY KEYUNIQUE索引的WHERE子句一起使用的資料表,這裡所有的索引部分使用常數資料表達式並且索引部分被定義為NOT NULL

下列的所有資料表用作常數資料表:

mysql> SELECT * FROM t WHERE primary_key=1;
mysql> SELECT * FROM t1,t2
           WHERE t1.primary_key=1 AND t2.primary_key=t1.id;
  • 嘗試所有可能性便可以找到資料表聯接的最好聯接組合。如果所有在ORDER BYGROUP BY的列來自同一個資料表,那麼當聯接時,該資料表首先被選中。
  • 如果有一個ORDER BY子句和不同的GROUP BY子句,或如果ORDER BYGROUP BY包含聯接隊列中的第一個資料表之外的其它資料表的列,則建立一個臨時資料表。
  • 如果使用SQL_SMALL_RESULTMySQL使用內存中的一個臨時資料表。
  • 每個資料表的索引被查詢,並且使用最好的索引,除非最佳化器認為使用資料表掃瞄更有效。是否使用掃瞄取決於是否最好的索引跨越超過30%的資料表。最佳化器更加複雜,其估計基於其它因素,例如資料表大小、行數和I/O塊大小,因此固定比例不再決定選擇使用索引還是掃瞄。
  • 在一些情況下,MySQL能從索引中讀出行,甚至不查詢數據檔案。如果索引使用的所有列是數值類,那麼只使用索引樹來進行查詢。
  • 輸出每個記錄前,跳過不匹配HAVING子句的行。

下面是一些快速查詢的例子:

SELECT COUNT(*) FROM tbl_name;
 
SELECT MIN(key_part1),MAX(key_part1) FROM tbl_name;
 
SELECT MAX(key_part2) FROM tbl_name
    WHERE key_part1=constant;
 
SELECT ... FROM tbl_name
    ORDER BY key_part1,key_part2,... LIMIT 10;
 
SELECT ... FROM tbl_name
    ORDER BY key_part1 DESC, key_part2 DESC, ... LIMIT 10;

下列查詢僅使用索引樹就可以解決(假設索引的列為數值型)

SELECT key_part1,key_part2 FROM tbl_name WHERE key_part1=val;
 
SELECT COUNT(*) FROM tbl_name
    WHERE key_part1=val1 AND key_part2=val2;
 
SELECT key_part2 FROM tbl_name GROUP BY key_part1;

下列查詢使用索引按排序順序檢索行,不用另外的排序:

SELECT ... FROM tbl_name
    ORDER BY key_part1,key_part2,... ;
 
SELECT ... FROM tbl_name
    ORDER BY key_part1 DESC, key_part2 DESC, ... ;

7.2.5. 範圍最佳化

range訪問方法使用單一索引來搜索包含在一個或幾個索引值距離內的資料表記錄的子集。可以用於單部分或多元素索引。後面的章節將詳細描述如何從WHERE子句提取區間。

7.2.5.1. 單元素索引的範圍訪問方法

對於單元素索引,可以用WHERE子句中的相應條件很方便地資料表示索引值區間,因此我們稱為範圍條件而不是「區間」。

單元素索引範圍條件的定義如下:

·         對於BTREEHASH索引,當使用=<=>INIS NULL或者IS NOT NULL操作符時,關鍵元素與常量值的比較關係對應一個範圍條件。

·         對於BTREE索引,當使用><>=<=BETWEEN!=或者<>,或者LIKE 'pattern'(其中 'pattern'不以通配符開始)操作符時,關鍵元素與常量值的比較關係對應一個範圍條件。

·         對於所有類型的索引,多個範圍條件結合ORAND則產生一個範圍條件。

前面描述的「量值」系指:

·         查詢字串中的常量

·         同一聯接中的constsystem資料表中的列

·         無關聯子查詢的結果

·         完全從前面類型的子資料表達式組成的資料表達式

下面是一些WHERE子句中有範圍條件的查詢的例子:

SELECT * FROM t1 
    WHERE key_col > 1 
    AND key_col < 10;
 
SELECT * FROM t1 
    WHERE key_col = 1 
    OR key_col IN (15,18,20);
 
SELECT * FROM t1 
    WHERE key_col LIKE 'ab%' 
    OR key_col BETWEEN 'bar' AND 'foo';
 

請注意在常量傳播階段部分非常量值可以轉換為常數。

MySQL嘗試為每個可能的索引從WHERE子句提取範圍條件。在提取過程中,不能用於構成範圍條件的條件被放棄,產生重疊範圍的條件組合到一起,並且產生空範圍的條件被刪除。

例如,考慮下面的語句,其中key1是有索引的列,nonkey沒有索引:

SELECT * FROM t1 WHERE
   (key1 < 'abc' AND (key1 LIKE 'abcde%' OR key1 LIKE '%b')) OR
   (key1 < 'bar' AND nonkey = 4) OR
   (key1 < 'uux' AND key1 > 'z');

key1的提取過程如下:

1.    用原始WHERE子句開始:

2.    (key1 < 'abc' AND (key1 LIKE 'abcde%' OR key1 LIKE '%b')) OR

3.     (key1 < 'bar' AND nonkey = 4) OR

4.     (key1 < 'uux' AND key1 > 'z')

5.    刪除nonkey = 4key1 LIKE '%b',因為它們不能用於範圍掃瞄。刪除它們的正確途徑是用TRUE替換它們,以便進行範圍掃瞄時不會丟失匹配的記錄。用TRUE替換它們後,可以得到:

6.            (key1 < 'abc' AND (key1 LIKE 'abcde%' OR TRUE)) OR
7.            (key1 < 'bar' AND TRUE) OR
8.            (key1 < 'uux' AND key1 > 'z')

9.    取消總是為truefalse的條件:

·         (key1 LIKE 'abcde%' OR TRUE)總是true

·         (key1 < 'uux' AND key1 > 'z')總是false

用常量替換這些條件,我們得到:

(key1 < 'abc' AND TRUE) OR (key1 < 'bar' AND TRUE) OR (FALSE)

刪除不必要的TRUEFALSE常量,我們得到

(key1 < 'abc') OR (key1 < 'bar')

10.將重疊區間組合成一個產生用於範圍掃瞄的最終條件:

11.        (key1 < 'bar')

總的來說(如前面的例子所述),用於範圍掃瞄的條件比WHERE子句限制少。MySQL再執行檢查以過濾掉滿足範圍條件但不完全滿足WHERE子句的行。

範圍條件提取算法可以處理嵌套的任意深度的AND/OR結構,並且其輸出不依賴條件在WHERE子句中出現的順序。

7.2.5.2. 多元素索引的範圍訪問方法

多元素索引的範圍條件是單元素索引的範圍條件的延伸。多元素索引的範圍條件將索引記錄限制到一個或幾個關鍵元組內。使用索引的順序,通過一系列關鍵元組來定義關鍵元組區間。

例如,考慮定義為key1(key_part1, key_part2, key_part3)的多元素索引,以及下面的按關鍵字順序所列的關鍵元組:

key_part1  key_part2  key_part3
  NULL       1          'abc'
  NULL       1          'xyz'
  NULL       2          'foo'
   1         1          'abc'
   1         1          'xyz'
   1         2          'abc'
   2         1          'aaa'
 

條件key_part1 = 1定義了下面的範圍:

(1-inf-inf) <= (key_part1key_part2key_part3) < (1+inf+inf)

範圍包括前面數據集中的第456個元組,可以用於範圍訪問方法。

通過對比,條件key_part3 = 'abc'不定義單一的區間,不能用於範圍訪問方法。

下面更加詳細地描述了範圍條件如何用於多元素索引中。

·         對於HASH索引,可以使用包含相同值的每個區間。這說明區間只能由下面形式的條件產生:

·                     key_part1 cmp const1
·                 AND key_part2 cmp const2
·                 AND ...
·                AND key_partN cmp constN;

這裡,const1const2...為常量,cmp=<=>或者IS NULL比較操作符之一,條件包括所有索引部分。(也就是說,有N 個條件,每一個對應N-元素索引的每個部分)

關於常量的定義,參見7.2.5.1節,「單元素索引的範圍訪問方法」

例如,下面為三元素HASH索引的範圍條件:

key_part1 = 1 AND key_part2 IS NULL AND key_part3 = 'foo'

·         對於BTREE索引,區間可以對結合AND的條件有用,其中每個條件用一個常量值通過=<=>IS NULL><>=<=!=<>BETWEEN或者LIKE 'pattern' (其中'pattern'不以通配符開頭)比較一個關鍵元素。區間可以足夠長以確定一個包含所有匹配條件(或如果使用<>!=,為兩個區間)的記錄的單一的關鍵元組。例如,對於條件:

·                  key_part1 = 'foo' AND key_part2 >= 10 AND key_part3 > 10

單一區間為:

('foo'1010)
   < (key_part1key_part2key_part3)
      < ('foo'+inf+inf)

建立的區間可以比原條件包含更多的記錄。例如,前面的區間包括值('foo'110),不滿足原條件。

·         如果包含區間內的一系列記錄的條件結合使用OR,則形成包括一系列包含在區間並集的記錄的一個條件。如果條件結合使用了AND,則形成包括一系列包含在區間交集內的記錄的一個條件。例如,對於兩部分索引的條件:

·                (key_part1 = 1 AND key_part2 < 2)
·                OR (key_part1 > 5)

區間為:

(1, -inf) < (key_part1, key_part2) < (1, 2)

(5, -inf) < (key_part1, key_part2)

在該例子中,第1行的區間左側的約束使用了一個關鍵元素,右側約束使用了兩個關鍵元素。第2行的區間只使用了一個關鍵元素。EXPLAIN輸出的key_len列資料表示所使用關鍵字前綴的最大長度。

在某些情況中,key_len可以資料表示使用的關鍵元素,但可能不是您所期望的。假定key_part1key_part2可以為NULL。則key_len列顯示下面條件的兩個關鍵元素的長度:

key_part1 >= 1 AND key_part2 < 2

但實際上,該條件可以變換為:

key_part1 >= 1 AND key_part2 IS NOT NULL

7.2.5.1節,「單元素索引的範圍訪問方法」描述了如何進行最佳化以結合或刪除單元素索引範圍條件的區間。多元素索引範圍條件的區間的步驟類似。

7.2.6. 索引合併最佳化

索引合併方法用於通過range掃瞄搜索行並將結果合成一個。合併會產生並集、交集或者正在進行的掃瞄的交集的並集。

EXPLAIN輸出中,該方法資料表現為type列內的index_merge。在這種情況下,key列包含一列使用的索引,key_len包含這些索引的最長的關鍵元素。

例如:

SELECT * FROM tbl_name WHERE key_part1 = 10 OR key_part2 = 20;

 

SELECT * FROM tbl_name

    WHERE (key_part1 = 10 OR key_part2 = 20) AND non_key_part=30;

 

SELECT * FROM t1, t2

    WHERE (t1.key1 IN (1,2) OR t1.key2 LIKE 'value%')

    AND t2.key1=t1.some_col;

 

SELECT * FROM t1, t2

    WHERE t1.key1=1

    AND (t2.key1=t1.some_col OR t2.key2=t1.some_col2);

 

索引合併方法有幾種訪問算法 (參見EXPLAIN輸出的Extra字段)

·         交集

·         聯合

·         排序並集

後面幾節更加詳細地描述了這些方法。

註釋:索引合併最佳化算法具有以下幾個已知問題:

·         如果可以對某些關鍵字進行範圍掃瞄,則不考慮索引合併。例如,下面的查詢:

·                SELECT * FROM t1 WHERE (goodkey1 < 10 OR goodkey2 < 20) AND badkey < 30;

對於該查詢,可以有兩個方案:

1.    使用(goodkey1 < 10 OR goodkey2 < 20)條件進行索引合併掃瞄。

2.    使用badkey < 30條件進行範圍掃瞄。

然而,最佳化器只考慮第2個方案。如果這不是您想要的,您可以通過使用IGNORE INDEXFORCE INDEX讓最佳化器考慮index_merge。下面的查詢使用索引合併執行:

SELECT * FROM t1 FORCE INDEX(goodkey1,goodkey2)

WHERE (goodkey1 < 10 OR goodkey2 < 20) AND badkey < 30;

 

SELECT * FROM t1 IGNORE INDEX(badkey)

WHERE (goodkey1 < 10 OR goodkey2 < 20) AND badkey < 30;

·         如果查詢有一個複雜的WHERE子句,有較深的AND/OR嵌套關係,MySQL不選擇該優選方案,通過下面的識別法則嘗試分佈各條件:

·                (x AND y) OR z = (x OR z) AND (y OR z)
·                (x OR y) AND z = (x AND z) OR (y AND z)

index_merge訪問方法的不同變數之間的選擇和其它訪問方法基於各適用選項的成本估計。

7.2.6.1. 索引合併交集訪問算法

該訪問算法可以用於當WHERE子句結合AND被轉換為不同的關鍵字的幾個範圍條件,每個條件為下面之一:

·         以這種形式,即索引有確切的N部分(即包括了所有索引部分)

·                key_part1=const1 AND key_part2=const2 ... AND key_partN=constN

·         任何InnoDBBDB資料表的主鍵的範圍條件。

下面是一些例子:

SELECT * FROM innodb_table WHERE primary_key < 10 AND key_col1=20;

 

SELECT * FROM tbl_name

WHERE (key1_part1=1 AND key1_part2=2) AND key2=2;

索引合併交集算法同時對所有使用的索引進行掃瞄,並產生從合併的索引掃瞄接收的行序列的交集。

如果使用的索引包括查詢中使用的所有列,所有資料表記錄均不搜索,並且在這種情況下EXPLAIN的輸出包含Extra字段中的Using index。下面是一個此類查詢的例子:

SELECT COUNT(*) FROM t1 WHERE key1=1 AND key2=1;

如果使用的索引未包括查詢中使用的所有列,只有滿足所有使用的關鍵字的範圍條件才搜索所有記錄。

如果某個合併條件是InnoDBBDB資料表的主鍵的一個條件,不用於記錄查詢,但用於過濾使用其它條件搜索的記錄。

7.2.6.2. 索引合併並集訪問算法

該算法的適用標準類似於索引合併方法交集算法的標準。算法可以用於當WHERE子句結合OR被轉換為不同的關鍵字的幾個範圍條件的時候,每個條件為下面之一:

·         以這種形式,即索引有確切的N部分(即包括了所有索引部分)

·                key_part1=const1 AND key_part2=const2 ... AND key_partN=constN

·         任何InnoDBBDB資料表的主鍵的範圍條件。

·         索引合併方法交集算法適用的一個條件。

下面是一些例子:

SELECT * FROM t1 WHERE key1=1 OR key2=2 OR key3=3;
 
SELECT * FROM innodb_table WHERE (key1=1 AND key2=2) OR
  (key3='foo' AND key4='bar') AND key5=5;

7.2.6.3. 索引合併排序並集訪問算法

該訪問算法可以用於當WHERE子句結合OR被轉換為不同的關鍵字的幾個範圍條件,但索引合併方法聯合算法並不適用的時候。

下面是一些例子:

SELECT * FROM tbl_name WHERE key_col1 < 10 OR key_col2 < 20;
 
SELECT * FROM tbl_name
     WHERE (key_col1 > 10 OR key_col2 = 20) AND nonkey_col=30;

排序聯合算法和聯合算法的區別是排序聯合算法必須先索取所有記錄的行ID,然後在返回記錄前對它們進行排序。

7.2.7. MySQL如何最佳化IS NULL

MySQL可以對可以結合col_name = constant_value使用的col_name IS NULL進行相同的最佳化。例如,MySQL可以使用索引和範圍用IS NULL搜索NULL

SELECT * FROM tbl_name WHERE key_col IS NULL;
 
SELECT * FROM tbl_name WHERE key_col <=> NULL;
 
SELECT * FROM tbl_name
    WHERE key_col=const1 OR key_col=const2 OR key_col IS NULL;

如果WHERE子句包括聲明為NOT NULL的列的col_name IS NULL條件,資料表達式則最佳化。當列會產生NULL時,不會進行最佳化;例如,如果來自LEFT JOIN右側的資料表。

MySQL也可以最佳化組合col_name = expr AND col_name IS NULL,這是解決子查詢的一種常用形式。當使用最佳化時EXPLAIN顯示ref_or_null

該最佳化可以為任何關鍵元素處理IS NULL

下面是一些最佳化的查詢例子,假定資料表t2的列ab有一個索引:

SELECT * FROM t1 WHERE t1.a=expr OR t1.a IS NULL;
 
SELECT * FROM t1, t2 WHERE t1.a=t2.a OR t2.a IS NULL;
 
SELECT * FROM t1, t2
    WHERE (t1.a=t2.a OR t2.a IS NULL) AND t2.b=t1.b;
 
SELECT * FROM t1, t2
    WHERE t1.a=t2.a AND (t2.b=t1.b OR t2.b IS NULL);
 
SELECT * FROM t1, t2
    WHERE (t1.a=t2.a AND t2.a IS NULL AND ...)
    OR (t1.a=t2.a AND t2.a IS NULL AND ...);

ref_or_null首先讀取參考關鍵字,然後單獨搜索NULL關鍵字的行。

請注意該最佳化只可以處理一個IS NULL。在後面的查詢中,MySQL只對資料表達式(t1.a=t2.a AND t2.a IS NULL)使用關鍵字查詢,不能使用b的關鍵元素:

SELECT * FROM t1, t2
     WHERE (t1.a=t2.a AND t2.a IS NULL)
     OR (t1.b=t2.b AND t2.b IS NULL);

7.2.8. MySQL如何最佳化DISTINCT

在許多情況下結合ORDER BYDISTINCT需要一個臨時資料表。

請注意因為DISTINCT可能使用GROUP BY,必須清楚MySQL如何使用所選定列的一部分的ORDER BYHAVING子句中的列。參見12.10.3節,「具有隱含字段的GROUP BY」

在大多數情況下,DISTINCT子句可以視為GROUP BY的特殊情況。例如,下面的兩個查詢是等效的:

SELECT DISTINCT c1, c2, c3 FROM t1 WHERE c1 > const;
 
SELECT c1, c2, c3 FROM t1 WHERE c1 > const GROUP BY c1, c2, c3;

由於這個等效性,適用於GROUP BY查詢的最佳化也適用於有DISTINCT子句的查詢。這樣,關於DISTINCT查詢的最佳化的更詳細的情況,參見7.2.13節,「MySQL如何最佳化GROUP BY

結合LIMIT row_countDISTINCT後,MySQL發現唯一的row_count行後立即停止。

如果不使用查詢中命名的所有資料表的列,MySQL發現第1個匹配後立即停止掃瞄未使用的資料表。在下面的情況中,假定t1t2之前使用(可以用EXPLAIN檢查),發現t2中的第1行後,MySQL不再(t1中的任何行)t2

SELECT DISTINCT t1.a FROM t1, t2 where t1.a=t2.a;

7.2.9. MySQL如何最佳化LEFT JOIN和RIGHT JOIN

MySQL中,A LEFT JOIN B join_condition執行過程如下:

·         根據資料表AA依賴的所有資料表設置資料表B

·         根據LEFT JOIN條件中使用的所有資料表(除了B)設置資料表A

·         LEFT JOIN條件用於確定如何從資料表B搜索行。(換句話說,不使用WHERE子句中的任何條件)

·         可以對所有標準聯接進行最佳化,只是只有從它所依賴的所有資料表讀取的資料表例外。如果出現循環依賴關係,MySQL提示出現一個錯誤。

·         進行所有標準WHERE最佳化。

·         如果A中有一行匹配WHERE子句,但B中沒有一行匹配ON條件,則生成另一個B行,其中所有列設置為NULL

·         如果使用LEFT JOIN找出在某些資料表中不存在的行,並且進行了下面的測試:WHERE部分的col_name IS NULL,其中col_name是一個聲明為 NOT NULL的列,MySQL找到匹配LEFT JOIN條件的一個行後停止(為具體的關鍵字組合)搜索其它行。

RIGHT JOIN的執行類似LEFT JOIN,只是資料表的角色反過來。

聯接最佳化器計算資料表應聯接的順序。LEFT JOINSTRAIGHT_JOIN強制的資料表讀順序可以幫助聯接最佳化器更快地工作,因為檢查的資料表交換更少。請注意這說明如果執行下面類型的查詢,MySQL進行全掃瞄b,因為LEFT JOIN強制它在d之前讀取:

SELECT *
    FROM a,b LEFT JOIN c ON (c.key=a.key) LEFT JOIN d ON (d.key=a.key)
    WHERE b.key=d.key;

在這種情況下修復時用a的相反順序,b列於FROM子句中:

SELECT *
    FROM b,a LEFT JOIN c ON (c.key=a.key) LEFT JOIN d ON (d.key=a.key)
    WHERE b.key=d.key;

MySQL可以進行下面的LEFT JOIN最佳化:如果對於產生的NULL行,WHERE條件總為假,LEFT JOIN變為普通聯接。

例如,在下面的查詢中如果t2.column1NULLWHERE 子句將為false

SELECT * FROM t1 LEFT JOIN t2 ON (column1) WHERE t2.column2=5;

因此,可以安全地將查詢轉換為普通聯接:

SELECT * FROM t1, t2 WHERE t2.column2=5 AND t1.column1=t2.column1;

這樣可以更快,因為如果可以使查詢更佳,MySQL可以在資料表t1之前使用資料表t2。為了強制使用資料表順序,使用STRAIGHT_JOIN

7.2.10. MySQL如何最佳化嵌套Join

資料表示聯接的語法允許嵌套聯接。下面的討論引用了13.2.7.1節,「JOIN語法」中描述的聯接語法。

SQL標準比較,table_factor語法已經延伸了。後者只接受table_reference,而不是括號內所列的。

table_reference項列資料表內的每個逗號等價於內部聯接,這是一個保留延伸名。例如:

SELECT * FROM t1 LEFT JOIN (t2, t3, t4)
                 ON (t2.a=t1.a AND t3.b=t1.b AND t4.c=t1.c)

等價於:

SELECT * FROM t1 LEFT JOIN (t2 CROSS JOIN t3 CROSS JOIN t4)
                 ON (t2.a=t1.a AND t3.b=t1.b AND t4.c=t1.c)

MySQL中,CROSS JOIN語法上等價於INNER JOIN (它們可以彼此代替。在標準SQL中,它們不等價。INNER JOIN結合ON子句使用;CROSS JOIN 用於其它地方。

總的來說,在只包含內部聯接操作的聯接資料表達式中可以忽略括號。刪除括號並將操作組合到左側後,聯接資料表達式:

t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL)
   ON t1.a=t2.a

轉換為資料表達式:

(t1 LEFT JOIN t2 ON t1.a=t2.a) LEFT JOIN t3
    ON t2.b=t3.b OR t2.b IS NULL

但是這兩個資料表達式不等效。要說明這點,假定資料表t1t2t3有下面的狀態:

·         資料表t1包含行{1}{2}

·         資料表t2包含行{1,101}

·         資料表t3包含行{101}

在這種情況下,第1個資料表達式返回包括行{1,1,101,101}{2,NULL,NULL,NULL}的結果,第2個資料表達式返回行{1,1,101,101}{2,NULL,NULL,101}

mysql> SELECT *
    -> FROM t1
    ->      LEFT JOIN
    ->      (t2 LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL)
    ->      ON t1.a=t2.a;
+------+------+------+------+
| a    | a    | b    | b    |
+------+------+------+------+
|    1 |    1 |  101 |  101 |
|    2 | NULL | NULL | NULL |
+------+------+------+------+
 
mysql> SELECT *
    -> FROM (t1 LEFT JOIN t2 ON t1.a=t2.a)
    ->      LEFT JOIN t3
    ->      ON t2.b=t3.b OR t2.b IS NULL;
+------+------+------+------+
| a    | a    | b    | b    |
+------+------+------+------+
|    1 |    1 |  101 |  101 |
|    2 | NULL | NULL |  101 |
+------+------+------+------+

在下面的例子中,外面的聯接操作結合內部聯接操作使用:

t1 LEFT JOIN (t2t3) ON t1.a=t2.a

該資料表達式不能轉換為下面的資料表達式:

t1 LEFT JOIN t2 ON t1.a=t2.at3.

對於給定的資料表狀態,第1個資料表達式返回行{1,1,101,101}{2,NULL,NULL,NULL},第2個資料表達式返回行{1,1,101,101}{2,NULL,NULL,101}

mysql> SELECT *
    -> FROM t1 LEFT JOIN (t2, t3) ON t1.a=t2.a;
+------+------+------+------+
| a    | a    | b    | b    |
+------+------+------+------+
|    1 |    1 |  101 |  101 |
|    2 | NULL | NULL | NULL |
+------+------+------+------+
 
mysql> SELECT *
    -> FROM t1 LEFT JOIN t2 ON t1.a=t2.a, t3;
+------+------+------+------+
| a    | a    | b    | b    |
+------+------+------+------+
|    1 |    1 |  101 |  101 |
|    2 | NULL | NULL |  101 |
+------+------+------+------+

因此,如果我們忽略聯接資料表達式中的括號連同外面的聯接操作符,我們會改變原資料表達式的結果。

更確切地說,我們不能忽視左外聯接操作的右操作數和右聯接操作的左操作數中的括號。換句話說,我們不能忽視外聯接操作中的內資料表達式中的括號。可以忽視其它操作數中的括號(外部資料表的操作數)

對於任何資料表t1t2t3和屬性t2.bt3.b的任何條件P,下