您好,登錄后才能下訂單哦!
本篇內(nèi)容介紹了“MySQL中不建議使用SELECT *的原因是什么”的有關(guān)知識,在實(shí)際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
“不要使用SELECT *
”幾乎已經(jīng)成為了MySQL使用的一條金科玉律,就連《阿里Java開發(fā)手冊》也明確表示不得使用*
作為查詢的字段列表,更是讓這條規(guī)則擁有了權(quán)威的加持。
不過我在開發(fā)過程中直接使用SELECT *
還是比較多的,原因有兩個(gè):
因?yàn)楹唵?,開發(fā)效率非常高,而且如果后期頻繁添加或修改字段,SQL語句也不需要改變;
我認(rèn)為過早優(yōu)化是個(gè)不好的習(xí)慣,除非在一開始就能確定你最終實(shí)際需要的字段是什么,并為之建立恰當(dāng)?shù)乃饕?;否則,我選擇遇到麻煩的時(shí)候再對SQL進(jìn)行優(yōu)化,當(dāng)然前提是這個(gè)麻煩并不致命。
但是我們總得知道為什么不建議直接使用SELECT *
,本文從4個(gè)方面給出理由。
我們知道 MySQL 本質(zhì)上是將用戶記錄存儲在磁盤上,因此查詢操作就是一種進(jìn)行磁盤IO的行為(前提是要查詢的記錄沒有緩存在內(nèi)存中)。
查詢的字段越多,說明要讀取的內(nèi)容也就越多,因此會增大磁盤 IO 開銷。尤其是當(dāng)某些字段是 TEXT
、MEDIUMTEXT
或者BLOB
等類型的時(shí)候,效果尤為明顯。
那使用SELECT *
會不會使MySQL占用更多的內(nèi)存呢?
理論上不會,因?yàn)閷τ赟erver層而言,并非是在內(nèi)存中存儲完整的結(jié)果集之后一下子傳給客戶端,而是每從存儲引擎獲取到一行,就寫到一個(gè)叫做net_buffer
的內(nèi)存空間中,這個(gè)內(nèi)存的大小由系統(tǒng)變量net_buffer_length
來控制,默認(rèn)是16KB;當(dāng)net_buffer
寫滿之后再往本地網(wǎng)絡(luò)棧的內(nèi)存空間socket send buffer
中寫數(shù)據(jù)發(fā)送給客戶端,發(fā)送成功(客戶端讀取完成)后清空net_buffer
,然后繼續(xù)讀取下一行并寫入。
也就是說,默認(rèn)情況下,結(jié)果集占用的內(nèi)存空間最大不過是net_buffer_length
大小罷了,不會因?yàn)槎鄮讉€(gè)字段就占用額外的內(nèi)存空間。
承接上一點(diǎn),雖然每次都是把socket send buffer
中的數(shù)據(jù)發(fā)送給客戶端,單次看來數(shù)據(jù)量不大,可架不住真的有人用*把TEXT
、MEDIUMTEXT
或者BLOB
類型的字段也查出來了,總數(shù)據(jù)量大了,這就直接導(dǎo)致網(wǎng)絡(luò)傳輸?shù)拇螖?shù)變多了。
如果MySQL和應(yīng)用程序不在同一臺機(jī)器,這種開銷非常明顯。即使MySQL服務(wù)器和客戶端是在同一臺機(jī)器上,使用的協(xié)議還是TCP,通信也是需要額外的時(shí)間。
為了說明這個(gè)問題,我們需要建一個(gè)表
CREATE TABLE `user_innodb` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `gender` tinyint(1) DEFAULT NULL, `phone` varchar(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `IDX_NAME_PHONE` (`name`,`phone`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
我們創(chuàng)建了一個(gè)存儲引擎為InnoDB的表user_innodb
,并設(shè)置id
為主鍵,另外為name
和phone
創(chuàng)建了聯(lián)合索引,最后向表中隨機(jī)初始化了500W+條數(shù)據(jù)。
InnoDB會自動(dòng)為主鍵id
創(chuàng)建一棵名為主鍵索引(又叫做聚簇索引)的B+樹,這個(gè)B+樹的最重要的特點(diǎn)就是葉子節(jié)點(diǎn)包含了完整的用戶記錄,大概長這個(gè)樣子。
如果我們執(zhí)行這個(gè)語句
SELECT * FROM user_innodb WHERE name = '蟬沐風(fēng)';
使用EXPLAIN
查看一下語句的執(zhí)行計(jì)劃:
發(fā)現(xiàn)這個(gè)SQL語句會使用到IDX_NAME_PHONE
索引,這是一個(gè)二級索引。二級索引的葉子節(jié)點(diǎn)長這個(gè)樣子:
InnoDB存儲引擎會根據(jù)搜索條件在該二級索引的葉子節(jié)點(diǎn)中找到name
為蟬沐風(fēng)
的記錄,但是二級索引中只記錄了name
、phone
和主鍵id
字段(誰讓我們用的是SELECT *
呢),因此InnoDB需要拿著主鍵id
去主鍵索引中查找這一條完整的記錄,這個(gè)過程叫做回表。
想一下,如果二級索引的葉子節(jié)點(diǎn)上有我們想要的所有數(shù)據(jù),是不是就不需要回表了呢?是的,這就是覆蓋索引。
舉個(gè)例子,我們恰好只想搜索name
、phone
以及主鍵字段。
SELECT id, name, phone FROM user_innodb WHERE name = "蟬沐風(fēng)";
使用EXPLAIN
查看一下語句的執(zhí)行計(jì)劃:
可以看到Extra一列顯示Using index
,表示我們的查詢列表以及搜索條件中只包含屬于某個(gè)索引的列,也就是使用了覆蓋索引,能夠直接摒棄回表操作,大幅度提高查詢效率。
我們創(chuàng)建兩張表t1
,t2
進(jìn)行連接操作來說明接下來的問題,并向t1
表中插入了100條數(shù)據(jù),向t2
中插入了1000條數(shù)據(jù)。
CREATE TABLE `t1` ( `id` int NOT NULL, `m` int DEFAULT NULL, `n` int DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT; CREATE TABLE `t2` ( `id` int NOT NULL, `m` int DEFAULT NULL, `n` int DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT;
如果我們執(zhí)行下面這條語句
SELECT * FROM t1 STRAIGHT_JOIN t2 ON t1.m = t2.m;
這里我使用了STRAIGHT_JOIN強(qiáng)制令
t1
表作為驅(qū)動(dòng)表,t2
表作為被驅(qū)動(dòng)表
對于連接查詢而言,驅(qū)動(dòng)表只會被訪問一遍,而被驅(qū)動(dòng)表卻要被訪問好多遍,具體的訪問次數(shù)取決于驅(qū)動(dòng)表中符合查詢記錄的記錄條數(shù)。由于已經(jīng)強(qiáng)制確定了驅(qū)動(dòng)表和被驅(qū)動(dòng)表,下面我們說一下兩表連接的本質(zhì):
t1
作為驅(qū)動(dòng)表,針對驅(qū)動(dòng)表的過濾條件,執(zhí)行對t1
表的查詢。因?yàn)闆]有過濾條件,也就是獲取t1
表的所有數(shù)據(jù);
對上一步中獲取到的結(jié)果集中的每一條記錄,都分別到被驅(qū)動(dòng)表中,根據(jù)連接過濾條件查找匹配記錄
用偽代碼表示的話整個(gè)過程是這樣的:
// t1Res是針對驅(qū)動(dòng)表t1過濾之后的結(jié)果集 for (t1Row : t1Res){ // t2是完整的被驅(qū)動(dòng)表 for(t2Row : t2){ if (滿足join條件 && 滿足t2的過濾條件){ 發(fā)送給客戶端 } } }
這種方法最簡單,但同時(shí)性能也是最差,這種方式叫做嵌套循環(huán)連接
(Nested-LoopJoin,NLJ)。怎么加快連接速度呢?
其中一個(gè)辦法就是創(chuàng)建索引,最好是在被驅(qū)動(dòng)表(t2
)連接條件涉及到的字段上創(chuàng)建索引,畢竟被驅(qū)動(dòng)表需要被查詢好多次,而且對被驅(qū)動(dòng)表的訪問本質(zhì)上就是個(gè)單表查詢而已(因?yàn)?code>t1結(jié)果集定了,每次連接t2
的查詢條件也就定死了)。
既然使用了索引,為了避免重蹈無法使用覆蓋索引的覆轍,我們也應(yīng)該盡量不要直接SELECT *
,而是將真正用到的字段作為查詢列,并為其建立適當(dāng)?shù)乃饕?/p>
但是如果我們不使用索引,MySQL就真的按照嵌套循環(huán)查詢的方式進(jìn)行連接查詢嗎?當(dāng)然不是,畢竟這種嵌套循環(huán)查詢實(shí)在是太慢了!
在MySQL8.0之前,MySQL提供了基于塊的嵌套循環(huán)連接
(Block Nested-Loop Join,BLJ)方法,MySQL8.0又推出了hash join
方法,這兩種方法都是為了解決一個(gè)問題而提出的,那就是盡量減少被驅(qū)動(dòng)表的訪問次數(shù)。
這兩種方法都用到了一個(gè)叫做join buffer
的固定大小的內(nèi)存區(qū)域,其中存儲著若干條驅(qū)動(dòng)表結(jié)果集中的記錄(這兩種方法的區(qū)別就是存儲的形式不同而已),如此一來,把被驅(qū)動(dòng)表的記錄加載到內(nèi)存的時(shí)候,一次性和join buffer
中多條驅(qū)動(dòng)表中的記錄做匹配,因?yàn)槠ヅ涞倪^程都是在內(nèi)存中完成的,所以這樣可以顯著減少被驅(qū)動(dòng)表的I/O代價(jià),大大減少了重復(fù)從磁盤上加載被驅(qū)動(dòng)表的代價(jià)。使用join buffer
的過程如下圖所示:
我們看一下上面的連接查詢的執(zhí)行計(jì)劃,發(fā)現(xiàn)確實(shí)使用到了hash join
(前提是沒有為t2
表的連接查詢字段創(chuàng)建索引,否則就會使用索引,不會使用join buffer
)。
最好的情況是join buffer
足夠大,能容納驅(qū)動(dòng)表結(jié)果集中的所有記錄,這樣只需要訪問一次被驅(qū)動(dòng)表就可以完成連接操作了。我們可以使用join_buffer_size
這個(gè)系統(tǒng)變量進(jìn)行配置,默認(rèn)大小為256KB
。如果還裝不下,就得分批把驅(qū)動(dòng)表的結(jié)果集放到join buffer
中了,在內(nèi)存中對比完成之后,清空join buffer
再裝入下一批結(jié)果集,直到連接完成為止。
重點(diǎn)來了!并不是驅(qū)動(dòng)表記錄的所有列都會被放到join buffer
中,只有查詢列表中的列和過濾條件中的列才會被放到join buffer
中,所以再次提醒我們,最好不要把*
作為查詢列表,只需要把我們關(guān)心的列放到查詢列表就好了,這樣還可以在join buffer
中放置更多的記錄,減少分批的次數(shù),也就自然減少了對被驅(qū)動(dòng)表的訪問次數(shù)
“MySQL中不建議使用SELECT *的原因是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。