優化和索引
提升SELECT 的最好方式是使用索引。索引條目作為表數據行的指針,使得查詢能夠很快的定位到所要查找的數據。所有的MySQL數據類型都可以創建索引。
不必要的索引會浪費存儲空間,同時也會增加數據更新成本(數據更新時,索引也相應的需要被更新)。
MySQL 使用索引
索引用於快速定位特定值的表數據行。如果不使用索引,MySQL則需要從第一個數據行開始查找整個數據表,直到找到要查找的數據行,表越大,查找成本越高。如果查找條件的列存在索引,那么MySQL就可以快速定位需要查找的數據位置。
大部分MySQL 索引(PRIMARY KEY, UNIQUE, INDEX, and FULLTEXT) 都是以 B-trees形式存儲。例外情況:基於R-trees的空間數據索引;MEMORY引擎的HASH索引;InnoDB 的基於倒序列表的全文索引。
MySQL 使用索引的操作情景:
- Where條件查找。
- 快速排序干擾數據。使用最具選擇行的索引。
- 最左前綴索引查找,如:在(col1, col2, col3)存在索引,則可以使用包括 (col1), (col1, col2)及 (col1, col2, col3)索引來進行查詢。
- 聯合查詢,從聯合表查詢數據。相同類型和大小的索引列使用更加高效。例如, VARCHAR and CHAR 列設定大小相同時,會被認為相同類型,如 VARCHAR(10) 和 CHAR(10)。
比較的列必須具有相同的字符類型。
SELECT MIN(key_part2),MAX(key_part2)
FROM tbl_name WHERE key_part1=10;
SELECT key_part3 FROM tbl_name
WHERE key_part1=1
索引對於小表查詢,或者需要訪問大部分數據的大表(此時全表掃描比較高效)作用很小。
主鍵優化
主鍵即表查詢應用列。主鍵上有相應的索引,用於快速查詢。主鍵要求不能為null。InnoDB 引擎物理上以一種有助於快速查詢的方式存儲。
如果表比較大,且很重要,但是沒有特別適合做主鍵的列,則,應該創建一個額外的列,以auto-increment方式增長,作為主鍵。可以作為聯合查詢的外鍵。
外鍵優化
如果表有很多的列,查詢也有很多的組合,那么有必要將使用率較低的列划分到關聯的不同表中,並使用主表主鍵進行關聯。這樣,每個小表都有個主鍵來提供快速查詢使用,對於綜合查詢,可以使用相關的表進行聯合查詢。數據存儲分布的不同及具體數據的組織形式不同,會對查詢緩存需求及I/O訪問產生較大影響。為了盡可能的提高性能,應該盡量減少磁盤I/O,一些具有較少列的表可以盡量一次將較多的行數據查詢到內存。
列索引
最常見的索引類型通常涉及單個數據列,索引以一定的數據結構存儲一列的數據,這樣就可以快速定位這一列的某一特定值。B-tree 數據結構提供了對特定值,值列表,范圍值包括=, >, ≤, BETWEEN, IN等在內的條件查詢的快速定位。
不同存儲引擎對於但表最大索引數及索引長度都有規定。所有的引擎都支持至少但表16個索引及索引總長度至少256 bytes的約定。一些引擎的限制限度寬泛。
前綴索引(Index Prefixes)
索引定義中,對於string類型列使用 col_name(N) 語句,可以創建只使用列前N 個字符作為索引數據。只使用列前部分數據作為索引能夠減少索引空間占用。例如,對於BLOB 或者 TEXT 類型列,必須使用這種方式:
CREATE TABLE test (blob_col BLOB, INDEX(blob_col(10)));
前綴最多支持使用長度為1000 bytes (InnoDB 最多支持767 bytes 長度,除非另外設置innodb_large_prefix 變量).
Note
前綴限制以bytes計量。但是,對於CREATE TABLE, ALTER TABLE, 和 CREATE INDEX 語句中,對於非二進制類型(CHAR, VARCHAR, TEXT) 是以字符數為計量限制的。對於二進制類型字符類型 (BINARY, VARBINARY, BLOB),則是以bytes為計量限制的。
全文索引(FULLTEXT Indexes)
FULLTEXT 全文索引用以支持全文搜索。只有InnoDB 及 MyISAM 存儲引擎對於 CHAR, VARCHAR和TEXT類型列支持全文索引。索引只能創建在列全部的值上,而不能使用列部分值。
對於Innodb單表上的特定類型的全文索引,MySQL會有些優化以優化查詢:
- FULLTEXT 查詢只返回文檔ID,或者文檔ID和查詢評級。
- FULLTEXT 查詢DESC 排序,並使用LIMIT 限制條件。對於這種類型優化,不能使用 WHERE 條件,並且只能有一個ORDER BY 條件。
- FULLTEXT 查詢 COUNT(*)結果,沒有 WHERE 條件。或者使用 WHERE MATCH(text) AGAINST ('other_text')類型條件,不能有任何 > 0 的對比條件。
對於這種類型的基於全文查詢,MySQL在執行查詢優化的過程中進行判別。
全文搜索比非全文搜索要慢,因為多出了這樣一個判斷階段。
用執行計划觀察執行全文搜索的查詢,當匹配數據出現在優化極端時,Extra列會有Select tables optimized away 的信息提示。
空間索引(Spatial Indexes)
創建在空間類型數據上的索引。MyISAM 和 InnoDB 支持空間類型數據上創建R-tree 結構類型索引。其它引擎索引結構為B-trees。(ARCHIVE引擎不支持空間索引)。
MEMORY 存儲引擎上的索引
MEMORY 存儲引擎好似用HASH 結構索引,同時也支持BTREE 結構索引。
多列索引
MySQL可以創建組合索引(創建於多列上的索引),一個索引最多包含16列。
MySQL可以使用多列索引進行查詢,基於索引多列匹配,或者只匹配索引包含的第一列,前兩列… 前n列。合理的排序,組合索引列,使之滿足大多數的查詢需求。
多列索引可以看作為排序數組,數組的每一行包含相關索引列的值組合。
Note
區別於多列索引,可以使用一種基於其它列hash值的列,如果這個hash列,足夠短,具備合理的選擇性。使用此列作為索引要比使用其所基於的多列更高效。使用如下:(限制比較大)
SELECT * FROM tbl_name WHERE hash_col=MD5(CONCAT(val1,val2)) AND col1=val1 AND col2=val2;
假定如下的表定義:
CREATE TABLE test (id INT NOT NULL, last_name CHAR(30) NOT NULL, first_name CHAR(30) NOT NULL, PRIMARY KEY (id), INDEX name (last_name,first_name));
索引 name 建立在 last_name 和 first_name 列上。使用此索引可以查詢基於此兩列的條件查詢,或者是基於last_name 列的查詢(索引前綴)。如下查詢:
SELECT * FROM test WHERE last_name='Widenius';
SELECT * FROM test WHERE last_name='Widenius' AND first_name='Michael';
SELECT * FROM test WHERE last_name='Widenius' AND (first_name='Michael' OR first_name='Monty');
SELECT * FROM test WHERE last_name='Widenius' AND first_name >='M' AND first_name < 'N';
如下查詢,則無法使用索引:
SELECT * FROM test WHERE first_name='Michael';
SELECT * FROM test WHERE last_name='Widenius' OR first_name='Michael';
假定有如下查詢:
SELECT * FROM tbl_name WHERE col1=val1 AND col2=val2;
如果有基於col1 和col2的多列索引,那么查詢就可以直接使用索引。如果只有分別基於col1 和 col2的單列索引,優化器會嘗試使用索引合並優化,或者嘗試使用更具篩選性(能夠排除更多的無關數據行的)的索引。
多列索引,可以使用任何的前綴索引來進行查詢。如基於(col1, col2, col3)的索引,可使用的索引形式如下: (col1), (col1, col2), 和 (col1, col2, col3)。
如下:第1,2個查詢可以使用此索引,第3,4個不支持使用此索引。
SELECT * FROM tbl_name WHERE col1=val1;
SELECT * FROM tbl_name WHERE col1=val1 AND col2=val2;
SELECT * FROM tbl_name WHERE col2=val2;
SELECT * FROM tbl_name WHERE col2=val2 AND col3=val3;
InnoDB 和 MyISAM 索引統計數據收集
存儲引擎會收集表的統計信息以供優化器使用。表統計數據是基於同一索引前綴值的行數據集合集。對於優化器來說,重要的統計數據為平均值集合的大小。
應用如下:
- 預估每個ref 訪問需要讀取多少行數據。
- 預估每個聯合查詢會產生多少條記錄。也就是說,如下的操作會產生多少行數據:
(...) JOIN tbl_name ON tbl_name.key = expr
如果一個索引導致平均值集合的大小增加(索引的一個值對應數據表中的記錄數),那么此索引可用性降低。為了優化的需求,最好每個索引值只對應小范圍數據行。
平均值集合大小和表的基數相關,即值集合的總大小。SHOW INDEX FROM語句展示了基於N/S 的基數值, N 代表表行數,S 代表平均值集合大小。比值代表表中值集合的數量。
對於聯合查詢中的<=> 比較符,NULL 和其它值N(其它任何類型)無異。NULL <=> NULL 同處理 N <=> N 。
然而,聯合查詢中的 = 操作符,對於NULL的處理則不同,對於條件 expr1 = expr2 , expr1 或者 expr2 或者都為NULL時,條件都不成立。這一情況影響ref (非唯一索引查找)類型訪問中類似tbl_name.key = expr形式的條件查詢,MySQL在條件值為expr 為 NULL時,將不會再訪問表數據,因為條件永遠不成立。
對於 = 比較符,無論表中有多少個NULL 值,為了優化,相關索引值集合大小為非NULL 值集合。然而,MySQL將不再收集和使用平均只集合大小。
對於InnoDB 和MyISAM 類型表,可以通過變量innodb_stats_method 和 myisam_stats_method 控制表統計信息的收集。變量值集合如下:
- nulls_equal:所有的 NULL 值作等值對待(作為一個值集合)。
如果NULL 值的集合大小遠遠大於非NULL值集合大小時,這種配置將會增大平均值集合大小。使得在進行非NULL條件聯合查詢時,索引對於優化器看起來不如它實際有用。從而導致對於ref 訪問,優化器將不再使用原本應該使用的索引。
- nulls_unequal,:每個NULL 值都做不等值對待,形成N個不同的NULL值集合(大小為1)
如果表中有過多的NULL 值,將會降低整體的平均值集合大小。此時,如果非NULL值集合大小非常大,那么就會造成優化器高估索引在非NULL條件查詢的可用性。從而導致優化器在ref訪問時,使用到不合適索引。
- nulls_ignored: NULL 值忽略。values are ignored.
If you tend to use many joins that use <=> rather than =, NULL values are not special in comparisons and one NULL is equal to another. In this case, nulls_equal is the appropriate statistics method.
innodb_stats_method 系統變量是全局性的; myisam_stats_method 則具有全局和會話級兩個值。全局值影響相應的存儲引擎對表統計數據的收集。會話級的值影響當前客戶端連接的統計數據收集。也就是說,會話級設置可以在不影響其它客戶端的情況下重新生成表的統計數據。
重新生成MyISAM 表統計數據,可以使用如下方法:
- 執行 myisamchk --stats_method=method_name --analyze
- 改變表(如插入數據,更新數據等)從而引發表統計數據過期,然后設置 myisam_stats_method 再觸發ANALYZE TABLE 語句。
innodb_stats_method 和 myisam_stats_method的一些使用說明:
- 可以指定觸發,但是MySQL 的自動收集仍然在進行。
- 統計數據產生的原因無從得知。
- 只有InnoDB 和MyISAM 類型表有這些變量類型。其它引擎只一種類似 nulls_equal。
B-Tree及Hash 索引對比
B-Tree 索引特點
B-tree索引應用:=, >, >=, <, <=, 及 BETWEEN,對比值為常量且不以通配符起始的LIKE 條件,如下:
SELECT * FROM tbl_name WHERE key_col LIKE 'Patrick%';
SELECT * FROM tbl_name WHERE key_col LIKE 'Pat%_ck%';
In the first statement, only rows with 'Patrick' <= key_col < 'Patricl' are considered. In the second statement, only rows with 'Pat' <= key_col < 'Pau' are considered.
一下查詢不使用索引:
SELECT * FROM tbl_name WHERE key_col LIKE '%Patrick%';//條件值包含通配符
SELECT * FROM tbl_name WHERE key_col LIKE other_col; //非常量值
如果 ... LIKE '%string%' 條件 string 不超過3個字符串。MySQL 會使用Turbo Boyer-Moore 字符串查詢算法來進行查詢。
條件 col_name IS NULL 當col_name 上有索引時會使用索引。
對於含有多個 AND 組合的條件,只有當每個 AND 組都使用了索引或者索引前綴,查詢才會使用索引。如下,使用索引場景:
... WHERE index_part1=1 AND index_part2=2 AND other_column=3
/* index = 1 OR index = 2 */
... WHERE index=1 OR A=10 AND index=2
/* optimized like "index_part1='hello'" */
... WHERE index_part1='hello' AND index_part3=5
/* Can use index on index1 but not on index2 or index3 */
... WHERE index1=1 AND index2=2 OR index1=3 AND index3=3;
如下將不使用索引:
/* index_part1 is not used */
... WHERE index_part2=1 AND index_part3=2
/* Index is not used in both parts of the WHERE clause */
... WHERE index=1 OR A=10
/* No index spans all rows */
... WHERE index_part1=1 OR index_part2=10
還有一種情況是有可用的索引,但是索引導致的數據掃描效率低於全表掃描查詢,則不會使用索引。但是如果有使用LIMIT 限制,則總會使用索引。
Hash 索引特點
- 只能等值或者不等值匹配(= 或 <=> )但很快。使用此類查詢的應用一般稱之為key-value 存儲。
- 無法使用HASH索引優化ORDER BY 操作。(這種類型的索引無法用於查詢排序)
- MySQL無法通過此索引估計范圍條件間的數據行(優化器對於范圍查詢的優化(選擇索引))。
- 只能使用全部的健來匹配查詢(區別於B-tree 類型索引的最左前綴原則)
索引擴展
InnoDB 引擎會自動添加主鍵到二級索引來實現索引擴展,如下情景:
CREATE TABLE t1 (
i1 INT NOT NULL DEFAULT 0,
i2 INT NOT NULL DEFAULT 0,
d DATE DEFAULT NULL,
PRIMARY KEY (i1, i2),
INDEX k_d (d)
) ENGINE = InnoDB;
表定義了基於列 (i1, i2)主索引,及基於列 (d)的二級索引,但是在內部,InnoDB 會自動將二級索引擴展為基於 (d, i1, i2)的二級索引。
優化器會自動考慮二級索引擴展包含的主鍵列。
優化器可以針對ref, range, 和index_merge 類型索引訪問,松散索引掃描,聯合查詢和排序優化及MIN()/MAX() 優化使用擴展二級索引。
如下展示了優化器使用擴展二級索引與否對執行計划的影響。假定 t1 數據填充如下:
INSERT INTO t1 VALUES
(1, 1, '1998-01-01'), (1, 2, '1999-01-01'),
(1, 3, '2000-01-01'), (1, 4, '2001-01-01'),
(1, 5, '2002-01-01'), (2, 1, '1998-01-01'),
(2, 2, '1999-01-01'), (2, 3, '2000-01-01'),
(2, 4, '2001-01-01'), (2, 5, '2002-01-01'),
(3, 1, '1998-01-01'), (3, 2, '1999-01-01'),
(3, 3, '2000-01-01'), (3, 4, '2001-01-01'),
(3, 5, '2002-01-01'), (4, 1, '1998-01-01'),
(4, 2, '1999-01-01'), (4, 3, '2000-01-01'),
(4, 4, '2001-01-01'), (4, 5, '2002-01-01'),
(5, 1, '1998-01-01'), (5, 2, '1999-01-01'),
(5, 3, '2000-01-01'), (5, 4, '2001-01-01'),
(5, 5, '2002-01-01');
有如下查詢:
EXPLAIN SELECT COUNT(*) FROM t1 WHERE i1 = 3 AND d = '2000-01-01'
因為條件設計i1,d,主鍵索引無法包含,所以無法使用主鍵索引。相反,二級索引擴展后則正好匹配條件列。所以會使用擴展二級索引。是否使用擴展索引會影響執行計划的輸出。
如果優化器不考慮索引擴展,那么二級索引則只包含列d,那么執行計划輸出如下:
mysql> EXPLAIN SELECT COUNT(*) FROM t1 WHERE i1 = 3 AND d = '2000-01-01'\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
type: ref
possible_keys: PRIMARY,k_d
key: k_d
key_len: 4
ref: const
rows: 5
Extra: Using where; Using index
優化器考慮索引擴展,則認定二級索引為 (d, i1, i2),則根據最左前綴原則,使用二級索引,執行計划如下:
mysql> EXPLAIN SELECT COUNT(*) FROM t1 WHERE i1 = 3 AND d = '2000-01-01'\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
type: ref
possible_keys: PRIMARY,k_d
key: k_d
key_len: 8
ref: const,const
rows: 1
Extra: Using index
兩種情況下,執行計划都指出將會使用二級索引,但是在考慮索引擴展時,會有如下的優化:
- key_len 從4 變為8,表示查詢使用了列d 和i1, 而不只是d(d和i1都是int類型,長度4)。
- ref 值從const 變為 const,const ,因為使用了索引的兩部分。
- rows 掃描數從5 變為1,意味着InnoDB 引擎只需掃描較少的行就能匹配到最終結果。
- Extra 值從Using where; Using index 變為 Using index,這意味着結果可以只通過索引查詢來獲取,而不需讀取額外的數據列。
擴展索引對 SHOW STATUS的影響:
FLUSH TABLE t1;
FLUSH STATUS;
SELECT COUNT(*) FROM t1 WHERE i1 = 3 AND d = '2000-01-01';
SHOW STATUS LIKE 'handler_read%'
前置語句FLUSH TABLES 及 FLUSH STATUS 會將刷新表緩存及清除計數器狀態。
不使用索引擴展情況下,SHOW STATUS 輸入如下:=
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 0 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 5 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
使用索引擴展情況下,SHOW STATUS 輸入如下結果:Handler_read_next 從 5 減少到1,表明索引的高效使用。
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 0 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 1 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
The use_index_extensions flag of the通過設置系統變量 optimizer_switch 值中的use_index_extensions 標志來InnoDB 表擴展索引優化。默認情況下 use_index_extensions 啟用。 可以通過如下設置:
SET optimizer_switch = 'use_index_extensions=off';
擴展索引的使用也需要服從索引列限制及索引長度限制。
索引優化使用
MySQL支持生成列上的索引,如下:
CREATE TABLE t1 (f1 INT, gc INT AS (f1 + 1) STORED, INDEX (gc));
生成列gc 定義為 f1 + 1,其上定義了索引,優化器在執行查詢時會考慮使用其列上的索引。如下。條件中使用生成列gc,優化器會評估其上索引的效率:
SELECT * FROM t1 WHERE gc > 9;
優化器可以使用生成列上的索引生成執行計划。即使是在沒有直接的使用生成列名稱的情況下,這種情況發生在WHERE, ORDER BY, 或者 GROUP BY 條件中涉及滿足生成列定義的表達式的情景。如下查詢,沒有直接使用gc列,但是使用的表達式f1+1 符合gc的定義,所以會使用gc上的索引:
SELECT * FROM t1 WHERE f1 + 1 > 9;
優化器會識別出f1 + 1 符合gc列的定義,且gc上存在索引,所以在執行計划構建時就會考慮此索引,如下執行計划輸出:
mysql> EXPLAIN SELECT * FROM t1 WHERE f1 + 1 > 9\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
partitions: NULL
type: range
possible_keys: gc
key: gc
key_len: 5
ref: NULL
rows: 1
filtered: 100.00
Extra: Using index condition
事實上,優化器已經將表達式f1 + 1 替換為了相應的生成列。這可以通過 SHOW WARNINGS語句在擴展執行計划信息中看出,如下:
mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
Level: Note
Code: 1003
Message: /* select#1 */ select `test`.`t1`.`f1` AS `f1`,`test`.`t1`.`gc`
AS `gc` from `test`.`t1` where (`test`.`t1`.`gc` > 9)
使用生成列索引的限制及條件:
- 匹配查詢表達式和生成列定義,兩者必須完全一致,且結果類型一致。例如,對於生成列定義f1 + 1 和查詢條件1 + f1是不一樣的;假如f1 + 1 結果類型為整型,生成列類型為string,那么這兩者也是不匹配的。
- 這種優化包含=, <, <=, >, >=, BETWEEN, 和IN()比較符。
對於BETWEEN 和IN()以外的操作符,每個操作符都可以用一個匹配的生成列代替。對於BETWEEN 和 IN()操作符,只有第一個參數可以被生成列替代,同時另外一個參數必須具有相同的結果類型。BETWEEN 和 IN() 暫時不支持JSON 類型值比較。
- 生成列的定義表達式必須包含至少一個函數操作,或者包含之前提到的操作符。表達式不能單純的使用另外一個列的引用。例如 gc INT AS (f1) STORED 只包含另外一個列的引用,那么優化器就不會考慮此列上的索引。
- 對於查詢條件中生成列和使用JSON函數產生的string類型的有引號字符串值對比,JSON_UNQUOTE() 可以用來去除JSON函數產生的引號。如下:
doc_name TEXT AS (JSON_EXTRACT(jdoc, '$.name')) STORED
需要變更為:
doc_name TEXT AS (JSON_UNQUOTE(JSON_EXTRACT(jdoc, '$.name'))) STORED
則,對於一下查詢,優化器可以進行生成列匹配:
... WHERE JSON_EXTRACT(jdoc, '$.name') = 'some_string' ...
... WHERE JSON_UNQUOTE(JSON_EXTRACT(jdoc, '$.name')) = 'some_string' ...
如果生成列定義不使用 JSON_UNQUOTE() ,那么只能匹配以上查詢的第一個。
- 當優化器沒有選擇我們希望的索引,那么我們也可以通過其它方式使強制調整優化器選擇。