跳轉到

MYSQL COUNT(*) 和 COUNT(col_name) 的差異問題

此問題我的工作夥伴在使用COUNT(*)後,解決了速度緩慢問題,另外他也有點無法理解為什麼這麼做速度變快...

我的想法和實驗的結果跟他有些許落差,故在這裡做個筆記,若日後回顧需再額外驗證正確性...

因為這件事情,LINE群都快炸了...感謝他救了所有人

問題發生

今天晚上收到來自某個客戶的親切問候,抱怨某個有分頁列表的UI跑得十分緩慢(7秒)...

問題分析

  • 這個UI畫面總共有2個SQL敘述所組成
  • 取得列表資料,沒有Where條件,有limit 和 offset 敘述,有order by 敘述(依日期降冪),沒有group by敘述
  • 取得總筆數數值,沒有Where條件,沒有limit 和 offset 敘述,沒有order by 敘述,沒有group by敘述僅單純使用SELECT COUNT(Product_Name)
  • 資料庫引擎 InnoDB
  • 資料庫總筆數約320萬筆左右
  • 目前已知影響速度的問題是來自計算總筆數的SQL敘述(4~5秒)
  • 我的工作夥伴使用COUNT(*)敘述後,速度降為1秒~1.3左右秒

原有想法

從我接觸的MySQL的時候,在我的認知裡,COUNT(column_name)一定會比COUNT(*)敘述快,主要的想法是COUNT(*)敘述 會吐出所有列的資料,而COUNT(column_name)則是吐出指定列的資料,所以就以邏輯來說COUNT(column_name)應該會是比COUNT(*)快。

想法思考

基於這個想法,我嘗試將SELECT COUNT(Product_Name)改成其他欄位的名稱,經測試,意外發現除了幾個有經過索引的欄位速度快以外,其他沒有經過索引的欄位,速度就跟原本的速度依樣都是約(4~5秒),可是這樣為何COUNT(*)速度會變快呢?

這是猜測(未經驗證)的想法

按照先前的測試,在使用COUNT函數有索引的欄位會比沒有索引的快,這個是能理解並且有邏輯的,假設欄位有使用索引,便會使用索引來計算行數,如果沒有索引,將不得不讀取數據頁以檢查列的NULL值然後計算行數,那COUNT(*)速度為什麼有時候會比COUNT(column_name)快,COUNT(*)操作被優化以快速計算表中的總行數,而不需要逐一檢查每個列的值。這種優化使得COUNT(*)在許多情況下,特別是使用InnoDB引擎時,比COUNT(column_name)更加高效,因為後者需要檢查指定列的每一個非NULL值。然而,如果COUNT(column_name)操作指定的列被索引,那麼性能可能得到提升,但通常不會超過COUNT(*)的性能,因為MySQL優化器會選擇最有效的計算方式。

COUNT函數速度慢的問題,似乎僅發生在InnoDB引擎上,MyISAM引擎則不會有這個問題,MyISAM引擎把一個表的總行數存在了硬碟上,因此執行count(*)的時候會直接返回這個內容

後續

這是另外發生事情,雖然我的工作夥伴解決掉速度緩慢的問題,但是在部屬前的單元測試未通過,我檢查了一下主要是忘記了一些COUNT的特性 * COUNT(*)會計算表中所有的算表中的所有行,不管任何列中的NULL值,全部都會計算 * COUNT(col_name) 計算指定列中非NULL值的數量。它專注於一個特定的列,並忽略指定列中有NULL值的行。

例子

SELECT * from `sales`;

+-----------+---------------------+--------------+
| ID        | product             | revenue      |
+-----------+---------------------+--------------+
| 1         | Widget A            | 150          |
| 2         | Widget B            | NULL         |
| 3         | Widget A            | 200          |
| 4         | Widget C            | 180          |
| 5         | Widget B            | 120          |
| 6         | Widget A            | NULL         |
+-----------+---------------------+--------------+

有一個sales的資料表,記錄了銷售的資料,包括產品名稱(product)和銷售額(revenue),但有些銷售額信息是NULL值。

-- 由於 NULL 值在計算中被忽略,這樣的結果可能不準確
SELECT SUM(revenue) / COUNT(*) FROM `sales`; -- 結果可能不準確
-- (150 + 200 + 180 + 120) / 6 = 650 / 6 = 108.33

-- 為了得到準確的結果,應該只計算非NULL值的數量
SELECT SUM(revenue) / COUNT(revenue) FROM `sales`; -- 得到準確的平均銷售額
-- (150 + 200 + 180 + 120) / 4 = 650 / 4 = 162.5