淺析MySQL中的資料儲存結構
本篇文章主要從InnoDB資料儲存結構的角度分析,在何種情況下,SQL查詢效率會降低。 常在網路上看到一些文章在吐槽,資料量大的情況下,查詢效率會降低很多。表關聯的多的時候,查詢效率會降低。單表資料量不要超過百萬等等。
資料庫版本: 8.0 引擎:InnoDB 參考資料:掘金小冊 《從根上理解Mysql》,有時間的建議親自看一下。
範例表:
CREATE TABLE `hospital_info` ( `pk_id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `id` varchar(36) NOT NULL COMMENT '外键', `hospital_code` varchar(36) NOT NULL COMMENT '医院编码', `hospital_name` varchar(36) NOT NULL COMMENT '医院名称', `is_deleted` tinyint DEFAULT NULL COMMENT '是否删除 0否 1是', `gmt_created` datetime DEFAULT NULL COMMENT '创建时间', `gmt_modified` datetime DEFAULT NULL COMMENT 'gmt_modified', `gmt_deleted` datetime(3) DEFAULT '9999-12-31 23:59:59.000' COMMENT '删除时间', PRIMARY KEY (`pk_id`), KEY `hospital_code` (`hospital_code`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='医院信息';
InnoDB 行格式
從一行資料開始看起,先了解單行資料的儲存格式。
目前行格式有4種,分別是Compact、Redundant、Dynamic、Compressed行格式。
在建立表格的時候一般不需要刻意指定,5.7以上的版本會預設Dynamic。
每種行格式大同小異,這裡以Compact作為一個範例,簡單的了解一下,每行資料是如何記錄的。
如上圖所示。 分為「額外資訊」和「真實資料」兩個部分。
變長欄位清單
這個比較有意思,一般在定義欄位的時候都需要指定欄位的類型和長度,
例如:範例表中的hospital_code欄位定義VARCHAR(36)。在實際使用中hospital_code欄位長度只用了32位元。
那剩下的4個字元長度會怎麼辦?若強行填入空字符,豈不是白白浪費4個字符的記憶體。若不填充,怎麼判斷目前欄位到底保存了多少個字元?佔用多少記憶體?
此時,變長欄位清單就會依照欄位反序,用1~2個位元組,記錄每個變長欄位實際的長度。這樣可以有效的利用記憶體空間。
與之類似的欄位:VARBINARY、各種TEXT類型,各種BLOB類型。
相對的也存在“定長字段”,例如:CHAR(10),該類型的字段,在初始化的時候就會默認佔用指定字元長度的空間,若不夠則填入空字符,因此對空間上是比較浪費的,一般建議按需設定長度。
當然「變長欄位清單」不是必定存在的,若定義的欄位類型沒有「變長欄位」則不會有。
拓展:對於TEXT或BLOB類型的字段,長度很可能一頁無法存下,這時會將大部分資料記錄在其他頁中,在當前記錄中保留下一頁資料的位址。
NULL值清單
在實際儲存資料的時候,某些欄位可能儲存的是NULL值,如果這些值都記錄在真實的資料中,則會浪費儲存空間。在Compact格式中,會把這些值為NULL的欄位統一管理,儲存到NULL值清單中。
若一行資料中,沒有欄位為NULL則不會產生此欄位。
儲存的方式也比較有意思,是二進位方式倒序記錄。
以範例表分析,表中存在is_deleted、gmt_created、gmt_modified三個欄位可能為空。假設在一筆記錄中gmt_created、gmt_modified都為空,那對應到NULL值清單中應該是下面的樣子。
拓展: Mysql是支援二進位資料儲存的,充分利用,可以減少很大的儲存空間。
記錄頭資訊
記錄頭資訊由固定的5個字元組成,即40個二進位位長度。
先作為一個了解,這裡有一個比較有意思標識:delete_mask用過redis的都知道,redis的中被刪除的資料不會被立刻清除,相同的mysql中也一樣,被刪除的資料不會立刻被清理,因為清理的過程會引發IO操作,這是很影響效率的。 被刪除的資料會組成一個鍊錶,想當與一個可重複使用的空間。
記錄真實資料資料
這個其實沒啥好說的,就是記錄真實的非NULL資料。
有一個網路上常能看到的問題:若沒有設定主鍵會怎樣?
InnoDB下,主鍵是一筆記錄的唯一標識,如果使用者沒有指定,mysql會從Unique(唯一)鍵中選取一個作為主鍵,如果沒有Unique鍵,則會新增一個名為row_id隱藏列,作為主鍵。
此外也會加入新增 transaction_id(事務ID) 和 roll_pointer(回滾指標) 這兩個欄位。
小結
4種行格式大同小異,就不一一介紹了,都分為「額外資訊」和「真實資料」兩個部分。差異主要在與「額外資訊」記錄的內容,以及變長欄位的保存上的些許不同。
InnoDB資料頁
資料頁的概念,相信已經耳熟能詳了。它是InnoDB管理儲存空間的基本單位,單頁大小一般是16KB。根據不同的目的設計了許多不同類型的頁,如:存放表空間頭部資訊的頁,存放Insert Buffer資訊的頁,存放INODE資訊的頁,存放undo日誌資訊的頁面等等。
頁空間劃分如下:
總共7個組成部分,大致描述一下7個部分。
其中File header和Page header中的屬性非常多,這裡不一一介紹,只要知道這兩個地方記錄頁的一些屬性,例如:頁號,上一頁和下一頁的頁號,頁的類型,以及頁的記憶體佔用等等。這裡說一下,頁與頁之間是雙向鍊錶進行連接的。資料記錄是單項鍊表。
File Trailer是校驗頁資料完整性的,當頁資料從記憶體重新寫入磁碟的時候需要校驗,防止資料頁損壞。
聚焦在下User Records(已使用空間)和Free Space(剩餘空間),這裡是儲存真實的資料記錄。
此外 Infimum 和 Supremum,分別標識最小記錄和最大記錄。即一個頁產生的時候,就預設包含這兩筆記錄,不過不用擔心這兩筆記錄只是作為資料鍊錶的頭和尾,不影響真實資料。
綜上,記錄在頁中的儲存如下:
簡單的來說,就是Free Space到User Records的轉換,當Free Space耗盡時則視為資料頁已經滿了。
到此,資料已經寫入了資料頁中。那該怎麼取出呢?上面知道了資料記錄是單項鍊表組成的,難道要從Infimum(最小)記錄開始沿著鍊錶遍歷嗎?
顯然,mysql的開發大佬不可能這麼蠢,否則我上我也行,哈哈。
這裡就要提到 Page Directory(頁目錄)了。在頁中,對資料進行了分組,每組最後一筆記錄的位址偏移量單獨提取出來按順序儲存到靠近頁尾的「頁目錄」中,頁目錄中的這些位址偏移量稱為“插槽”,此外最後一筆記錄頭部(n_owned)還要保存所在分組中有多少筆記錄。
頁目錄是由一個個的槽組成的。
整體結構圖如下:
有了目錄之後,查詢就比較簡單了。可以使用二分法進行快查。上圖中,知道最小槽為0,最大為4. 舉個栗子:
假設要查詢主鍵記錄為6的資料。
1)計算中間槽位置即(0 4)/ 2 = 2。取出槽對應的記錄主鍵為8,因為8>6。
2)同理,將最大的槽設為2,即(0 2)/2 =1,槽1對應的主鍵為4,因為4
為了方便後續的描述,將頁的資料形式簡化為如下圖所示的樣子。
B 樹索引
不妨思考一個問題,前面說了。資料頁之間使用的是雙向鍊錶連結的,大致如下圖所示:上圖可以看能出頁號並非連續的,也不一定是連續的內存空間(記住這句話後面會說到)。
假設每頁能存放3筆記錄,現在有10w筆記錄需要保存,則需要3w多個資料頁,此時會面對和單頁資料過多一樣的查詢問題,總不能逐一遍歷吧。此時也需要一個能快速快速查詢的目錄,而這個目錄就是「索引」。
在上圖所示的資料頁基礎上,可以形成如下的索引結構:這種就是常說的叢集索引,葉子即資料。這裡要注意的一點,「頁30」中存放的是主鍵以及其所在的頁號。
如果說單一索引頁滿了,則會進行分裂。產生如下所示的樹狀結構。
不過上圖為了標示方便,是不完全準確的。應該是先產生一個根節點,當根節點滿了,則會進行分裂。根節點則記錄分裂後的索引頁資訊。
簡單的來說就跟樹木成長一樣,先從根部再到樹幹、樹枝、樹葉等。
二級索引與叢集索引的想法是一樣的,差別在於二級索引的葉子節點不是真實數據,而是資料的主鍵。需要進行回表操作才能取得真實資料。
表空間
到目前為止,已經知道單一資料的儲存結構,以及最小的儲存資料單元頁。資料頁之間透過雙向鍊錶進行連接,且資料頁之間是不一定連續的。
此時,產生了一個問題,同一個表的記錄,如果所在的頁在記憶體位址上相距太遠怎麼辦? 設想為了找3個人,他們分別再北京、紐約、倫敦。你要挨個去找,中間要浪費大量的時間在旅途中。如果把他們聚集在一個國家,甚至一個城市,那就快很多。
於是區的概念誕生了。區是由連續的64個頁組成,預設情況下一個區佔用1M的記憶體。在申請記憶體的時候,一次佔用1M的空間,其中的資料頁都是相鄰的,一定程度上解決了隨機IO的問題。
在區的基礎上,為了更有效的提升查詢效率,將B 樹的葉子節點和非葉子節點記錄在不同的區中,這些區的集合被成為「段(segment )」。 在此概念下,插入第一筆記錄,就需要申請2個區空間,一個聚集索引根節點,一個資料頁,這次就需要申請2M的空間! 啥也沒乾呢,2M空間就沒了,這合理嗎?顯然,這很不合理。
因此又搞出一個"碎片區"的概念。碎片區直屬於表空間,不屬於任何一段。分配記憶體的流程轉變成:
1)剛開始插入資料時,從碎片區以單一頁面來分配儲存空間。
2)當某個段落已經佔用了32個碎片區頁面後,就會以完整的區來分配空間。
表空間也分為:系統表空間和獨立表空間,此外還有區的XDES Entry資料結構。內容過多且複雜,需要了解的可以去看原書。
思考
1)索引越多越好嗎?多了會有 什麼影響?
那肯定不是越多越好,上面可以知道,索引的記錄也是需要記憶體損耗的。每個索引都會對應一個B 樹,每個樹有需要2個段分別記錄葉子節點和非葉子節點。這麼下來會帶來很多記憶體的浪費。 只是這樣的話也不是不能接受,畢竟索引本身的意義就是用空間換時間。但我們要知道,資料的增刪改,會導致索引的變化,需要索引重新分配節點,以及頁記憶體的回收分配。這些都是IO操作,若索引過多,勢必導致效能的降低。
因此合理的利用聯合索引,可以解決單一索引過多的問題。此外索引有長度限制,過長的欄位不適合作為索引。
2)索引為何查詢效率這麼高?
這個其實屬於演算法問題,以叢集索引為例,假設非葉子節點的索引頁,每個能記錄1000條數據,葉子節點每個能記錄500條數據,一個3層的B 樹(不算根節點),能存放10001000500筆記錄。一個3層結構的索引能存放這麼多記錄,每次只需幾次查詢就能定位數據,效率自然也就高了。
實際上單一索引頁所能記錄的資料比這大的多。
同樣的這裡可以思考一個問題,若葉子節點中的單一資料非常大,大到一個資料頁只能存放3筆記錄,這時B 樹的深度就會增加,因此合理的減少表中單一記錄的大小,也是一種最佳化。
3)資料量大,sql會執行緩慢?
其實這個問題真的很想吐槽,動不動就百萬資料查詢效率xx秒,太慢了。不否認mysql的效能的確弱於一些資料庫,但是百萬的資料量就慢的,想想自己的SQL和表格結構設計是否合理。別說百萬級,就是千萬級的也能實現毫秒級的查詢。 只談數量都是扯淡,要實際看看鎖佔用的記憶體大小,若你的表中有上百個字段,或者存在字符超長的字段。那麼神仙也救不了你。
總結
文章主要介紹MySql資料結構的概念,大部分內容都來自於《從根上理解Mysql》一書。做了很多簡化,可以作為基礎來了解一些概念。
如有錯漏,感謝指正。
【相關推薦:mysql影片教學】
#以上是淺析MySQL中的資料儲存結構的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Apache 連接數據庫需要以下步驟:安裝數據庫驅動程序。配置 web.xml 文件以創建連接池。創建 JDBC 數據源,指定連接設置。從 Java 代碼中使用 JDBC API 訪問數據庫,包括獲取連接、創建語句、綁定參數、執行查詢或更新以及處理結果。

MySQL在數據庫和編程中的地位非常重要,它是一個開源的關係型數據庫管理系統,廣泛應用於各種應用場景。 1)MySQL提供高效的數據存儲、組織和檢索功能,支持Web、移動和企業級系統。 2)它使用客戶端-服務器架構,支持多種存儲引擎和索引優化。 3)基本用法包括創建表和插入數據,高級用法涉及多表JOIN和復雜查詢。 4)常見問題如SQL語法錯誤和性能問題可以通過EXPLAIN命令和慢查詢日誌調試。 5)性能優化方法包括合理使用索引、優化查詢和使用緩存,最佳實踐包括使用事務和PreparedStatemen

MySQL在Web應用中的主要作用是存儲和管理數據。 1.MySQL高效處理用戶信息、產品目錄和交易記錄等數據。 2.通過SQL查詢,開發者能從數據庫提取信息生成動態內容。 3.MySQL基於客戶端-服務器模型工作,確保查詢速度可接受。

在 Docker 中啟動 MySQL 的過程包含以下步驟:拉取 MySQL 鏡像創建並啟動容器,設置根用戶密碼並映射端口驗證連接創建數據庫和用戶授予對數據庫的所有權限

Laravel 是一款 PHP 框架,用於輕鬆構建 Web 應用程序。它提供一系列強大的功能,包括:安裝: 使用 Composer 全局安裝 Laravel CLI,並在項目目錄中創建應用程序。路由: 在 routes/web.php 中定義 URL 和處理函數之間的關係。視圖: 在 resources/views 中創建視圖以呈現應用程序的界面。數據庫集成: 提供與 MySQL 等數據庫的開箱即用集成,並使用遷移來創建和修改表。模型和控制器: 模型表示數據庫實體,控制器處理 HTTP 請求。

優雅安裝 MySQL 的關鍵在於添加 MySQL 官方倉庫。具體步驟如下:下載 MySQL 官方 GPG 密鑰,防止釣魚攻擊。添加 MySQL 倉庫文件:rpm -Uvh https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm更新 yum 倉庫緩存:yum update安裝 MySQL:yum install mysql-server啟動 MySQL 服務:systemctl start mysqld設置開機自啟動

在開發一個小型應用時,我遇到了一個棘手的問題:需要快速集成一個輕量級的數據庫操作庫。嘗試了多個庫後,我發現它們要么功能過多,要么兼容性不佳。最終,我找到了minii/db,這是一個基於Yii2的簡化版本,完美地解決了我的問題。

在 CentOS 上安裝 MySQL 涉及以下步驟:添加合適的 MySQL yum 源。執行 yum install mysql-server 命令以安裝 MySQL 服務器。使用 mysql_secure_installation 命令進行安全設置,例如設置 root 用戶密碼。根據需要自定義 MySQL 配置文件。調整 MySQL 參數和優化數據庫以提升性能。
