mysql的count()函數如何選擇索引,千萬級表的count()查詢優化實例

一、前言

      博主今天在對一個千萬級表進行count(*)查詢的時候,發現速度有點慢,達到了9s,這對於程序來說是不可承受的,因此萌生了優化count(*)查詢的想法,這裏記錄一下。

1、網上的主要兩種說法

1count(*) 函數會選擇索引長度最短的字段
	ps:索引長度指的是執行計劃explain裏面的key_len長度。
	
(2count(*)函數會選擇索引基數最小的字段
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_timeint類型的時間戳,索引長度爲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(*)的時候會自動找長度最短的索引使用,可以有效加速查詢速度。

      上面2000Wuser表就是最好的例子,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(*)的優化,獲取千萬級數據表的總行數

mysql count(*) 會選哪個索引?

end,好久沒寫了,奧利給!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章