博文首先說明索引的分類及創建,然后會涉及到索引的可用性選擇以及索引的優化。
索引是什么?先說創建索引的目的,創建索引是為提高對數據的查詢速度。在字典的目錄中,我們可以很快找到某個字的位置,索引的作用就是類似於目錄,是為了針對select操作而存在的。
【索引是創建在表上,是對數據庫表中一列或多列的值進行排序的一種結構。索引可以提高查詢速度。】
就像在字典上創建索引會增加字典的厚度一樣,數據庫的索引也是有缺點的,在文章的后面會說明。
索引有兩種存儲類型,B型樹索引和Hash索引。innoDB和MyISAM存儲引擎支持B型樹索引,memory存儲引擎兩者都支持。默認是B型樹索引。
【本片博文如果沒有特別說明,創建的都是B型樹索引(用的最多)】
創建索引以及索引的分類
- 普通索引
在創建索引時,不附加任何限制條件。這類索引可以創建在任何數據類型中,值是否唯一和非空有本身的完整性約束條件決定。
索引的創建可以在創建表時創建,也可以在建表之后創建。
#1.創建表時創建索引
CREATE TABLE tb1( id int, name varchar(20), INDEX id_index (id DESC) ) #index|key 作為索引的標識, id_index為索引名(可以不指定會有默認的),后面必須加上一個括號,括號里創建索引的字段,最后的DESC表示倒序,ASC表示正序,默認正序!
#2.創建表之后添加索引,有兩種方法如下:
第一種:使用create語句
CREATE 【UNIQUE|FULLTEXT|SPATIAL】INNEX 索引名 ON TABLE_NAME (屬性名 [(長度)]);
第二種:使用alter語句。
ALTER TABLE TABLE_NAME ADD 【UNIQUE|FULLTEXT|SPATIAL】 INNEX 索引名 (屬性名[(長度)]);
##需要注意的是在char類型的字段上創建索引時,可以指定在當前字段的前幾個字符來創建索引。
#如下:在上面的表的name字段的前5個字符創建索引。(這里的索引只是為了練習)
CREATE INDEX index_name ON tb1 (name(5) DESC);
mysql> SHOW CREATE TABLE tb1\G
*************************** 1. row ***************************
Table: tb1
Create Table: CREATE TABLE `tb1` (
`id` int(11) DEFAULT NULL,
`name` varchar(20) DEFAULT NULL,
KEY `index_name` (`name`(5)) #創建的以name字段的前5個字符為索引
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec
- 唯一性索引
使用UNIQUE參數可以設置索引為唯一性索引。限制該索引值必須是唯一的。主鍵是一種特殊的唯一性索引。
在上面的表中,id字段一般為唯一性索引,我們在id字段上創建唯一性索引。
ALTER TABLE tb1 ADD UNIQUE INDEX index_id ( id ASC ); #在已經創建的表上添加唯一性索引
- 全文索引
使用fulltext參數可以設置索引為全文索引。全文索引只能創建在CHAR, VARCHAR,TEXT類型的字段上。查詢數據量較大的字符串類型字段時,使用全文索引可以提高查詢速度。
#在表中添加一個text字段,然后在字段上創建全文索引 ALTER TABLE tb1 ADD info text; CREATE FULLTEXT INDEX index_info ON tb1 ( info )
- 單列索引
在表中一個字段上創建的索引。以上的創建的三個索引均為單列索引。
- 多列索引
多列索引時在表的多個字段上創建一個索引。該索引指向創建時對應的多個字段,可以通過這幾個字段進行查詢。但是,只有查詢條件中使用了這些字段的第一個字段時,索引才會被引用。
CREATE INDEX name_index ON employees ( first_name, last_name ); #在employees表中創建一個雙列索引
需要注意的是,在多列索引時,在查詢時,只有第一個字段被引用,那么這個索引才會被使用。
#有如下表,在表中插入數據 CREATE TABLE tb2 ( a INT, b INT ); INSERT INTO tb2 VALUES ( 1, 2 ), ( 4, 3 ), ( 2, 1 ), ( 5, 9 ), ( 3, 4 ),
( 2, 4 ),
( 3, 1 ); CREATE INDEX test_index ON tb2 ( a, b );
#然后在表中創建一個復合索引如圖。
特別需要注意的是:創建索引之后對應的字段時邏輯有序的,而不是物理有序。
索引創建之后,表中的這些數據邏輯順序如下:
+------+------+
| a | b | #字段a是按照邏輯大小的順序排列的,但是字段b卻不是,
+------+------+ #因此在使用索引時,必須使用第一個字段才可以在查詢中使用索引
| 1 | 2 |
| 2 | 1 |
| 2 | 4 |
| 3 | 1 |
| 3 | 4 |
| 4 | 3 |
| 5 | 9 |
+------+------+
- 空間索引
空間索引的存儲引擎必須為MyIsam。使用SPATIAL參數可以設置索引為空間索引。空間索引只能建立在空間類型上。MySQL中的空間數據類型包括GEOMETRY,POINT,LINESTRING和POLYGON等。(暫時沒用到,不詳細說明)
刪除索引
刪除索引可以使用如下語句:
drop index 索引名 on 表名; mysql> drop index test_index on tb2; Query OK, 0 rows affected (0.02 sec) Records: 0 Duplicates: 0 Warnings: 0
索引為何會提高數據查詢的效率?
(提高數據的查詢速度,最重要的是想辦法減少數據查詢時對磁盤的IO操作,而服務器的CPU運算基本都是盈余的)
【待續】
索引的可選擇性:
創建一個索引,我們需要去評估這個創建的是否合理?如果一個表的數據量很少,或者這個字段的值重復性比較多,那么創建這個索引就沒有意義。在一張數據量比較大的表中,並且這個字段的重復性值不高,這時候我們可以創建索引。
我們如何知道這個字段究竟有多少條不重復的數據?
MySQL給我們提供了一個參數:Cardinality,這個值表示的是記錄不重復數據量的行數。
mysql> show index from employees\G *************************** 1. row *************************** Table: employees Non_unique: 0 Key_name: PRIMARY Seq_in_index: 1 Column_name: emp_no Collation: A Cardinality: 299290 Sub_part: NULL Packed: NULL Null: Index_type: BTREE Comment: Index_comment: *************************** 2. row *************************** Table: employees Non_unique: 1 Key_name: name_index Seq_in_index: 1 Column_name: first_name Collation: A Cardinality: 1288 Sub_part: NULL Packed: NULL Null: Index_type: BTREE Comment: Index_comment: *************************** 3. row *************************** Table: employees Non_unique: 1 Key_name: name_index Seq_in_index: 2 Column_name: last_name Collation: A Cardinality: 279473 Sub_part: NULL Packed: NULL Null: Index_type: BTREE Comment: Index_comment: 3 rows in set (0.00 sec) #各個字段解釋如下:
Table: 表名。
Non_unique:如果索引不能包含重復項則為0,可以則為1.
Key_name:索引的名字。
Seq_in_index:當前字段在復合索引中是第幾個字段。(單列索引則為1)
Column_name:字段名字。
Collation:列如何在索引中排序,值A表示升序。未排序則為NULL。
Cardinality:利用抽樣法估計的當前字段中不重復的行數。
Sub_part:索引前綴,若是整個字段索引則值為NULL,若是僅字符類型的前幾個字符索引,則顯示字符的數量。
Packed: 指示關鍵字如何被壓縮。如果沒有被壓縮,則為NUL
Index_type:索引類型。(BTREE, FULLTEXT,HASH, RTREE)
commecnt: Information about the index not described in its own column, such as disabled if the index is disabled.
Index_comment:創建索引時的一些說明信息。
#證明索引可行性的時候,我們需要額外關注Cardinality這個數值,這個數值的更新可以人為的使用ANALYZE table(myisam存儲引擎需要使用 myisamchk -a)
在innodb存儲引擎中,Cardinality統計信息的更新發生在兩個操作中:INSERT,UPDATE。但是不是會在每次操作時,都會更新這個數值,innodb存儲引擎更新Cardinality值得策略為:
- 表中的1/16數據已經發生變化
- stat_modified_counter >2 000 000 000
第一種策略為自上次統計Cardinality信息后,表中1/16的數據已經發生變化,這時需要更新Cardinality信息。第二種:如果對表中某一行的數據頻繁的更新,那么表中的數據並沒有增加,
發生變化的還是這一行數據,那么第一種策略就無法生效。因此在innodb存儲引擎內部有一個計數器stat_modified_counter,用來表示發生變化的次數,當更新的值大於指定的值時,
就會更新Cardinality的數值。
innodb打開某些INFORMATION_SCHEMA表,或者使用show table status和show index,抑或在MySQL客戶端開啟自動補全功能的時候都會觸發索引統計信息的更新,如果服務器上有大量的數據,這可能就是個很嚴重的問題,尤其是當I/O比較慢的時候,客戶端或者監控程序觸發索引信息采樣更新時會導致大量的鎖,並給服務器帶來很多額外的壓力。因此MySQL內部使用了一個參數來關閉自動觸發的索引采樣。
mysql> show variables like "innodb_stats_on_metadata"; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | innodb_stats_on_metadata | OFF | +--------------------------+-------+ 1 row in set (0.00 sec) mysql>
那么在MySQL內部,是怎么樣通過采樣計算card'inality值的?默認innodb存儲引擎對8個葉子節點進行采樣處理。
mysql> show variables like "innodb_stats_sample_pages"; #默認的采樣的8個葉子節點。 +---------------------------+-------+ | Variable_name | Value | +---------------------------+-------+ | innodb_stats_sample_pages | 8 | +---------------------------+-------+ 1 row in set (0.01 sec)
#采樣過程如下:
- 取得B+樹索引中葉子節點的數量,記為A。
- 隨機取得B+樹索引中的8個葉子節點。統計每個頁不同的記錄個數,即為p1,p2,p3,....p8
- 根據采樣信息給出cardinality的預估值: cardinality=(p1+p2+...p8)*A/8
#隨機采樣獲得的8個頁是隨機的,因此每次采樣得到的cardinality值可能是不同的。
innodb_stats_sample_pages參數用來控制隨機采樣葉的多少,而innodb_stats_method用來判斷如何對待索引中出現的null值激勵。該值默認值為nulls_equal,表示將null值視為相等的記錄。
其有效值還有null_unequal,null_ignored,分別表示將null值記錄視為不同的記錄和忽略null值的記錄。【注意三個值的區別,視為相等的記錄,視為不同的記錄,忽略null值】
與cardinality值相關的還有如下的幾個參數:
innodb_stats_persistent: 是否將命令analyze table計算得到的cardinality值存放到磁盤上。若是,則這樣做的好處是可以減少重新計算每個索引的cardinality值。
例如當MySQL數據庫重啟時。此外,用戶也可以通過命令create table和alter table的選項stats_persistent來對每張表進行控制。
innodb_stats_on_metadata: 當命令show table status, show index以及訪問information_schema架構下的表tables和statistics使,是否需要重新計算cardinality值,默認是OFF。
innodb_stats_persistent_sample_pages:若參數innodb_stats_persistent設置為ON,該參數表示analyze table更新cardinality值時的每次采樣頁的數量。默認是20.
innodb_stats_transient_sample_pages: 這個參數用來取代之前版本的innodb_stats_sample_pages參數,表示每次采樣頁的數量。默認是8.
查看表的一些基本信息:
mysql> show table status like "employees"\G *************************** 1. row *************************** Name: employees Engine: InnoDB Version: 10 Row_format: Dynamic #表格式 Rows: 299290 #表行數,對於myisam表這個值時精確的,對innodb這個值時估算的,可以使用select count(*) from tbname.來精確計算 Avg_row_length: 50 #表的評價每行的長度 Data_length: 15220736 #對myisam表,是數據文件的長度,以字節為單位。對innodb表,是為聚簇索引分配的大致內存量,以字節為單位。 Max_data_length: 0 #對於MyISAM,Max_data_length是數據文件的最大長度。這是在給定數據指針大小的情況下可以存儲在表中的數據的總字節數,未使用innodb。 Index_length: 0 Data_free: 2097152 # Auto_increment: NULL #下一個AUTO_INCREMENT值 Create_time: 2018-10-07 16:54:40 Update_time: NULL Check_time: NULL Collation: latin1_swedish_ci #排序規則 Checksum: NULL #實時校驗和 Create_options: Comment: 1 row in set (0.01 sec)
字段的詳細解釋可以查看:https://dev.mysql.com/doc/refman/5.7/en/show-table-status.html
在這里我們暫時只用到: Rows
#information_schema:這個庫中tables表和show table status輸出的內容是一樣。
可選擇性計算: Cardinality/ table_rows,數值越接近1,則說明索引的可選擇性越高。
查看數據庫中指定庫中表的索引的可選擇性,可以使用如下代碼:
USE information_schema; SELECT t.table_schema, t.table_name, a.index_name, t.table_rows, a.COLUMN_NAME, a.cardinality, a.cardinality / t.table_rows AS seletivity FROM TABLES t INNER JOIN ( SELECT s.table_schema, s.table_name, s.index_name, b.COLUMN_NAME, s.cardinality FROM statistics s INNER JOIN ( SELECT table_schema, table_name, index_name, GROUP_CONCAT(COLUMN_NAME) AS COLUMN_NAME, max(seq_in_index) AS seq_in_index FROM STATISTICS WHERE table_schema = "employees" GROUP BY table_schema, table_name, index_name ) b ON s.table_schema = b.table_schema AND s.table_name = b.table_name AND s.seq_in_index = b.seq_in_index ) a ON t.table_schema = a.table_schema AND t.table_name = a.table_name ORDER BY seletivity
結果如下:

explain語句
創建索引之后,我們可以使用explain語句查看select查詢是否使用了索引。
mysql> EXPLAIN SELECT * from employees LIMIT 1; +----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------+ | 1 | SIMPLE | employees | NULL | ALL | NULL | NULL | NULL | NULL | 299246 | 100.00 | NULL | +----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------+ 1 row in set, 1 warning (0.00 sec)
#explain語句各個字段解釋如下:
id: 表示當前select語句的編號,該值可能為空,如果行聯合了其他行的結果;在這種情況下table列顯示的是,引用的行的並集。
select_type: 這個值有很多,暫時可以先記以下幾個:
- SIMPLE: 簡單查詢,不包含連接查詢和子查詢。
- PRIMARY: 最外層查詢,主鍵查詢
- UNION:連接查詢的第二個或后面的查詢語句。 其余參數可以查看https://dev.mysql.com/doc/refman/5.7/en/explain-output.html
table: 查詢的表名
partitions:顯示查詢使用的分區,若為NULL則未使用分區。
type:表示表的連接類型,有如下取值:
- const :表示表中有多條記錄,但只從表中查詢一條記錄;
- eq_ref :表示多表連接時,后面的表使用了UNIQUE或者PRIMARY KEY;
- ref :表示多表查詢時,后面的表使用了普通索引;
- unique_ subquery:表示子查詢中使用了UNIQUE或者PRIMARY KEY;
- index_ subquery:表示子查詢中使用了普通索引;
- range :表示查詢語句中給出了查詢范圍;
- index :表示對表中的索引進行了完整的掃描;
- all :表示此次查詢進行了全表掃描;(一般來說全表掃描需要優化,表的記錄很少除外)
possible_keys:表示查詢中可能使用的索引;如果備選的數量大於3那說明已經太多了,因為太多會導致選擇索引而損耗性能, 所以建表時字段最好精簡,同時也要建立聯合索引,避免無效的單列索引;
key: 查詢實際使用的索引(不太准確,可以查閱官方文檔)。
key_len:索引的長度
ref: REF列顯示哪些列或常量與鍵列中所命名的索引進行比較,以從表中選擇行。
rows: 查詢掃描的行數。
filtered:表示按條件過濾表行的百分比,最大為100表示100%。
Extra: 表示查詢額外的附加信息說明。
上面的expalin語句也可以換位desc命令。
除了直接使用explain命令之外,MySQL5.7還支持json格式的輸出,
mysql> EXPLAIN format=json SELECT * from employees LIMIT 1\G *************************** 1. row *************************** EXPLAIN: { "query_block": { "select_id": 1, "cost_info": { "query_cost": "60778.20" }, "table": { "table_name": "employees", "access_type": "ALL", "rows_examined_per_scan": 299246, "rows_produced_per_join": 299246, "filtered": "100.00", "cost_info": { "read_cost": "929.00", "eval_cost": "59849.20", "prefix_cost": "60778.20", "data_read_per_join": "13M" }, "used_columns": [ "emp_no", "birth_date", "first_name", "last_name", "gender", "hire_date" ] } } } 1 row in set, 1 warning (0.00 sec) mysql>
