文章目錄
一、前言
博主今天在對一個千萬級表進行count(*)
查詢的時候,發現速度有點慢,達到了9s
,這對於程序來說是不可承受的,因此萌生了優化count(*)
查詢的想法,這裏記錄一下。
1、網上的主要兩種說法
(1)count(*) 函數會選擇索引長度最短的字段
ps:索引長度指的是執行計劃explain裏面的key_len長度。
(2)count(*)函數會選擇索引基數最小的字段
PS:索引基數其實就是說該字段在表中對應的不重複的記錄值。
查詢方式:
select count(distinct xxx) from xxx;
咱們這裏主要就來討論測試下這兩種說法,並且找到適合千萬級數據庫查詢的優化方式。
2、不貼出mysql版本的測試都是耍流氓~
mysql> select count(*) from test;
+----------+
| count(*) |
+----------+
| 11793920 |
+----------+
1 row in set (9.10 sec)
3、mysql的count(*)和count(1)
有很多文章都在說count(*)
和count(1)
的區別,還有文章說count(1)
比count(*)
速度快之類的,這裏鄭重說一下,對於目前的的mysql(>=5.6)
對於count(1)
和count(*)
,MySQL
的優化是完全一樣的,根本不存在誰更快,而且count(*)
是SQL92
定義的標準統計行數的語法,使用count(*)準沒錯。(PS:更早的版本可能會有一些區別)
二、測試索引長度和索引基數對count(*)查詢的影響
1、總數據量1100W+ 表的速度
mysql> select count(*) from test;
+----------+
| count(*) |
+----------+
| 11793920 |
+----------+
1 row in set (9.10 sec)
查看count(*)
速度並不是很理想
2、默認使用的索引
mysql> explain select count(*) from test;
+------+-------------+-------------+-------+---------------+-------------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+------+-------------+-------------+-------+---------------+-------------+---------+------+----------+-------------+
| 1 | SIMPLE | test | index | NULL | idx_install | 4 | NULL | 10795387 | Using index |
+------+-------------+-------------+-------+---------------+-------------+---------+------+----------+-------------+
這裏可以看到選用的索引長度很短,是個int
類型的時間戳。那麼不同索引字段的基數如何呢?
3、查看該表所有索引信息
mysql> show index from test;
+-------------+------------+---------------------------+--------------+-----------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------------+------------+---------------------------+--------------+-----------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
|test | 0 | PRIMARY | 1 | id | A | 10795387 | NULL | NULL | | BTREE | | |
| test | 1 | idx_install_tracker | 1 | install_tracker | A | 981398 | NULL | NULL | | BTREE | | |
|test | 1 | idx_user_id | 1 | user_id | A | 10795387 | NULL | NULL | | BTREE | | |
| test | 1 | idx_country | 1 | country | A | 21334 | NULL | NULL | | BTREE | | |
| test | 1 | idx_install | 1 | installed_at | A | 3598462 | NULL | NULL | | BTREE | | |
+-------------+------------+---------------------------+--------------+-----------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
可以看到,默認使用的idx_install
索引基數並不是最小的,但是爲何還是選擇它了呢?明明country
字段的基數是最小的纔對。
4、強制選擇基數最小的country字段
mysql> explain select count(*) from test force index (idx_country);
+------+-------------+-------------+-------+---------------+-------------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+------+-------------+-------------+-------+---------------+-------------+---------+------+----------+-------------+
| 1 | SIMPLE | test | index | NULL | idx_country | 62 | NULL | 10795387 | Using index |
+------+-------------+-------------+-------+---------------+-------------+---------+------+----------+-------------+
可以看到,雖然掃描行數差不多,但是這個索引的長度太長了,,達到了62
字節。那麼速度呢,會加快嗎
5、強制使用基數最小索引的聚合查詢速度
mysql> select count(*) from test force index (idx_country);
+----------+
| count(*) |
+----------+
| 11793920 |
+----------+
1 row in set (34.33 sec)
可以看到,比使用默認的idx_install
索引速度要慢很多。這就是爲什麼很多文章都說目前的count(*)
是經過優化的原因,mysql
對於count(*)
的優化確實是下過功夫的,默認選擇的就是最優解。
按理說此時應該新建個索引長度最小的標識字段,來測試下實際的count(*)
優化速度,奈何沒有數據庫權限,還需要找dba
,所以暫時就這麼着了,只要懂原理了就行。不過雖然這個表沒有權限,那麼我們可以看看其他的表是否有類似的優化。
三、兩千萬的大表count(*) 優化
這裏重新選擇了user
表,主要對比的就是,在有int
類型的索引時,count(*)
選擇的是哪個索引,速度是否有加快?以及對於2000W
的大表來說,如何優化count(*)
查詢。
1、user表的數量級以及默認count(*)的速度
mysql> select count(*) from user;
+----------+
| count(*) |
+----------+
| 20190648 |
+----------+
1 row in set (2.33 sec)
這裏可以看到,2000W
的大表,速度竟然比上面1000W
的錶快很對!經過博主的比對,原來這個表不經意間已經有了優化的字段,怪不得這麼快。不過下面的測試還是需要的。
2、查看錶索引
mysql> show index from user;
+-------+------------+--------------------+--------------+-----------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+--------------------+--------------+-----------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| user | 0 | PRIMARY | 1 | uin | A | 14607030 | NULL | NULL | | BTREE | | |
| user | 1 | last_login_ip | 1 | last_login_ip | A | 14607030 | NULL | NULL | | BTREE | | |
| user | 1 | first_login_time | 1 | reg_time | A | 14607030 | NULL | NULL | | BTREE | | |
| user | 1 | last_login_time | 1 | last_login_time | A | 14607030 | NULL | NULL | | BTREE | | |
| user | 1 | email | 1 | email | A | 2 | NULL | NULL | | BTREE | |
| user | 1 | email_state | 1 | email_state | A | 2 | NULL | NULL | | BTREE | | |
+-------+------------+--------------------+--------------+-----------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
可以看到,first_login_time
是int
類型的時間戳,索引長度爲4
,但是我們用到的索引是它嗎?
3、實際使用的索引
mysql> explain select count(*) from user;
+----+-------------+-------+-------+---------------+-------------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+-------------+---------+------+----------+-------------+
| 1 | SIMPLE | user | index | NULL | email_state | 1 | NULL | 14607063 | Using index |
+----+-------------+-------+-------+---------------+-------------+---------+------+----------+-------------+
email_state
這個索引,索引長度僅爲1
,符合我們上面說的優化手段,即基數很小。
四、索引基數一致的情況下,會選擇使用哪個索引呢
這裏繼續引用user
表作爲比對錶。
1、索引基數一致
mysql> select count(distinct email) from user;
+-----------------------+
| count(distinct email) |
+-----------------------+
| 1 |
+-----------------------+
mysql> select count(distinct email_state) from user;
+-----------------------------+
| count(distinct email_state) |
+-----------------------------+
| 1 |
+-----------------------------+
可以看到在索引列表裏面,有個email
字段,基數也很小,那麼爲什麼不選擇它呢?
2、查看對應的索引長度
mysql> explain select count(*) from user force index (email);
+----+-------------+-------+-------+---------------+-------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+-------+---------+------+----------+-------------+
| 1 | SIMPLE | user | index | NULL | email | 194 | NULL | 14608467 | Using index |
+----+-------------+-------+-------+---------------+-------+---------+------+----------+-------------+
發現email
索引長度194
,是遠遠大於email_state
的索引長度1的。
3、實際的優化手段
從上面對比,我們可以發現,mysql
對於count(*)
部分的優化是偏向於選擇索引長度短,字段重複值比較多的字段。
而索引字段最短也得是1
吧,對應目前數據庫常見的類型就是tinyint
。字段重複值最多的情況就是某個字段的值完全一樣,不過這種字段一般不會出現在正式的表裏面,不過我們經常用到的標識字段倒是可以勝任。
tinyint(1) 和 tinyint(3) 沒什麼區別,佔用字節都是一位,存儲範圍都是一樣的。
tinyint一個字節 smallint 兩個字節 MEDIUMINT三個字節 int 4個字節 BIGINT 8個字節。
綜上所述:
當對於一個千萬級的大表進行count(*)
的時候,速度耗時是比較慢的,此時我們可以考慮到,給大表的一些標識字段,比如is_del tinyint(1)
這種加索引,因爲tinyint
類型佔字節最少,因此count(*)
的時候會自動找長度最短的索引使用,可以有效加速查詢速度。
上面2000W
的user
表就是最好的例子,email_state
確實是個標識字段,類似於bool
值的存在,完美符合優化規則。
五、爲什麼count(*)不用主鍵,或者count(主鍵)速度並不快
這裏直接說一下結論,還有一些例子:
1、部分解釋
(1) 主鍵雖然一般是int
類型,索引長度比較短,但是主鍵的索引是比較大的,存儲了整行數據,而輔助索引只存儲了主鍵,使用輔助索引更快一些
(2) 從基數上來講,主鍵的基數當然是很大的,使用主鍵確實不符合優化原則
2、有人可能會覺得,如果輔助索引長度和主鍵索引長度一樣,那麼count(*) 會不會使用PRIMARY索引呢?
這裏選用了另一個比較小的服務器表,直接對比兩個執行計劃,大家注意選擇的key
以及長度:
mysql> explain select count(*) from server_list;
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | server_list | index | NULL | game_id | 4 | NULL | 157 | Using index |
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)
mysql> explain select count(*) from server_list force index (PRIMARY);
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | server_list | index | NULL | PRIMARY | 4 | NULL | 157 | Using index |
+----+-------------+-------------+-------+---------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)
結果很明顯了,索引長度一致的情況下,優先使用輔助索引的。
六、總結
1、結論
(1)索引長度最小的字段會優先被count(*)選擇,一般是int類型的
(2)如果索引長度一致,那麼選擇基數最小的(這部分是猜測,但是綜合各種文章,感覺還是有可信度的)
(3)如果索引基數一致,選擇索引長度最小的
(4)大表的count()查詢優化手段就是新增tinyint類型的標識字段,速度可以得到有效提升
2、其他
(1)這些sql都是建立在沒有where條件的基礎上。
如果有where
條件,那麼就會使用where
條件中的索引,這樣的話,count
查詢的速度是不能保證的。目前沒什麼好辦法,除非你的where
條件用到的索引剛好符合咱們上面說的,基數小,索引長度小。
(2)如果只是要手動統計一個大表有多少數據,可以採用另一種方式:
SELECT TABLE_ROWS FROM `information_schema`.tables WHERE table_name='xxx'
缺點: 不夠實時,這個類似於定時統計表條數寫入的關係,如果對數據要求不是很精準的話,可以用這種方式
參考鏈接:
mysql的count(*)的優化,獲取千萬級數據表的總行數
end,好久沒寫了,奧利給!!!