MySQL實戰45講 14

来源:https://www.cnblogs.com/ydssx7/archive/2022/07/25/16517122.html
-Advertisement-
Play Games

14 | count(*)這麼慢,我該怎麼辦? 在開發系統的時候,你可能經常需要計算一個表的行數,比如一個交易系統的所有變更記錄總數。 隨著系統中記錄數越來越多,select count(*) from t 語句執行得也會越來越慢 count(*) 的實現方式 在不同的 MySQL 引擎中,coun ...


14 | count(*)這麼慢,我該怎麼辦?

在開發系統的時候,你可能經常需要計算一個表的行數,比如一個交易系統的所有變更記錄總數。

隨著系統中記錄數越來越多,select count(*) from t 語句執行得也會越來越慢

count(*) 的實現方式

在不同的 MySQL 引擎中,count(*) 有不同的實現方式。

  • MyISAM 引擎把一個表的總行數存在了磁碟上,因此執行 count(*) 的時候會直接返回這個數,效率很高;
  • 而 InnoDB 引擎執行 count(*) 的時候,需要把數據一行一行地從引擎裡面讀出來,然後累積計數。

這裡討論的是沒有過濾條件的 count(*),如果加了 where 條件的話,MyISAM 表也是不能返回得這麼快的。

Q:為什麼 InnoDB 不跟 MyISAM 一樣,也把數字存起來呢?

A:因為即使是在同一個時刻的多個查詢,由於多版本併發控制(MVCC)的原因,InnoDB 表“應該返回多少行”也是不確定的。

舉個例子:

假設表 t 中現在有 10000 條記錄,我們設計了三個用戶並行的會話。

  • 會話 A 先啟動事務並查詢一次表的總行數;
  • 會話 B 啟動事務,插入一行後記錄後,查詢表的總行數;
  • 會話 C 先啟動一個單獨的語句,插入一行記錄後,查詢表的總行數。

在最後一個時刻,三個會話 A、B、C 會同時查詢表 t 的總行數,但拿到的結果卻不同。

這和 InnoDB 的事務設計有關係,可重覆讀是它預設的隔離級別,在代碼上就是通過多版本併發控制,也就是 MVCC 來實現的。每一行記錄都要判斷自己是否對這個會話可見,因此對於 count(*) 請求來說,InnoDB 只好把數據一行一行地讀出依次判斷,可見的行才能夠用於計算“基於這個查詢”的表的總行數

在執行 count(*) 操作時的優化

InnoDB 是索引組織表,主鍵索引樹的葉子節點是數據,而普通索引樹的葉子節點是主鍵值。所以,普通索引樹比主鍵索引樹小很多。對於 count(*) 這樣的操作,遍歷哪個索引樹得到的結果邏輯上都是一樣的。因此,MySQL 優化器會找到最小的那棵樹來遍歷

在保證邏輯正確的前提下,儘量減少掃描的數據量,是資料庫系統設計的通用法則之一。

Q:TABLE_ROWS 能代替 count(*) 嗎?

A:show table status 命令輸出結果是 TABLE_ROWS 。但是實際上,TABLE_ROWS 是從採樣估算得來的,因此它很不准。所以,show table status 命令顯示的行數也不能直接使用。

小結

  • MyISAM 表雖然 count(*) 很快,但是不支持事務;
  • show table status 命令雖然返回很快,但是不准確;
  • InnoDB 表直接 count(*) 會遍歷全表,雖然結果準確,但會導致性能問題。

如果你現在有一個頁面經常要顯示交易系統的操作記錄總數,只能自己計數

自己計數的方法以及優缺點

用緩存系統保存計數

可以用一個 Redis 服務來保存這個表的總行數。這個表每被插入一行 Redis 計數就加 1,每被刪除一行 Redis 計數就減 1。

這種方式下,讀和更新操作都很快

存在問題:Redis 的數據不能永久地留在記憶體里,緩存系統可能會丟失更新

解決方法:找一個地方把這個值定期地持久化存儲起來。

存在問題:即使持久話存儲,仍然可能丟失更新。試想如果剛剛在數據表中插入了一行,Redis 中保存的值也加了 1,然後 Redis 異常重啟了,重啟後你要從存儲 redis 數據的地方把這個值讀回來,而剛剛加 1 的這個計數操作卻丟失了。

解決方法:Redis 異常重啟以後,到資料庫裡面單獨執行一次 count(*) 獲取真實的行數,再把這個值寫回到 Redis 里就可以了。異常重啟畢竟不是經常出現的情況,這一次全表掃描的成本,可以接受。

存在問題:即使 Redis 正常工作,這個值還是邏輯上不精確的

假設存在一個頁面,要顯示操作記錄的總數,同時還要顯示最近操作的 100 條記錄。那麼,這個頁面的邏輯就需要先到 Redis 裡面取出計數,再到數據表裡面取數據記錄。

可能存在兩種情況:

  1. 一種是,查到的 100 行結果裡面有最新插入記錄,而 Redis 的計數里還沒加 1;
  2. 另一種是,查到的 100 行結果里沒有最新插入的記錄,而 Redis 的計數里已經加了 1。

情況1:

會話 A 是一個插入交易記錄的邏輯,往數據表裡插入一行 R,然後 Redis 計數加 1;會話 B 就是查詢頁面顯示時需要的數據。

在 T3 時刻會話 B 來查詢的時候,會顯示出新插入的 R 這個記錄,但是 Redis 的計數還沒加 1。這時候,就會出現數據不一致。

情況2:

會話 B 在 T3 時刻查詢的時候,Redis 計數加了 1 了,但還查不到新插入的 R 這一行,也是數據不一致的情況。

在併發系統裡面,我們是無法精確控制不同線程的執行時刻的,因為存在圖中的這種操作序列,所以,我們說即使 Redis 正常工作,這個計數值還是邏輯上不精確的

兩個不同的存儲構成的系統,不支持分散式事務,無法拿到精確一致的視圖。

在資料庫保存計數(優)

這個計數直接放到資料庫里單獨的一張計數表 C 中

解決了崩潰丟失的問題,InnoDB 是支持崩潰恢復不丟數據的。

解決計數不精確的問題,由於事務,存在不可見,邏輯上就是一致的。

雖然會話 B 的讀操作仍然是在 T3 執行的,但是因為這時候更新事務還沒有提交,所以計數值加 1 這個操作對會話 B 還不可見。因此,會話 B 看到的結果里, 查計數值和“最近 100 條記錄”看到的結果,邏輯上就是一致的。

不同的 count 用法

Q:在 select count(?) from t 這樣的查詢語句裡面,count(*)、count(主鍵 id)、count(欄位) 和 count(1) 等不同用法的性能,有哪些差別?

A:

count() 的語義

count() 是一個聚合函數對於返回的結果集,一行行地判斷,如果 count 函數的參數不是 NULL,累計值就加 1,否則不加。最後返回累計值。

所以,count(*)、count(主鍵 id) 和 count(1) 都表示返回滿足條件的結果集的總行數;而 count(欄位),則表示返回滿足條件的數據行裡面,參數“欄位”不為 NULL 的總個數

分析性能差別的原則

  1. server 層要什麼就給什麼;
  2. InnoDB 只給必要的值;
  3. 現在的優化器只優化了 count(*) 的語義為“取行數”,其他“顯而易見”的優化並沒有做。

對於 count(主鍵 id) 來說,InnoDB 引擎會遍歷整張表,把每一行的 id 值都取出來,返回給 server 層。server 層拿到 id 後,判斷是不可能為空的,就按行累加。

對於 count(1) 來說,InnoDB 引擎遍歷整張表,但不取值server 層對於返回的每一行,放一個數字“1”進去,判斷是不可能為空的,按行累加。

單看這兩個用法的差別的話,count(1) 執行得要比 count(主鍵 id) 快。因為從引擎返回 id 會涉及到解析數據行,以及拷貝欄位值的操作

對於 count(欄位) 來說

  1. 如果這個“欄位”是定義為 not null 的話,一行行地從記錄裡面讀出這個欄位,判斷不能為 null,按行累加;
  2. 如果這個“欄位”定義允許為 null,那麼執行的時候,判斷到有可能是 null,還要把值取出來再判斷一下,不是 null 才累加。

也就是前面的第一條原則,server 層要什麼欄位,InnoDB 就返回什麼欄位。

但是 count(*) 是例外,並不會把全部欄位取出來,而是專門做了優化,不取值。count(*) 肯定不是 null,按行累加。

Q:優化器就不能自己判斷一下嗎,主鍵 id 肯定非空啊,為什麼不能按照 count(*) 來處理

A:MySQL 專門針對這個語句進行優化,也不是不可以。但是這種需要專門優化的情況太多了,而且 MySQL 已經優化過 count(*) 了,直接使用這種用法就可以了。

結論:

按照效率排序的話,count(欄位)<count(主鍵 id)<count(1)count(*),所以我建議你,儘量使用 count(*)

Q:先前用事務來確保計數準確。由於事務可以保證中間結果不被別的事務讀到,因此修改計數值和插入新記錄的順序是不影響邏輯結果的。但是,從併發系統性能的角度考慮,你覺得在這個事務序列里,應該先插入操作記錄(insert into t),還是應該先更新計數表(update cnt_t)呢?

A:

併發系統性能的角度考慮,應該先插入操作記錄,再更新計數表。

  • 更新計數表涉及到行鎖的競爭,先插入再更新能最大程度地減少事務之間的鎖等待,提升併發度。

PS:計數表保存了多個業務表的計數值不會導致行鎖等待。

用一個計數表記錄多個業務表的行數,也肯定會給表名欄位加唯一索引。類似於下麵這樣的表結構:

CREATE TABLE `rows_stat` (
  `table_name` varchar(64) NOT NULL,
  `row_count` int(10) unsigned NOT NULL,
  PRIMARY KEY (`table_name`)
) ENGINE=InnoDB;

在更新計數表的時候,一定會傳入where table_name=$table_name,使用主鍵索引,更新加行鎖只會鎖在一行上而在不同業務表插入數據,是更新不同的行,不會有行鎖。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 各位讀者好,新版本的系統平臺低功耗藍牙開發介面庫已發佈,提供BLE設備的枚舉、掃描、連接和檢測,藍牙服務和特征的枚舉、讀寫、訂閱,控制器版本查詢等API函數。支持Windows、Android等系統。鏈接:WCHBleLib_MultiOS.ZIP - 南京沁恆微電子股份有限公司 前言 上一篇文章中 ...
  • 鏡像下載、功能變數名稱解析、時間同步請點擊 阿裡雲開源鏡像站 一、Git LFS Git Large File Storage (LFS) 使用 Git 內部的文本指針替換音頻樣本、視頻、數據集和圖形等大文件,同時將文件內容存儲在 GitHub.com 或 GitHub Enterprise 等遠程伺服器上 ...
  • 一、TTY介紹 當前主要有三種類型tty設備及驅動程式:控制台、pty和串口。其中控制台和pty可看作為虛擬tty設備,物理tty設備包含串口、USB轉串口、SPI轉串口等。 二、TTY設備及驅動信息查看 確定tty設備及驅動程式類型可查閱/proc/tty/drivers文件。如下所示: 文件從左 ...
  • 在嵌入式應用領域中,串口是最為常見的一種硬體通信介面。因為其具備協議簡單,硬體電路精簡等優勢使得串口基本成為MCU、電腦或嵌入式產品的標配介面。本文僅介紹在Linux系統下串口編程需要使用的API和一些應用技巧,關於串口的背景知識介紹,以及Windows系統下串口編程讀者可以移步至其他文章。 Li ...
  • Things3 for Mac是Mac平臺上一款非常優秀的任務管理軟體,嚴格按照GTD流程來規劃人們的任務安排,設計方式也和很多其他的應用程式有很大差別。things3 mac版是一個功能強大,易於使用的任務管理應用程式,可幫助您輸入,組織和處理待辦事項列表中的項目。基於流行的Getting Thi ...
  • 17 | 如何正確地顯示隨機消息? 場景:從一個單詞表中隨機選出三個單詞。 表的建表語句和初始數據的命令如下,在這個表裡面插入了 10000 行記錄: CREATE TABLE `words` ( `id` int(11) NOT NULL AUTO_INCREMENT, `word` varcha ...
  • 16 | “order by”是怎麼工作的? 以市民表為例,假設要查詢城市是“杭州”的所有人名字,並且按照姓名排序返回前 1000 個人的姓名、年齡。 這個表的部分定義: CREATE TABLE `t` ( `id` int(11) NOT NULL, `city` varchar(16) NOT ...
  • 15 | 答疑文章(一):日誌和索引相關問題 日誌相關 binlog(歸檔日誌)和redo log(重做日誌)配合崩潰恢復,在兩階段提交的不同瞬間,MySQL如果發生異常重啟,是怎麼保證數據完整性的? Q:這個圖不是一個update 語句的執行流程嗎,怎麼還會調用 commit 語句? A: 兩個“ ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...