1. 程式人生 > >新特新解讀 | MySQL 8.0 對 count(*)的優化

新特新解讀 | MySQL 8.0 對 count(*)的優化

原創: 楊濤濤

摘要:MySQL 8.0 取消了 sql_calc_found_rows 的語法,以後求表 count(*) 的寫法演進為直接 select。

我們知道,MySQL 一直依賴對 count(*) 的執行很頭疼。很早的時候,MyISAM 引擎自帶計數器,可以秒回;不過 InnoDB 就需要實時計算,所以很頭疼。以前有多方法可以變相解決此類問題,比如:

 

1. 模擬 MyISAM 的計數器

比如表 ytt1,要獲得總數,我們建立兩個觸發器分別對 insert/delete 來做記錄到表 ytt1_count,這樣只需要查詢表 ytt1_count 就能拿到總數。ytt1_count 這張表足夠小,可以長期固化到記憶體裡。不過缺點就是有多餘的觸發器針對 ytt1 的每行操作,寫效能降低。這裡需要權衡。

 

2. 用 MySQL 自帶的 sql_calc_found_rows 特性來隱式計算

依然是表 ytt1,不過每次查詢的時候用 sql_calc_found_rows 和 found_rows() 來獲取總數,比如:

mysql> select sql_calc_found_rows * from ytt1 where 1 order by id desc limit 1;
+------+------+
| id | r1 |
+------+------+
| 3072 | 73 |
+------+------+
1 row in set, 1 warning (0.00 sec)


mysql> show warnings;
+---------+------+-------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+-------------------------------------------------------------------------------------------------------------------------+
| Warning | 1287 | SQL_CALC_FOUND_ROWS is deprecated and will be removed in a future release. Consider using two separate queries instead. |
+---------+------+-------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select found_rows() as 'count(*)';
+----------+
| count(*) |
+----------+
| 3072 |
+----------+
1 row in set, 1 warning (0.00 sec)

這樣的好處是寫法簡單,用的是 MySQL 自己的語法。缺點也有,大概有兩點:

1. sql_calc_found_rows 是全表掃。

2. found_rows() 函式是語句級別的儲存,有很大的不確定性,所以在 MySQL 主從架構裡,語句級別的行級格式下,從機資料可能會不準確。不過行記錄格式改為 ROW 就 OK。所以最大的缺點還是第一點。

從 warnings 資訊看,這種是 MySQL 8.0 之後要淘汰的語法。

 

3. 從資料字典裡面拿出來粗略的值

mysql> select table_rows from information_schema.tables where table_name = 'ytt1';
+------------+
| TABLE_ROWS |
+------------+
| 3072 |
+------------+
1 row in set (0.12 sec)

那這樣的適合新聞展示,比如行數非常多,每頁顯示幾行,一般後面的很多大家也都不怎麼去看。缺點是資料不是精確值。

 

4. 根據表結構特性特殊的取值

這裡假設表 ytt1 的主鍵是連續的,並且沒有間隙,那麼可以直接

mysql> select max(id) as cnt from ytt1;
+------+
| cnt |
+------+
| 3072 |
+------+
1 row in set (0.00 sec)

不過這種對錶的資料要求比較高。

 

5. 標準推薦取法(MySQL 8.0.17 建議)

MySQL 8.0 建議用常規的寫法來實現。

mysql> select * from ytt1 where 1 limit 1;
+----+------+
| id | r1 |
+----+------+
| 87 | 1 |
+----+------+
1 row in set (0.00 sec)

mysql> select count(*) from ytt1;
+----------+
| count(*) |
+----------+
| 3072 |
+----------+
1 row in set (0.01 sec)

第五種寫法是 MySQL 8.0.17 推薦的,也就是說以後大部分場景直接實時計算就 OK 了。

MySQL 8.0.17 以及在未來的版本都取消了sql_calc_found_rows 特性,可以檢視第二種方法裡的 warnings 資訊。相比 MySQL 5.7,8.0 對 count(*) 做了優化,沒有必要在用第二種寫法了。我們來看看 8.0 比 5.7 在此類查詢是否真的有優化?

MySQL 5.7

mysql> select version();
+------------+
| version() |
+------------+
| 5.7.27-log |
+------------+
1 row in set (0.00 sec)

mysql> explain format=json select count(*) from ytt1\G
*************************** 1. row ***************************
EXPLAIN: {
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "622.40"
},
"table": {
"table_name": "ytt1",
"access_type": "index",
"key": "PRIMARY",
"used_key_parts": [
"id"
],
"key_length": "4",
"rows_examined_per_scan": 3072,
"rows_produced_per_join": 3072,
"filtered": "100.00",
"using_index": true,
"cost_info": {
"read_cost": "8.00",
"eval_cost": "614.40",
"prefix_cost": "622.40",
"data_read_per_join": "48K"
}
}
}
}
1 row in set, 1 warning (0.00 sec)

MySQL 8.0 下執行同樣的查詢

mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.17 |
+-----------+
1 row in set (0.00 sec)

mysql> explain format=json select count(*) from ytt1\G
*************************** 1. row ***************************
EXPLAIN: {
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "309.95"
},
"table": {
"table_name": "ytt1",
"access_type": "index",
"key": "PRIMARY",
"used_key_parts": [
"id"
],
"key_length": "4",
"rows_examined_per_scan": 3072,
"rows_produced_per_join": 3072,
"filtered": "100.00",
"using_index": true,
"cost_info": {
"read_cost": "2.75",
"eval_cost": "307.20",
"prefix_cost": "309.95",
"data_read_per_join": "48K"
}
}
}
}
1 row in set, 1 warning (0.00 sec)

從以上結果看出,第二個 SQL 效能(cost_info)相對第一