show index from 及analyze table 詳解
- 2019 年 10 月 4 日
- 筆記
內容參考:官方文檔、老葉茶館、姜承堯的InnoDB存儲引擎2
https://mp.weixin.qq.com/s/1MsyxhtG6Zk3Q9gIV2QVbA
https://dev.mysql.com/doc/refman/5.7/en/show-index.html
https://dev.mysql.com/doc/refman/5.7/en/innodb-analyze-table-complexity.html
https://dev.mysql.com/doc/refman/5.7/en/innodb-statistics-estimation.html
show index from tb1 G 或者使用select * from information_schema.STATISTICS G 都可以列出表上面存在哪些索引及其它相關資訊。
創建一個測試表:
CREATE TABLE `t1` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` char(20) CHARACTER SET latin1 NOT NULL DEFAULT '', `age` int(11) DEFAULT NULL, `sex` enum('F','M') CHARACTER SET latin1 DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`), KEY `idx_name_age` (`name`,`age`), KEY `idx_name_sub` (`name`(5)) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 ; INSERT INTO t1 (name,age,sex) VALUES('zhang san',24,'F'); INSERT INTO t1 (name,age,sex) VALUES('li si',26,'F'); INSERT INTO t1 (name,age,sex) VALUES('wang wu',20,'M'); INSERT INTO t1 (name,age,sex) VALUES('zhao liu',34,'M'); INSERT INTO t1 (name,age,sex) VALUES('John',64,'F'); show index from t1 ;
+---------+--------------+--------------+----------------+---------------+-------------+---------------+------------+----------+--------+--------------+-----------+-----------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | |---------+--------------+--------------+----------------+---------------+-------------+---------------+------------+----------+--------+--------------+-----------+-----------------| | t1 | 0 | PRIMARY | 1 | id | A | 5 | <null> | <null> | | BTREE | | | | t1 | 1 | idx_name | 1 | name | A | 5 | <null> | <null> | | BTREE | | | | t1 | 1 | idx_name_age | 1 | name | A | 5 | <null> | <null> | | BTREE | | | | t1 | 1 | idx_name_age | 2 | age | A | 5 | <null> | <null> | YES | BTREE | | | | t1 | 1 | idx_name_sub | 1 | name | A | 5 | 5 | <null> | | BTREE | | | +---------+--------------+--------------+----------------+---------------+-------------+---------------+------------+----------+--------+--------------+-----------+-----------------
詳解:
1.Table
表的名稱。
2.Non_unique
如果索引不能包括重複詞,則為0。如果可以,則為1。
3.Key_name
索引的名稱。
4.Seq_in_index
索引中的列序列號,從1開始。
5.Column_name
列名稱。
6.Collation
列以什麼方式存儲在索引中。在MySQL中,有值『A』(升序)或NULL(無分類)。[InnoDB都是A,Heap表都是NULL]
7.Cardinality
非常關鍵的一個參數。表示的是索引中唯一值的數目的估計值。
Cardinality/n_rows_in_table的值應儘可能接近1,如果非常小,那麼用戶需要考慮是否可以刪除該索引。
8.Sub_part
如果列只是被部分地編入索引,則為被編入索引的字元的數目。如果整列被編入索引,則為NULL。
9.Packed
指示關鍵字如何被壓縮。如果沒有被壓縮,則為NULL。
10.Null
索引的列中含有NULL。含有NULL則為YES。如果沒有,則這裡顯示為空。
11.Index_type
索引的類型(BTREE, FULLTEXT, HASH, RTREE)。這裡是InnoDB存儲引擎,所以顯示的都是BTREE
12.Comment
多種評註。
上面這一堆的狀態值中,最重要的就是Cardinality。
Cardinality 值:
表的cardinality(可以翻譯為「散列程度」),優化器會根據這個值來判斷是否使用這個索引。但是這個值不是實時更新的,因為實時的話代價太大了,因此這個值不是太準確,只是個估值。
如果cardinality大大少於數據的實際散列程度,那麼索引就基本失效了。
一般經驗是,在訪問表中很少一部分時使用B+樹索引才會有意義。對應性別、地區、這些欄位,它們可取值的範圍很小,稱為低選擇性。如:
SELECT* FROM students WHERE sex='M';
這種查詢的話,在大量樣本的情況下一般能返回50% 的數據,這時添加B+樹索引是完全沒有必要的。
相反,如果某個欄位的取值範圍很廣,幾乎沒有重複,即屬於高選擇性,則此時建議使用B+樹索引。
例如,對應姓名欄位,基本上在一個應用中不允許重名的出現。
在實際應用中,cardinality/n_rows_in_table應儘可能地接近1。如果非常小,那麼用戶需要考慮是否還有必要創建這個索引了。
故在訪問高選擇性屬性的欄位並從表中去除很少一部分數據時,對這個欄位添加B+樹索引是非常有必要的。
Cardinality 統計值的更新:
MySQL對Cardinality的統計是放在存儲引擎層進行的。
在生產環境,索引的更新操作可能會非常的頻繁。如果每次索引在發生操作時就對其進行Cardinality統計的話,會給資料庫造成很大的負擔。
假如一張表數據非常大,對其進行一次Cardinality統計可能要花費很長的時間,這對於生產環境而言,是不可接受的。
因此,資料庫對於Cardinality的統計都是通過取樣(Sample)的方法來完成的。
在InnoDB存儲引擎中,Cardinality統計資訊的更新發生在兩個操作中:INSERT、UPDATE。但是我們不可能每次變動就去更新Cardinality,這樣消耗太大了。
因此,InnoDB存儲引擎內部對更新Cardinality資訊的策略為:
1、 表中1/16的數據已發生過變化。
2、 stat_modified_counter > 20 0000 0000 【20億行】
第一種策略是自從上次統計Cardinality資訊後,表中1/16的數據已經發生過變化,這時需要更新Cardinality資訊。
第二種情況考慮的是,如果對表中某一行數據頻繁地更新操作,這時表中的數據實際上並沒有增加,實際發生變化的還是這一樣數據,則第一種更新策略就無法適用這種情況。故在InnoDB存儲引擎內部有一個計數器stat_modified_counter,用來表示發生變化的次數,當stat_modified_counter大於20 0000 0000時,則同樣需要更新Cardinality資訊。
InnoDB存儲引擎內部是如何進行Cardinality資訊的統計和更新操作的?
方法如下:
1、取得B+樹所以中葉子節點的數量,記為A。
2、InnoDB存儲引擎隨機對8個葉子節點(leaf page)進行取樣,統計每個頁不同記錄的個數,記為P1,P2 … P8。
3、估算出的Cardinality值為:(P1+P2…+P8) * A/8
因為是隨機取8個葉子節點做的統計,因此每次算出的Cardinality可能都不一樣。如下我從脫敏的資料庫摘錄的例子:
> use Mobile; > show index from wx_tableG *************************** 1. row *************************** Table: wx_table Non_unique: 0 Key_name: PRIMARY Seq_in_index: 1 Column_name: Id Collation: A Cardinality: 39129 #第一次計算的Cardinality結果。表示的是存儲引擎估算表中有多少個不同的值 Sub_part: NULL Packed: NULL Null: Index_type: BTREE Comment: Index_comment: 1 row in set (0.00 sec) > analyze table wx_table; #手工執行下,以便更新 Cardinality 值,不然要等到InnoDB的刷新要等好久。 +--------------------+---------+----------+----------+ | Table | Op | Msg_type | Msg_text | +--------------------+---------+----------+----------+ | Mobile.wx_table | analyze | status | OK | +--------------------+---------+----------+----------+ 1 row in set (0.01 sec) > show index from wx_tableG *************************** 1. row *************************** Table: wx_table Non_unique: 0 Key_name: PRIMARY Seq_in_index: 1 Column_name: Id Collation: A Cardinality: 41417 第二次計算的Cardinality結果 Sub_part: NULL Packed: NULL Null: Index_type: BTREE Comment: Index_comment: 1 row in set (0.00 sec)
如果我們再次執行 analyze table wx_table; 然後執行show index from wx_tableG 可以發現Cardinality的值又變了。
另外,如果用戶不管怎麼刷新,看到的Cardinality值都是不變的。一般這種情況發生在表足夠小,表的葉子節點數小於或等於8。這樣無論怎樣隨機取樣,都會全部取樣,算出的Cardinality總是一樣的。
與之相關的參數有:
>show VARIABLES like 'innodb_stats%'; +--------------------------------------+-------------+ | Variable_name | Value | |--------------------------------------+-------------| | innodb_stats_auto_recalc | ON | | innodb_stats_method | nulls_equal | | innodb_stats_on_metadata | OFF | | innodb_stats_persistent | ON | | innodb_stats_persistent_sample_pages | 20 | | innodb_stats_sample_pages | 8 | | innodb_stats_transient_sample_pages | 8 | +--------------------------------------+-------------+
innodb_stats_sample_pages
設置取樣的頁的數量,默認是8
MySQL5.6.3開始已經廢棄,改為使用innodb_stats_transient_sample_pages
innodb_stats_method
判斷如何對待索引中出現的NULL值記錄。
默認是nulls_equal,表示將NULL值記錄視為相等的記錄。
其有效值還有nulls_unequal和nulls_ignored。 nulls_unequal表示將NULL值記錄視為不同的記錄,nulls_ignored表示忽略NULL值記錄。
舉例子:
假如取樣的8個頁中索引記錄值為 NULL、NULL、1、2、2、3、3、3。
如果設置innodb_stats_method=nulls_equal,則Cardinality為4; 【取樣值:NULL、1、2、3】
如果設置innodb_stats_method=nulls_unequal,則Cardinality為5; 【取樣值:NULL、NULL、1、2、3】
如果設置innodb_stats_method=nulls_ignored,則Cardinality為3; 【取樣值:1、2、3】
注意:
當我們執行analyze table xxx; show table status; show index from tb_name; 以及訪問information_schema架構下的表tables和statistics時會導致InnoDB存儲引擎去重新計算索引的Cardinality值。
若表中存在多個輔助索引時,執行上述這些操作可能會非常慢,雖然用戶可能並不希望去更新Cardinality值。
在innodb1.2後,對這些參數又做了下擴展及修改:
innodb_stats_persistent 默認ON
控制是否使用永久化的統計數據(也就是是否將analyze table計算出的Cardinality值存放到磁碟上)。
設置為ON的好處是可以減少重新計算每個索引的Cardinality值,例如當MySQL資料庫重啟時。
此外,用戶也可通過命令create table和alter table的選項STATS_PERSISTENT來對每張表進行控制。
例如:
關閉某張表的取樣統計:
CREATE TABLE `t2` (a int(11) NOT NULL AUTO_INCREMENT,b int(11) DEFAULT NULL, PRIMARY KEY (a), KEY idx_b (b)) ENGINE=InnoDB STATS_PERSISTENT=0;
手動設置取樣的page數量:
CREATE TABLE `t3` (a int(11) NOT NULL AUTO_INCREMENT,b int(11) DEFAULT NULL, PRIMARY KEY (a), KEY idx_b (b)) ENGINE=InnoDB STATS_SAMPLE_PAGES=30;
永久化的統計數據存儲在 mysql.innodb_index_stats 和 mysql.innodb_table_stats 中,官方文檔說統計數據並非實時的,也就是收集統計數據會滯後幾分鐘,如果想要及時的更新統計可以執行analyze table。
innodb_stats_persistent_sample_pages 默認是20
只有在 innodb_stats_persistent=ON時候,innodb_stats_persistent_sample_pages 參數的設置才會生效。 【為了獲得更準確的執行計劃,我們可以設置my.cnf裡面這個值稍微大些,如設置為32或者64】
官方文檔:
innodb_stats_persistent_sample_pages only applies when innodb_stats_persistent is enabled for a table; when innodb_stats_persistent is disabled, innodb_stats_transient_sample_pages applies instead.
https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_stats_on_metadata
該參數表示analyze table更新Cardinality值時每次需要取樣的頁的數量。默認是20個頁。
增加這個值,可以提高統計資訊的精確度,同樣也能提高執行計劃的準確性,不過也相應增加了analyze table的時間,也會增加在InnoDB表上分析的I/O開銷。
innodb_stats_on_metadata 默認為OFF 【不建議設置為ON】
注意:這個參數只有在 innodb_stats_persistent=OFF 時候才會起作用。
【官方文檔:https://dev.mysql.com/doc/refman/5.7/en/innodb-statistics-estimation.html
innodb_stats_on_metadata only applies when optimizer statistics are configured to be non-persistent (when innodb_stats_persistent is disabled).】
如果這個參數是ON的話, 使用show table status、show index from tb_name、及訪問information_schema架構下的表tables和statistics時,會立即導致存儲引擎重新計算索引的Cardinality值,這個造成的影響在日常業務時間是不能接受。
innodb_stats_transient_sample_pages
表示每次取樣頁的數量。默認是8。該參數用來取代老版本裡面之前版本的參數 innodb_stats_sample_pages
只有在 某張表 STATS_PERSISTENT=0 時候, innodb_stats_transient_sample_pages 參數的設置才會生效。
官方文檔:
innodb_stats_transient_sample_pages only applies when innodb_stats_persistent is disabled for a table; when innodb_stats_persistent is enabled, innodb_stats_persistent_sample_pages applies instead.
https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_stats_on_metadata
innodb_stats_auto_recalc
用於決定是否在表上存在大量更新時(超過10%的記錄更新)重新計算統計資訊。
默認打開.
如果關閉該選項,就需要在每次創建索引或者更改列之後,運行一次ANALYZE TABLE命令來更新統計資訊,否則可能選擇錯誤的執行計劃。
同樣的,也可以在CREATE TABLE/ALTER TABLE命令中指定STATS_AUTO_RECALC值。
analyze table 說明:
老葉部落格:https://mp.weixin.qq.com/s/1MsyxhtG6Zk3Q9gIV2QVbA
官網:https://dev.mysql.com/doc/refman/5.7/en/innodb-analyze-table-complexity.html
ANALYZE TABLE 作用:
ANALYZE TABLE 會統計索引分布資訊,並將結果持久化存儲;
對於 MyISAM 表,相當於執行了一次 myisamchk –analyze;
支援 InnoDB、NDB、MyISAM 等存儲引擎,但不支援 視圖(view);
ANALYZE TABLE也可以用在表分區上;
對InnoDB、MyISAM表執行 ANALYZE TABLE 時,會加上讀鎖(read lock);
執行 ANALYZE TABLE 會記錄binlog。(這是合理的,因為索引分析這個操作,在MASTER端執行完後,SLAVE端也是需要的)
ANALYZE TABLE 代價估算:
影響代價因素:
innodb_stats_persistent_sample_pages定義值大小;
表中索引數多少;
表中分區數多少。
analye table的代價 = innodb_stats_persistent_sample_pages * 索引數 * 分區數
而更嚴謹的計算公式見下:
O(n_sample * (n_cols_in_uniq_i + n_cols_in_non_uniq_i + n_cols_in_pk * (1 + n_non_uniq_i)) * n_part)
各項指標解釋:
n_sample
採集的data page數量
is the number of pages sampled (defined by innodb_stats_persistent_sample_pages)
n_cols_in_uniq_i
所有唯一索引(不含主鍵索引)中的列總數
is total number of all columns in all unique indexes (not counting the primary key columns)
n_cols_in_non_uniq_i
所有普通索引中的列總數
is the total number of all columns in all non-unique indexes
n_cols_in_pk
主鍵索引中的列總數(若未顯式定義主鍵,則相當於只有一列的ROWID)
is the number of columns in the primary key (if a primary key is not defined, InnoDB creates a single column primary key internally)
n_non_uniq_i
非唯一索引數量
is the number of non-unique indexes in the table
n_part
表分區數量
is the number of partitions. If no partitions are defined, the table is considered to be a single partition.
mysql官網的實例:
USE test; CREATE TABLE t ( a INT, b INT, c INT, d INT, e INT, f INT, g INT, h INT, PRIMARY KEY (a, b), UNIQUE KEY i1uniq (c, d), KEY i2nonuniq (e, f), KEY i3nonuniq (g, h) ); SELECT index_name, stat_name, stat_description FROM mysql.innodb_index_stats WHERE database_name='test' AND table_name='t' AND stat_name like 'n_diff_pfx%'; +------------+--------------+------------------+ | index_name | stat_name | stat_description | +------------+--------------+------------------+ | PRIMARY | n_diff_pfx01 | a | | PRIMARY | n_diff_pfx02 | a,b | | i1uniq | n_diff_pfx01 | c | | i1uniq | n_diff_pfx02 | c,d | | i2nonuniq | n_diff_pfx01 | e | | i2nonuniq | n_diff_pfx02 | e,f | | i2nonuniq | n_diff_pfx03 | e,f,a | | i2nonuniq | n_diff_pfx04 | e,f,a,b | | i3nonuniq | n_diff_pfx01 | g | | i3nonuniq | n_diff_pfx02 | g,h | | i3nonuniq | n_diff_pfx03 | g,h,a | | i3nonuniq | n_diff_pfx04 | g,h,a,b | +------------+--------------+------------------+
上面這個結果看起來有點奇怪是不是,其實沒錯,先科普幾點知識:
所有的普通索引,實際物理存儲時,都要包含主鍵列的,也就是所謂的 index extensions 特性;
統計索引資訊時,是根據最左原則,要統計各種組合的。比如(a,b) 索引,要統計(a), (a,b), (a,b,pk) 三種資訊,而不是只統計(a,b)這個資訊;
不過,在 mysql.innodb_index_stats 中存儲統計資訊時,是不統計唯一索引後面存儲主鍵列資訊的,非唯一普通索引後存儲主鍵列資訊則會被統計進去;
因此,上面 mysql.innodb_index_stats 中存儲的統計結果是正確的。
我們再回來看下索引統計的代價公式,像下面這樣計算:
– n_sample,採集的data page數量,值為 20(默認值);
– n_cols_in_uniq_i,所有唯一索引(不含主鍵索引)中的列總數,值為 2(c和d)
– n_cols_in_non_uniq_i,所有普通索引中的列總數,值為 4 (e、f、g、h)
– n_cols_in_pk,主鍵索引中的列總數(若未顯式定義主鍵,則相當於只有一列的ROWID),值為 2 (a、b)
– n_non_uniq_i,非唯一索引數量,值為 2 (注意,這裡指的是索引的數量,而不是不是列的數量。因此這裡是i2nonuniq、i3nonuniq)
– n_part,表分區數量,值為 1(沒有表分區,值為1,而不是0)。
那麼根據公式最終需要掃描的data page數結果就是:
20 * (2 + 4 + 2 * (1 + 2)) * 1 = 240 (單位是pages)
實際需要讀取的位元組數則是:240 * 16*1024 = 3932160 (即 3.84M)
當然了,要讀取的data page,有可能已經在buffer pool中了,因此並不全是物理讀。
從中,我們也可以看到,這個代價和表的數據量並無直接關係。
不過,當表數量越大時,聚集索引的 B+ 樹也越大,搜索代價肯定也越大。