count(*)為什么很慢?下面本篇文章就來給大家分析一下原因,并聊聊count(*)的執(zhí)行過程,希望對大家有所幫助!
本沒想著寫這篇文章的,因為我覺得這個東西大多數(shù)有經(jīng)驗的開發(fā)遇到過,肯定也了解過相關(guān)的原因,但最近我看到有幾個關(guān)注的技術(shù)公眾號在推送相關(guān)的文章。實在令我吃驚!
先上公眾號文章的結(jié)論:
- count(*) :它會獲取所有行的數(shù)據(jù),不做任何處理,行數(shù)加1。
- count(1):它會獲取所有行的數(shù)據(jù),每行固定值1,也是行數(shù)加1。
- count(id):id代表主鍵,它需要從所有行的數(shù)據(jù)中解析出id字段,其中id肯定都不為NULL,行數(shù)加1。
- count(普通索引列):它需要從所有行的數(shù)據(jù)中解析出普通索引列,然后判斷是否為NULL,如果不是NULL,則行數(shù)+1。
- count(未加索引列):它會全表掃描獲取所有數(shù)據(jù),解析中未加索引列,然后判斷是否為NULL,如果不是NULL,則行數(shù)+1。
結(jié)論:count(*) ≈ count(1) > count(id) > count(普通索引列) > count(未加索引列)
我也不想賣關(guān)子了,以上結(jié)論純屬放屁。根本就是個人yy出來的東西,甚至不愿意去驗證一下,哪怕看一眼執(zhí)行計劃,也得不出這么離譜的結(jié)論。
我不敢相信這是一篇被多個技術(shù)公眾號轉(zhuǎn)載的文章!
以下所有的內(nèi)容均是基于,mysql 5.7 + InnoDB引擎
, 進行的分析。
拓展:
MyISAM 如果沒有查詢條件,只是簡單的統(tǒng)計表中數(shù)據(jù)總數(shù),將會返回的超快,因為service層中獲取到表信息中的總行數(shù)是準確的,而InnoDB只是一個估值。
實例
廢話不多說,先看一個例子。
以下是一張表數(shù)據(jù)量有100w,表中字段相對較短,整體數(shù)據(jù)量不算大。
CREATE TABLE `hospital_statistics_data` ( `pk_id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `id` varchar(36) COLLATE utf8mb4_general_ci NOT NULL COMMENT '外鍵', `hospital_code` varchar(36) COLLATE utf8mb4_general_ci NOT NULL COMMENT '醫(yī)院編碼', `biz_type` tinyint NOT NULL COMMENT '1服務(wù)流程 2管理效果', `item_code` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核項目編碼', `item_name` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核項目名稱', `item_value` varchar(36) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核結(jié)果', `is_deleted` tinyint DEFAULT NULL COMMENT '是否刪除 0否 1是', `gmt_created` datetime DEFAULT NULL COMMENT '創(chuàng)建時間', `gmt_modified` datetime DEFAULT NULL COMMENT 'gmt_modified', `gmt_deleted` datetime(3) DEFAULT '9999-12-31 23:59:59.000' COMMENT '刪除時間', PRIMARY KEY (`pk_id`) ) DEFAULT CHARSET=utf8mb4 COMMENT='醫(yī)院統(tǒng)計數(shù)據(jù)';
此表初始狀態(tài)只有一個聚簇索引
。
以下分不同索引情況,看一下COUNT(*)的執(zhí)行計劃。
1)在只有一個聚簇索引的情況下看一下執(zhí)行計劃。
EXPLAIN select COUNT(*) from hospital_statistics_data;
結(jié)果:
關(guān)于執(zhí)行計劃的各個參數(shù)的含義,不在本文的討論范圍內(nèi),可自行了解。
這里只關(guān)注以下幾個屬性。
-
type: 這里顯示index,說明使用了索引。
-
key:PRIMARY使用了主鍵索引。
-
key_len: 索引長度8字節(jié)。
這里有很關(guān)鍵的一點:count(*)也會走索引
,在當前情況下使用了聚簇索引。
好,再往下看。
2)存在一個非聚簇索引(二級索引)
給表添加一個hospital_code索引。
alter table hospital_statistics_data add index idx_hospital_code(hospital_code)
此時表中存在2個索引,主鍵
和 hospital_code
。
同樣的,再執(zhí)行一下:
EXPLAIN select COUNT(*) from hospital_statistics_data;
結(jié)果:
同樣的,看一下 type、key和key_len三個字段。
是不是覺得有點“神奇”。
為何索引變成剛添加的idx_hospital_code
了。
先別急著想結(jié)論,再看下面一種情況。
3)存在兩個非聚簇索引(二級索引)
在上面的基礎(chǔ)上,再添加一個二級索引。
alter table hospital_statistics_data add index idx_biz_type(biz_type)
此時表中存在3個索引,主鍵 、hospital_code 和 biz_type。
同樣的,執(zhí)行一下:
EXPLAIN select COUNT(*) from hospital_statistics_data;
結(jié)果:
是不是更困惑了,索引又..又又…變了.
變成新添加的idx_biz_type。
先不說為何會產(chǎn)生以上的變化,繼續(xù)往下分析。
在以上3個索引的基礎(chǔ)上,分別看一下,count(1)
、count(id)
、count(index)
、count(無索引)
這4種情況,與count(*)的執(zhí)行計劃有何區(qū)別。
-
count(1)
-
count(id) 對于樣例表來說是,主鍵是pk_id
-
count(index)
這里選取biz_type索引字段。
-
count(無索引)
小結(jié):
-
count(index) 會使用當前index指定的索引。
-
count(無索引) 是全表掃描,未走索引。
-
count(1) , count(*), count(id) 一樣都會選擇idx_biz_type索引
看到這,你還覺得那些千篇一律的公眾號文章的結(jié)論正確嗎?
必要知識點
-
mysql 分為
service層
和引擎層
。 -
所有的sql在執(zhí)行前會經(jīng)過service層的優(yōu)化,優(yōu)化分為很多類型,簡單的來說可分為
成本
和規(guī)則
。 -
執(zhí)行計劃所反映的是service層經(jīng)過sql優(yōu)化后,可能的執(zhí)行過程。并非絕對(免得有些人說我只看執(zhí)行計劃過于片面)。
絕大多數(shù)情況執(zhí)行計劃是可信的
。 -
索引類型分為
聚簇索引
和非聚簇索引(二級索引)
。其中數(shù)據(jù)都是掛在聚簇索引上的,非聚簇索引上只是記錄的主鍵id。 -
拋開數(shù)據(jù)內(nèi)存,只談數(shù)據(jù)量,都是扯淡。什么500w就是極限,什么2個表以上的join都需要優(yōu)化了,什么is null不會走索引等,純純的放屁。
-
相信一點,編寫mysql代碼的人比,看此文章的大部分人都要優(yōu)秀。他們會盡可能在執(zhí)行前,對我這樣菜逼寫的亂七八糟的sql進行優(yōu)化。
原因分析
其實原因非常非常簡單,上面也說了,service層會基于成本進行優(yōu)化。
并且,正常情況下,非聚簇索引
所占有的內(nèi)存要遠遠小于聚簇索引
。所以問題來了,如果你是mysql的開發(fā)人員,你在執(zhí)行count(*)查詢的時候會使用那個索引?
我相信正常人都會使用非聚簇索引
。
那如果存在2個甚至多個非聚簇索引又該如何選擇呢?
那肯定選擇最短的,占用內(nèi)存最小的一個呀,在回頭看看上面的實例,還迷惑嗎。
同樣都是非聚簇索引。idx_hospital_code
的len
是146
字節(jié);而idx_biz_type
的len
只有1
。那還要選嗎?
那為何count(*)走了索引,卻還是很慢呢?
這里要明確一點,索引只是提升效率的一種方式,但不能完全的解決效率問題。count(*)有一個明顯的缺陷,就是它要計算總數(shù),那就意味著要遍歷所有符合條件的數(shù)據(jù),相當于一個計數(shù)器,在數(shù)據(jù)量足夠大的情況下,即使使用非聚簇索引也無法優(yōu)化太多。
官方文檔:
InnoDBhandlesSELECT COUNT(*)andSELECT COUNT(1)operations in the same way. There is no performance difference.
簡單的來說就是,InnoDB下 count(*) 等價于 count(1)
既然會自動走索引,那么上面那個所謂的速度排序還覺得對嗎? count(*)的性能跟數(shù)據(jù)量有很大的關(guān)系,此外最好有一個字段長度較短的二級索引。
拓展:
另外,多說一下,關(guān)于網(wǎng)上說的那些索引失效的情況,大多都是片面的,我這里只說一點。量變才能引起質(zhì)變,索引的失效取決于你圈定數(shù)據(jù)的范圍,若你圈定的數(shù)據(jù)量占整體數(shù)據(jù)量的比例過高,則會放棄使用索引,反之則會優(yōu)先使用索引。但是此規(guī)則并不是完美的,有時候可能與你預(yù)期的不同,也可以通過一些技巧強制使用索引,但這種方式少用。
舉個栗子:
通過上面這個表hospital_statistics_data
,我進行了如下查詢:
select * from hospital_statistics_data where hospital_code is not null;
此時這個sql會使用到hospital_code
的索引嗎?
這里也不賣關(guān)子了,若hospital_code只有很少一部分數(shù)據(jù)是null
值,那么將不會走索引,反之則走索引。
原因就2個字:回表
。
好比去買砂糖橘,如果你只買幾斤,那么你隨便挑筐里面好的就行。但是如果你要買一筐,我相信老板不會讓你在里面一個個挑,而是一次給你一整筐,當然大家都不傻,都知道筐里里面肯定有那么幾個壞果子。但是這樣效率最高,而且對老板來說損失更小。
執(zhí)行過程
摘抄自《從根上理解mysql》。我強烈推薦沒有系統(tǒng)學(xué)過mysql的,看看這本書。
1.首先在server層維護一個count變量
2.server層向InnoDB引擎要第一條記錄
3.InnoDB找到第一條二級索引記錄,并返回給server層(注意:由于此時只是統(tǒng)計記錄數(shù)量,所以并不需要回表)
4.由于COUNT函數(shù)的參數(shù)是*,MySQL會將*當作常數(shù)0處理。由于0并不是NULL,server層給count變量加1。
5.server層向InnoDB要下一條記錄。
6.InnoDB通過二級索引記錄的next_record屬性找到下一條二級索引記錄,并返回給server層。
7.server層繼續(xù)給count變量加1。
8.重復(fù)上述過程,直到InnoDB向server層返回沒記錄可查的消息。
9.server層將最終的count變量的值發(fā)送到客戶端。
總結(jié)
寫完后還是心中挺郁悶的,現(xiàn)在能從公眾號獲取到的好文章越來越少了,現(xiàn)在已經(jīng)是知識付費的時代了。
挺懷念剛工作的時候,那時候每天上午都花點時間看看公眾號文章,現(xiàn)在全都是廣告。哎!
不過也正常,誰也不能一直為愛發(fā)電。
學(xué)習(xí)還是建議多看看書籍,一般能成書的都不會太差?,F(xiàn)在晚上能搜到的都是千篇一律的文章,對錯不知。網(wǎng)上
【