新特新解讀 | 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)相對第一