日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

distinct作用于后面所有的列吗_InnoDB索引允许NULL对性能有影响吗

發(fā)布時(shí)間:2025/4/5 编程问答 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 distinct作用于后面所有的列吗_InnoDB索引允许NULL对性能有影响吗 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

談?wù)処nnoDB輔助索引的幾個(gè)特征。

閱讀目錄

0. 初始化測(cè)試表、數(shù)據(jù)1. 問(wèn)題1:索引列允許為NULL,對(duì)性能影響有多少? ??結(jié)論1,存儲(chǔ)大量的NULL值,除了計(jì)算更復(fù)雜之外,數(shù)據(jù)掃描的代價(jià)也會(huì)更高一些2. 問(wèn)題2:輔助索引需要MVCC多版本讀的時(shí)候,為什么需要依賴聚集索引? ??結(jié)論2,輔助索引中不存儲(chǔ)DB_TRX_ID,需要依托聚集索引實(shí)現(xiàn)MVCC3. 問(wèn)題3:為什么查找數(shù)據(jù)時(shí),一定要讀取葉子節(jié)點(diǎn),只讀非葉子節(jié)點(diǎn)不行嗎? ??結(jié)論3,在索引樹中查找數(shù)據(jù)時(shí),最終一定是要讀取葉子節(jié)點(diǎn)才行4. 問(wèn)題4:索引列允許為NULL,會(huì)額外存儲(chǔ)更多字節(jié)嗎? 結(jié)論4,定義列值允許為NULL并不會(huì)增加物理存儲(chǔ)代價(jià),但對(duì)索引效率的影響要另外考慮5. 幾點(diǎn)總結(jié)6. 延伸閱讀

本文開始之前,有幾篇文章建議先復(fù)習(xí)一下

  • InnoDB表聚集索引層高什么時(shí)候發(fā)生變化

  • 淺析InnoDB索引結(jié)構(gòu)

  • Innodb頁(yè)合并和頁(yè)分裂

  • innblock | InnoDB page觀察利器

接下來(lái),我們一起測(cè)試驗(yàn)證關(guān)于輔助索引的幾個(gè)特點(diǎn)。

0. 初始化測(cè)試表、數(shù)據(jù)

測(cè)試表結(jié)構(gòu)如下:

[root@yejr.run]>?CREATE?TABLE?`t_sk`?(
??`id`?int(10)?unsigned?NOT?NULL?AUTO_INCREMENT,
??`c1`?int(10)?unsigned?NOT?NULL,
??`c2`?int(10)?unsigned?NOT?NULL,
??`c3`?int(10)?unsigned?NOT?NULL,
??`c4`?int(10)?unsigned?NOT?NULL,
??`c5`?datetime?NOT?NULL,
??`c6`?char(20)?NOT?NULL,
??`c7`?varchar(30)?NOT?NULL,
??`c8`?varchar(30)?NOT?NULL,
??`c9`?varchar(30)?NOT?NULL,
??PRIMARY?KEY?(`id`),
??KEY?`k1`?(`c1`)
)?ENGINE=InnoDB;

除了主鍵索引外,還有個(gè)?c1?列上的輔助索引。

用?mysql_random_data_load?灌入50萬(wàn)測(cè)試數(shù)據(jù)。

[root@yejr.run]#?mysql_random_data_load?-hXX?-uXX?-pXX?test?t_sk?500000

1. 問(wèn)題1:索引列允許為NULL,對(duì)性能影響有多少

把輔助索引列?c1?修改為允許NULL,并且隨機(jī)更新5萬(wàn)條數(shù)據(jù),將 c1 列設(shè)置為NULL

[root@yejr.run]>?alter?table?t_sk?modify?c1?int?unsigned;

[root@yejr.run]>?update?t_sk?set?c1?=?NULL?order?by?rand()?limit?50000;
Query?OK,?50000?rows?affected?(2.83?sec)
Rows?matched:?50000??Changed:?50000??Warnings:?0
#隨機(jī)1/10為null
[root@yejr.run]>?select?count(*)?from?t_sk?where?c1?is?null;
+----------+
|?count(*)?|
+----------+
|????50000?|
+----------+

好,現(xiàn)在觀察輔助索引的索引數(shù)據(jù)頁(yè)結(jié)構(gòu)。

[root@yejr.run]#?innblock?test/t_sk.ibd?scan?16
...
Datafile?Total?Size:100663296
===INDEX_ID:46???--聚集索引(主鍵索引)
level2?total?block?is?(1)??--根節(jié)點(diǎn),層高2(共3層),共1個(gè)page
block_no:?????????3,level:???2|*|
level1?total?block?is?(5)??--中間節(jié)點(diǎn),層高1,共5個(gè)page
block_no:???????261,level:???1|*|block_no:???????262,level:???1|*|block_no:???????263,level:???1|*|
block_no:???????264,level:???1|*|block_no:???????265,level:???1|*|
level0?total?block?is?(5020)??--葉子節(jié)點(diǎn),層高0,共5020個(gè)page
block_no:?????????5,level:???0|*|block_no:?????????6,level:???0|*|block_no:?????????7,level:???0|*|
...
===INDEX_ID:47???--輔助索引
level1?total?block?is?(1)??--根節(jié)點(diǎn),層高1(共2層),共1個(gè)page
block_no:?????????4,level:???1|*|
level0?total?block?is?(509)??--葉子節(jié)點(diǎn),層高0,共509個(gè)page
block_no:????????18,level:???0|*|block_no:????????19,level:???0|*|block_no:????????31,level:???0|*|
...

觀察輔助索引的根節(jié)點(diǎn)里的數(shù)據(jù)

[root@yejr.run]#?innodb_space?-s?ibdata1?-T?test/t_sk?-p?4?page-dump
...
records:
{:format=>:compact,
?:offset=>126,????--第一條記錄
?:header=>
??{:next=>428,
???:type=>:node_pointer,
???:heap_number=>2,
???:n_owned=>0,
???:min_rec=>true,????--min_rec表示最小記錄
???:deleted=>false,
???:nulls=>["c1"],
???:lengths=>{},
???:externs=>[],
???:length=>6},
?:next=>428,
?:type=>:secondary,
?:key=>[{:name=>"c1",?:type=>"INT?UNSIGNED",?:value=>:NULL}],????--對(duì)應(yīng)c1列值為NULL
?:row=>[{:name=>"id",?:type=>"INT?UNSIGNED",?:value=>9}],????--對(duì)應(yīng)id=9
?:sys=>[],
?:child_page_number=>18,????--指向葉子節(jié)點(diǎn)?pageno?=?18
?:length=>8}
...
{:format=>:compact,
?:offset=>6246,????--最后一條記錄(next=>112,指向supremum)
?:header=>
??{:next=>112,
???:type=>:node_pointer,
???:heap_number=>346,
???:n_owned=>0,
???:min_rec=>false,
???:deleted=>false,
???:nulls=>[],
???:lengths=>{},
???:externs=>[],
???:length=>6},
?:next=>112,
?:type=>:secondary,
?:key=>[{:name=>"c1",?:type=>"INT?UNSIGNED",?:value=>2142714688}],????--對(duì)應(yīng)c1=2142714688
?:row=>[{:name=>"id",?:type=>"INT?UNSIGNED",?:value=>73652}],????--對(duì)應(yīng)id=73652
?:sys=>[],
?:child_page_number=>2935,????--指向葉子節(jié)點(diǎn)2935
?:length=>12}

經(jīng)過(guò)統(tǒng)計(jì),根節(jié)點(diǎn)中c1列值為NULL的記錄共有33條,其余476條是c1列值為非NULL,共509條記錄。

葉子節(jié)點(diǎn)中,每個(gè)page大約可以存儲(chǔ)1547條記錄,共有5萬(wàn)條記錄值為NULL,因此需要至少33個(gè)page來(lái)保存(ceiling(50000/1547) = 33)。

看下這個(gè)SQL的查詢計(jì)劃

[root@yejr.run]>?desc?select?count(*)?from?t_sk?where?c1?is?null\G
***************************?1.?row?***************************
???????????id:?1
??select_type:?SIMPLE
????????table:?t_sk
???partitions:?NULL
?????????type:?ref
possible_keys:?k1
??????????key:?k1
??????key_len:?5ref:?const
?????????rows:?99112
?????filtered:?100.00
????????Extra:?Using?where;?Using?index

從上面的輸出中,我們能看到,當(dāng)索引列設(shè)置允許為NULL時(shí),是會(huì)對(duì)其納入索引統(tǒng)計(jì)信息,并且值為NULL的記錄,都是存儲(chǔ)在索引樹的最左邊。

接下來(lái),跑幾個(gè)SQL查詢。

SQL1,統(tǒng)計(jì)所有NULL值數(shù)量

[root@yejr.run]>?select?count(*)?from?t_sk?where?c1?is?null;
+----------+
|?count(*)?|
+----------+
|????50000?|
+----------+

查看slow log

InnoDB_pages_distinct:?34
...
select?count(*)?from?t_sk?where?c1?is?null;

共需要掃描34個(gè)page,根節(jié)點(diǎn)(1)+葉子節(jié)點(diǎn)(33),正好34個(gè)page。

備注:需要用Percona版本才能在slow query log中有InnoDB_pages_distinct信息。

SQL2, 查詢 c1 is null

[root@yejr.run]>?select?id,c1?from?t_sk?where?c1?is?null?limit?1;
+------+------+
|?id???|?c1???|
+------+------+
|?9607?|?NULL?|
+------+------+

查看slow log

InnoDB_pages_distinct:?12
...
select?id,c1?from?t_sk?where?c1?is?null?limit?1;

這次的查詢需要掃描12個(gè)page,除去1個(gè)根節(jié)點(diǎn)外,還需要掃描12個(gè)葉子節(jié)點(diǎn),只是為了返回一條數(shù)據(jù)而已,這代價(jià)有點(diǎn)大。

如果把SQL微調(diào)改成下面這樣

[root@yejr.run]>?select?id,c1?from?t_sk?where?c1?is?null?limit?10000,1;
+-------+------+
|?id????|?c1???|
+-------+------+
|?99671?|?NULL?|
+-------+------+

可以看到還是需要掃描12個(gè)page。

InnoDB_pages_distinct:?12
...
select?id,c1?from?t_sk?where?c1?is?null?limit?10000,1;

SQL3, 查詢 c1 任意非NULL值
如果把 c1列條件改成正常的int值,結(jié)果就不太一樣了

[root@yejr.run]>?select?id,?c1?from?t_sk?where?c1??=?907299016;
+--------+-----------+
|?id?????|?c1????????|
+--------+-----------+
|?365115?|?907299016?|
+--------+-----------+
1?row?in?set?(0.00?sec)

slow log是這樣的

InnoDB_pages_distinct:?2
...
select?id,?c1?from?t_sk?where?c1??=?907299016;

可以看到,只需要掃描2個(gè)page,這個(gè)看起來(lái)就正常了。

結(jié)論1,存儲(chǔ)大量的NULL值,除了計(jì)算更復(fù)雜之外,數(shù)據(jù)掃描的代價(jià)也會(huì)更高一些

另外,如果要查詢的c1值正好介于兩個(gè)page的臨界位置,那么需要多讀取一個(gè)page。

掃描第31號(hào)page,確認(rèn)該數(shù)據(jù)頁(yè)中的最小和最大物理記錄

[root@yejr.run]#?innodb_space?-s?ibdata1?-T?test/t_sk?-p?31?page-dump
...
records:
{:format=>:compact,
?:offset=>126,
?:header=>
??{:next=>9996,
???:type=>:conventional,
???:heap_number=>2,
???:n_owned=>0,
???:min_rec=>false,
???:deleted=>false,
???:nulls=>[],
???:lengths=>{},
???:externs=>[],
???:length=>6},
?:next=>9996,
?:type=>:secondary,
?:key=>[{:name=>"c1",?:type=>"INT?UNSIGNED",?:value=>1531865685}],
?:row=>[{:name=>"id",?:type=>"INT?UNSIGNED",?:value=>1507}],
?:sys=>[],
?:length=>8}
?...
{:format=>:compact,
?:offset=>5810,
?:header=>
??{:next=>112,
???:type=>:conventional,
???:heap_number=>408,
???:n_owned=>0,
???:min_rec=>false,
???:deleted=>false,
???:nulls=>[],
???:lengths=>{},
???:externs=>[],
???:length=>6},
?:next=>112,
?:type=>:secondary,
?:key=>[{:name=>"c1",?:type=>"INT?UNSIGNED",?:value=>1536700825}],
?:row=>[{:name=>"id",?:type=>"INT?UNSIGNED",?:value=>361382}],
?:sys=>[],
?:length=>8}?

指定c1的值為 1531865685、1536700825 執(zhí)行查詢,查看slow log,確認(rèn)都需要掃描3個(gè)page,而如果換成介于這兩個(gè)值之間的數(shù)據(jù),則只需要掃描2個(gè)page。

InnoDB_pages_distinct:?3
...
select?id,?c1?from?t_sk?where?c1??=?1531865685;

InnoDB_pages_distinct:?3
...
select?id,?c1?from?t_sk?where?c1??=?1536700825;

InnoDB_pages_distinct:?2
...
select?id,?c1?from?t_sk?where?c1??=?1536630003;

InnoDB_pages_distinct:?2
...
select?id,?c1?from?t_sk?where?c1??=?1536575377;

這是因?yàn)檩o助索引是非唯一的,即便是在等值查詢時(shí),也需要再讀取下一條記錄,以確認(rèn)已獲取所有符合條件的數(shù)據(jù)。

還有,當(dāng)利用輔助索引讀取數(shù)據(jù)時(shí),如果要讀取整行數(shù)據(jù),則需要回表。

也就是說(shuō),除了掃描輔助索引數(shù)據(jù)頁(yè)之外,還需要掃描聚集索引數(shù)據(jù)頁(yè)。

來(lái)個(gè)例子看看就知道了。

#無(wú)需回表時(shí)
InnoDB_pages_distinct:?2
...
select?id,?c1?from?tnull?where?c1??=?1536630003;

#需要回表時(shí)
InnoDB_pages_distinct:?5
...
select?*?from?t_sk?where?c1??=?1536630003;

需要回表時(shí),除了掃描輔助索引頁(yè)2個(gè)page外,還需要回表掃描聚集索引頁(yè),而聚集索引是個(gè)3層樹,因此總共需要掃描5個(gè)page。

2. 問(wèn)題2:輔助索引需要MVCC多版本讀的時(shí)候,為什么需要依賴聚集索引

InnoDB的MVCC是通過(guò)在聚集索引頁(yè)中同時(shí)存儲(chǔ)了DB_TRX_ID和DB_ROLL_PTR來(lái)實(shí)現(xiàn)的。

但是我們從上面page dump出來(lái)的結(jié)果也很明顯能看到,附注索引頁(yè)是不存儲(chǔ)DB_TRX_ID信息的。

所以說(shuō),輔助索引上如果想要實(shí)現(xiàn)MVCC,需要通過(guò)回表讀聚集索引來(lái)實(shí)現(xiàn)。

結(jié)論2,輔助索引中不存儲(chǔ)DB_TRX_ID,需要依托聚集索引實(shí)現(xiàn)MVCC

3. 問(wèn)題3:為什么查找數(shù)據(jù)時(shí),一定要讀取葉子節(jié)點(diǎn),只讀非葉子節(jié)點(diǎn)不行嗎

在輔助索引的根節(jié)點(diǎn)這個(gè)頁(yè)面中(pageno=4),我們注意到它記錄的最小記錄(min_rec)對(duì)應(yīng)的是(c1=NULL, id=9)這條記錄。

在它指向的葉子節(jié)點(diǎn)頁(yè)面中(pageno=18)也確認(rèn)了這個(gè)情況。

現(xiàn)在把id=9的記錄刪掉,看看輔助索引數(shù)據(jù)頁(yè)會(huì)發(fā)生什么變化。

[root@yejr.run]>?delete?from?t_sk?where?id?=?9?and?c1?is?null;
Query?OK,?1?row?affected?(0.01?sec)

先檢查第4號(hào)數(shù)據(jù)頁(yè)。

[root@yejr.run]#?innodb_space?-s?ibdata1?-T?test/t_sk?-p?4?page-dump
...
records:
{:format=>:compact,
?:offset=>126,
?:header=>
??{:next=>428,
???:type=>:node_pointer,
???:heap_number=>2,
???:n_owned=>0,
???:min_rec=>true,
???:deleted=>false,
???:nulls=>["c1"],
???:lengths=>{},
???:externs=>[],
???:length=>6},
?:next=>428,
?:type=>:secondary,
?:key=>[{:name=>"c1",?:type=>"INT?UNSIGNED",?:value=>:NULL}],
?:row=>[{:name=>"id",?:type=>"INT?UNSIGNED",?:value=>9}],
?:sys=>[],
?:child_page_number=>18,
?:length=>8}
...

看到第四號(hào)數(shù)據(jù)頁(yè)中,最小記錄還是 id=9,沒(méi)有更新。

再查看第18號(hào)數(shù)據(jù)頁(yè)。

[root@yejr.run]#?innodb_space?-s?ibdata1?-T?test/t_sk?-p?18?page-dump
...
records:
{:format=>:compact,
?:offset=>136,
?:header=>
??{:next=>146,
???:type=>:conventional,
???:heap_number=>3,
???:n_owned=>0,
???:min_rec=>false,
???:deleted=>false,
???:nulls=>["c1"],
???:lengths=>{},
???:externs=>[],
???:length=>6},
?:next=>146,
?:type=>:secondary,
?:key=>[{:name=>"c1",?:type=>"INT?UNSIGNED",?:value=>:NULL}],
?:row=>[{:name=>"id",?:type=>"INT?UNSIGNED",?:value=>30}],
?:sys=>[],
?:length=>4}
...

在這個(gè)數(shù)據(jù)頁(yè)(葉子節(jié)點(diǎn))中,最小記錄已經(jīng)被更新成 id=30 這條數(shù)據(jù)了。

可見,索引樹中的非葉子節(jié)點(diǎn)數(shù)據(jù)不是實(shí)時(shí)更新的,只有葉子節(jié)點(diǎn)的數(shù)據(jù)才是最準(zhǔn)確的。

結(jié)論3,在索引樹中查找數(shù)據(jù)時(shí),最終一定是要讀取葉子節(jié)點(diǎn)才行

4. 問(wèn)題4:索引列允許為NULL,會(huì)額外存儲(chǔ)更多字節(jié)嗎

之前流傳有一種說(shuō)法,不允許設(shè)置列值允許NULL,是因?yàn)闀?huì)額外多存儲(chǔ)一個(gè)字節(jié),事實(shí)是這樣嗎?

我們先把c1列改成NOT NULL DEFAULT 0,當(dāng)然了,改之前要先把所有NULL值更新成0。

[root@yejr.run]>?update?t_sk?set?c1=0?where?c1?is?null;
[root@yejr.run]>?alter?table?t_sk?modify?c1?int?unsigned?not?null?default?0;

在修改之前,每條索引記錄長(zhǎng)度都是10字節(jié),更新之后卻變成了13個(gè)字節(jié)。
直接對(duì)比索引頁(yè)中的數(shù)據(jù),發(fā)現(xiàn)不同之處

#允許為NULL,且默認(rèn)值為NULL時(shí)
{:format=>:compact,
?:offset=>136,
?:header=>
??{:next=>146,
???:type=>:conventional,
???:heap_number=>3,
???:n_owned=>0,
???:min_rec=>false,
???:deleted=>false,
???:nulls=>["c1"],
???:lengths=>{},
???:externs=>[],
???:length=>6},
?:next=>146,
?:type=>:secondary,
?:key=>[{:name=>"c1",?:type=>"INT?UNSIGNED",?:value=>:NULL}],
?:row=>[{:name=>"id",?:type=>"INT?UNSIGNED",?:value=>48}],
?:sys=>[],
?:length=>4}


#不允許為NULL,默認(rèn)值為0時(shí)
{:format=>:compact,
?:offset=>138,
?:header=>
??{:next=>151,
???:type=>:conventional,
???:heap_number=>3,
???:n_owned=>0,
???:min_rec=>false,
???:deleted=>false,
???:nulls=>[],
???:lengths=>{},
???:externs=>[],
???:length=>5},
?:next=>151,
?:type=>:secondary,
?:key=>[{:name=>"c1",?:type=>"INT?UNSIGNED",?:value=>0}],
?:row=>[{:name=>"id",?:type=>"INT?UNSIGNED",?:value=>48}],
?:sys=>[],
?:length=>8}

可以看到,原先允許為NULL時(shí),record header需要多一個(gè)字節(jié)(共6字節(jié)),但實(shí)際物理存儲(chǔ)中無(wú)需存儲(chǔ)NULL值。

而當(dāng)設(shè)置為NOT NULL DEFAULT 0時(shí),record header只需要5字節(jié),但實(shí)際物理存儲(chǔ)卻多了4字節(jié),總共多了3字節(jié),所以索引記錄以前是10字節(jié),更新后變成了13字節(jié),實(shí)際上代價(jià)反倒變大了。

列值允許為NULL更多的是計(jì)算代價(jià)變大了,以及索引對(duì)索引效率的影響,反倒可以說(shuō)是節(jié)省了物理存儲(chǔ)開銷。

結(jié)論4,定義列值允許為NULL并不會(huì)增加物理存儲(chǔ)代價(jià),但對(duì)索引效率的影響要另外考慮

最后,本文使用的MySQL版本Percona-Server-5.7.22,下載源碼后自編譯的。

Server?version:????????5.7.22-22-log?Source?distribution

5. 幾點(diǎn)總結(jié)

最后針對(duì)InnoDB輔助索引,總結(jié)幾條建議吧。
a) 索引列最好不要設(shè)置允許NULL。
b) 如果是非索引列,設(shè)置允許為NULL基本上無(wú)所謂。
c) 輔助索引需要依托聚集索引實(shí)現(xiàn)MVCC。
d) 葉子節(jié)點(diǎn)總是存儲(chǔ)最新數(shù)據(jù),而非葉子節(jié)點(diǎn)則不一定。
e) 盡可能不SELECT *,盡量利用覆蓋索引完成查詢,能不回表就不回表。

6. 延伸閱讀

  • InnoDB表聚集索引層高什么時(shí)候發(fā)生變化

  • 淺析InnoDB索引結(jié)構(gòu)

  • Innodb頁(yè)合并和頁(yè)分裂

  • innblock | InnoDB page觀察利器

  • jcole.us:The physical structure of InnoDB index pages

  • jcole.us:B+Tree index structures in InnoDB

Enjoy MySQL :)

全文完。


由葉老師主講的「MySQL優(yōu)化」課已升級(jí)到MySQL 8.0版本,掃碼開啟MySQL 8.0的修行之旅吧

總結(jié)

以上是生活随笔為你收集整理的distinct作用于后面所有的列吗_InnoDB索引允许NULL对性能有影响吗的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。